From 73e4bdc6b60926584657fedf437856d8b025b378 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 13 Feb 2017 15:06:59 -0500 Subject: [PATCH] INT-4227: (4.3) Allow Custom Messaging Annotations JIRA: https://jira.spring.io/browse/INT-4227 Expose `MessagingAnnotationPostProcessor` for inheritors. The `CustomMessagingAnnotationTests` demonstrates how custom Messaging Annotation can be registered and used **Cherry-pick to 4.3.x** Add `setupCustomPostProcessors()` and `addMessagingAnnotationPostProcessor` to the `MessagingAnnotationPostProcessor` Expose some API for inheritors Conflicts: spring-integration-core/src/main/java/org/springframework/integration/config/annotation/MessagingAnnotationPostProcessor.java --- ...AbstractMethodAnnotationPostProcessor.java | 4 +- .../MessagingAnnotationPostProcessor.java | 170 ++++++++++-------- .../CustomMessagingAnnotationTests.java | 164 +++++++++++++++++ 3 files changed, 265 insertions(+), 73 deletions(-) create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/config/annotation/CustomMessagingAnnotationTests.java diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AbstractMethodAnnotationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AbstractMethodAnnotationPostProcessor.java index 0097388a15..410cca5c63 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AbstractMethodAnnotationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AbstractMethodAnnotationPostProcessor.java @@ -245,7 +245,7 @@ public abstract class AbstractMethodAnnotationPostProcessor extractAdviceChain(String beanName, List annotations) { + protected List extractAdviceChain(String beanName, List annotations) { List adviceChain = null; String[] adviceChainNames = MessagingAnnotationUtils.resolveAttribute(annotations, ADVICE_CHAIN_ATTRIBUTE, String[].class); @@ -447,7 +447,7 @@ public abstract class AbstractMethodAnnotationPostProcessor H extractTypeIfPossible(Object targetObject, Class expectedType) { + protected H extractTypeIfPossible(Object targetObject, Class expectedType) { if (targetObject == null) { return null; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/MessagingAnnotationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/MessagingAnnotationPostProcessor.java index aedf96f5b3..89120f871d 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/MessagingAnnotationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/MessagingAnnotationPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,6 +58,7 @@ import org.springframework.integration.util.MessagingAnnotationUtils; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.ReflectionUtils; @@ -75,7 +76,7 @@ import org.springframework.util.StringUtils; public class MessagingAnnotationPostProcessor implements BeanPostProcessor, BeanFactoryAware, InitializingBean, SmartInitializingSingleton { - private final Log logger = LogFactory.getLog(this.getClass()); + protected final Log logger = LogFactory.getLog(this.getClass()); // NOSONAR private final Map, MethodAnnotationPostProcessor> postProcessors = new HashMap, MethodAnnotationPostProcessor>(); @@ -121,6 +122,20 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean new InboundChannelAdapterAnnotationPostProcessor(this.beanFactory)); this.postProcessors.put(BridgeFrom.class, new BridgeFromAnnotationPostProcessor(this.beanFactory)); this.postProcessors.put(BridgeTo.class, new BridgeToAnnotationPostProcessor(this.beanFactory)); + Map, MethodAnnotationPostProcessor> customPostProcessors = + setupCustomPostProcessors(); + if (!CollectionUtils.isEmpty(customPostProcessors)) { + this.postProcessors.putAll(customPostProcessors); + } + } + + protected Map, MethodAnnotationPostProcessor> setupCustomPostProcessors() { + return null; + } + + public void addMessagingAnnotationPostProcessor(Class annotation, + MethodAnnotationPostProcessor postProcessor) { + this.postProcessors.put(annotation, postProcessor); } @Override @@ -152,85 +167,90 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean return bean; } - ReflectionUtils.doWithMethods(beanClass, new ReflectionUtils.MethodCallback() { + ReflectionUtils.doWithMethods(beanClass, + new ReflectionUtils.MethodCallback() { - @Override - @SuppressWarnings({"unchecked", "rawtypes"}) - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - Map, List> annotationChains = - new HashMap, List>(); - for (Class annotationType : - MessagingAnnotationPostProcessor.this.postProcessors.keySet()) { - if (AnnotatedElementUtils.isAnnotated(method, annotationType.getName())) { - List annotationChain = getAnnotationChain(method, annotationType); - if (annotationChain.size() > 0) { - annotationChains.put(annotationType, annotationChain); - } - } - } - - for (Map.Entry, List> entry : annotationChains.entrySet()) { - Class annotationType = entry.getKey(); - List annotations = entry.getValue(); - MethodAnnotationPostProcessor postProcessor = - MessagingAnnotationPostProcessor.this.postProcessors.get(annotationType); - if (postProcessor != null && postProcessor.shouldCreateEndpoint(method, annotations)) { - Method targetMethod = method; - if (AopUtils.isJdkDynamicProxy(bean)) { - try { - targetMethod = bean.getClass().getMethod(method.getName(), method.getParameterTypes()); - } - catch (NoSuchMethodException e) { - throw new IllegalArgumentException("Service methods must be extracted to the service " - + "interface for JdkDynamicProxy. The affected bean is: '" + beanName + "' " - + "and its method: '" + method + "'", e); - } - } - Object result = postProcessor.postProcess(bean, beanName, targetMethod, annotations); - if (result != null && result instanceof AbstractEndpoint) { - AbstractEndpoint endpoint = (AbstractEndpoint) result; - String autoStartup = MessagingAnnotationUtils.resolveAttribute(annotations, "autoStartup", - String.class); - if (StringUtils.hasText(autoStartup)) { - autoStartup = getBeanFactory().resolveEmbeddedValue(autoStartup); - if (StringUtils.hasText(autoStartup)) { - endpoint.setAutoStartup(Boolean.parseBoolean(autoStartup)); + @Override + public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + Map, List> annotationChains = + new HashMap, List>(); + for (Class annotationType : + MessagingAnnotationPostProcessor.this.postProcessors.keySet()) { + if (AnnotatedElementUtils.isAnnotated(method, annotationType.getName())) { + List annotationChain = getAnnotationChain(method, annotationType); + if (annotationChain.size() > 0) { + annotationChains.put(annotationType, annotationChain); } } + } - String phase = MessagingAnnotationUtils.resolveAttribute(annotations, "phase", String.class); - if (StringUtils.hasText(phase)) { - phase = getBeanFactory().resolveEmbeddedValue(phase); - if (StringUtils.hasText(phase)) { - endpoint.setPhase(Integer.parseInt(phase)); - } - } - - String endpointBeanName = generateBeanName(beanName, method, annotationType); - endpoint.setBeanName(endpointBeanName); - getBeanFactory().registerSingleton(endpointBeanName, endpoint); - getBeanFactory().initializeBean(endpoint, endpointBeanName); - - Role role = AnnotationUtils.findAnnotation(method, Role.class); - if (role != null) { - MessagingAnnotationPostProcessor.this.lazyLifecycleRoles.add(role.value(), - endpointBeanName); - } + for (Entry, List> entry : annotationChains.entrySet()) { + Class annotationType = entry.getKey(); + List annotations = entry.getValue(); + processAnnotationTypeOnMethod(bean, beanName, method, annotationType, annotations); } } - } - } - }, ReflectionUtils.USER_DECLARED_METHODS); + }, ReflectionUtils.USER_DECLARED_METHODS); return bean; } + protected void processAnnotationTypeOnMethod(Object bean, String beanName, Method method, + Class annotationType, List annotations) { + MethodAnnotationPostProcessor postProcessor = + MessagingAnnotationPostProcessor.this.postProcessors.get(annotationType); + if (postProcessor != null && postProcessor.shouldCreateEndpoint(method, annotations)) { + Method targetMethod = method; + if (AopUtils.isJdkDynamicProxy(bean)) { + try { + targetMethod = bean.getClass().getMethod(method.getName(), method.getParameterTypes()); + } + catch (NoSuchMethodException e) { + throw new IllegalArgumentException("Service methods must be extracted to the service " + + "interface for JdkDynamicProxy. The affected bean is: '" + beanName + "' " + + "and its method: '" + method + "'", e); + } + } + Object result = postProcessor.postProcess(bean, beanName, targetMethod, annotations); + if (result != null && result instanceof AbstractEndpoint) { + AbstractEndpoint endpoint = (AbstractEndpoint) result; + String autoStartup = MessagingAnnotationUtils.resolveAttribute(annotations, "autoStartup", + String.class); + if (StringUtils.hasText(autoStartup)) { + autoStartup = getBeanFactory().resolveEmbeddedValue(autoStartup); + if (StringUtils.hasText(autoStartup)) { + endpoint.setAutoStartup(Boolean.parseBoolean(autoStartup)); + } + } + + String phase = MessagingAnnotationUtils.resolveAttribute(annotations, "phase", String.class); + if (StringUtils.hasText(phase)) { + phase = getBeanFactory().resolveEmbeddedValue(phase); + if (StringUtils.hasText(phase)) { + endpoint.setPhase(Integer.parseInt(phase)); + } + } + + String endpointBeanName = generateBeanName(beanName, method, annotationType); + endpoint.setBeanName(endpointBeanName); + getBeanFactory().registerSingleton(endpointBeanName, endpoint); + getBeanFactory().initializeBean(endpoint, endpointBeanName); + + Role role = AnnotationUtils.findAnnotation(method, Role.class); + if (role != null) { + MessagingAnnotationPostProcessor.this.lazyLifecycleRoles.add(role.value(), + endpointBeanName); + } + } + } + } + /** * @param method the method. * @param annotationType the annotation type. * @return the hierarchical list of annotations in top-bottom order. */ - private List getAnnotationChain(Method method, Class annotationType) { + protected List getAnnotationChain(Method method, Class annotationType) { Annotation[] annotations = AnnotationUtils.getAnnotations(method); List annotationChain = new LinkedList(); Set visited = new HashSet(); @@ -244,8 +264,8 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean return annotationChain; } - private boolean recursiveFindAnnotation(Class annotationType, Annotation ann, - List annotationChain, Set visited) { + protected boolean recursiveFindAnnotation(Class annotationType, Annotation ann, + List annotationChain, Set visited) { if (ann.annotationType().equals(annotationType)) { annotationChain.add(ann); return true; @@ -263,13 +283,13 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean return false; } - private Class getBeanClass(Object bean) { + protected Class getBeanClass(Object bean) { Class targetClass = AopUtils.getTargetClass(bean); return (targetClass != null) ? targetClass : bean.getClass(); } - private String generateBeanName(String originalBeanName, Method method, - Class annotationType) { + protected String generateBeanName(String originalBeanName, Method method, + Class annotationType) { String baseName = originalBeanName + "." + method.getName() + "." + ClassUtils.getShortNameAsProperty(annotationType); String name = baseName; @@ -280,4 +300,12 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean return name; } + protected Map, MethodAnnotationPostProcessor> getPostProcessors() { + return this.postProcessors; + } + + protected MultiValueMap getLazyLifecycleRoles() { + return this.lazyLifecycleRoles; + } + } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/CustomMessagingAnnotationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/CustomMessagingAnnotationTests.java new file mode 100644 index 0000000000..d174d17b23 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/CustomMessagingAnnotationTests.java @@ -0,0 +1,164 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.config.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.context.IntegrationContextUtils; +import org.springframework.integration.handler.LoggingHandler; +import org.springframework.integration.handler.MethodInvokingMessageProcessor; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.integration.util.MessagingAnnotationUtils; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author Artem Bilan + * + * @since 4.3.8 + */ +@RunWith(SpringRunner.class) +public class CustomMessagingAnnotationTests { + + @Autowired(required = false) + @Qualifier("customMessagingAnnotationTests.Config.logger.logging.handler") + private LoggingHandler loggingHandler; + + @Autowired + private MessageChannel loggingChannel; + + @Test + public void testLogAnnotation() { + assertNotNull(this.loggingHandler); + + Log log = spy(TestUtils.getPropertyValue(this.loggingHandler, "messageLogger", Log.class)); + + given(log.isWarnEnabled()) + .willReturn(true); + + new DirectFieldAccessor(this.loggingHandler) + .setPropertyValue("messageLogger", log); + + this.loggingChannel.send(MessageBuilder.withPayload("foo") + .setHeader("bar", "baz") + .build()); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); + + verify(log) + .warn(argumentCaptor.capture()); + + assertEquals("foo for baz", argumentCaptor.getValue()); + } + + @Configuration + @EnableIntegration + public static class Config { + + @Bean(name = IntegrationContextUtils.MESSAGING_ANNOTATION_POSTPROCESSOR_NAME) + public static MessagingAnnotationPostProcessor messagingAnnotationPostProcessor( + ConfigurableListableBeanFactory beanFactory) { + + MessagingAnnotationPostProcessor messagingAnnotationPostProcessor = new MessagingAnnotationPostProcessor(); + messagingAnnotationPostProcessor. + addMessagingAnnotationPostProcessor(Logging.class, new LogAnnotationPostProcessor(beanFactory)); + return messagingAnnotationPostProcessor; + } + + @Logging(value = "loggingChannel", level = LoggingHandler.Level.WARN) + public String logger(Message message) { + return message.getPayload() + " for " + message.getHeaders().get("bar"); + } + + } + + @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @Documented + public @interface Logging { + + String value(); + + + LoggingHandler.Level level() default LoggingHandler.Level.INFO; + + } + + private static class LogAnnotationPostProcessor extends AbstractMethodAnnotationPostProcessor { + + LogAnnotationPostProcessor(ConfigurableListableBeanFactory beanFactory) { + super(beanFactory); + } + + @Override + protected String getInputChannelAttribute() { + return "value"; + } + + @Override + protected MessageHandler createHandler(Object bean, Method method, List annotations) { + LoggingHandler.Level level = MessagingAnnotationUtils.resolveAttribute(annotations, "level", + LoggingHandler.Level.class); + LoggingHandler loggingHandler = new LoggingHandler(level.name()); + MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor<>(bean, method); + loggingHandler.setLogExpression(new LiteralExpression(null) { + + @Override + public String getValue(EvaluationContext context, Object rootObject) throws EvaluationException { + return processor.processMessage((Message) rootObject); + } + + }); + return loggingHandler; + } + + } + +}