diff --git a/build.gradle b/build.gradle index 30beb50788..db6b50dc99 100644 --- a/build.gradle +++ b/build.gradle @@ -80,7 +80,7 @@ subprojects { subproject -> javaxMailVersion = '1.4.7' jmsApiVersion = '1.1-rev-1' jpaApiVersion = '2.0.0' - jrubyVersion = '1.7.8' + jrubyVersion = '1.7.12' jschVersion = '0.1.51' jsonpathVersion = '0.9.1' junitVersion = '4.11' diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/AnnotationFinder.java b/spring-integration-core/src/main/java/org/springframework/integration/config/AnnotationFinder.java deleted file mode 100644 index 1caefbdbe1..0000000000 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/AnnotationFinder.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2002-2013 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; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.concurrent.atomic.AtomicReference; - -import org.springframework.aop.support.AopUtils; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.util.ClassUtils; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.ReflectionUtils.MethodCallback; - -/** - * Helper to provide common features for inspecting objects and locating annotated methods. - * - * @author Dave Syer - * @author Gunnar Hillert - * @author Soby Chacko - * - */ -abstract class AnnotationFinder { - - public static Method findAnnotatedMethod(Object target, final Class annotationType) { - final AtomicReference reference = new AtomicReference(); - ReflectionUtils.doWithMethods(getTargetClass(target), new MethodCallback() { - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - if (AnnotationUtils.findAnnotation(method, annotationType) != null) { - reference.set(method); - } - } - }); - return reference.get(); - } - - private static Class getTargetClass(Object targetObject) { - Class targetClass = targetObject.getClass(); - if (AopUtils.isAopProxy(targetObject)) { - targetClass = AopUtils.getTargetClass(targetObject); - } - else if (ClassUtils.isCglibProxyClass(targetClass)) { - Class superClass = targetObject.getClass().getSuperclass(); - if (!Object.class.equals(superClass)) { - targetClass = superClass; - } - } - return targetClass; - } - -} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/CorrelationStrategyFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/config/CorrelationStrategyFactoryBean.java index aabc154b6b..75d1aef62c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/CorrelationStrategyFactoryBean.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/CorrelationStrategyFactoryBean.java @@ -22,6 +22,7 @@ import org.springframework.integration.IntegrationMessageHeaderAccessor; import org.springframework.integration.aggregator.CorrelationStrategy; import org.springframework.integration.aggregator.HeaderAttributeCorrelationStrategy; import org.springframework.integration.aggregator.MethodInvokingCorrelationStrategy; +import org.springframework.integration.config.annotation.MessagingAnnotationUtils; import org.springframework.util.StringUtils; /** @@ -60,7 +61,8 @@ public class CorrelationStrategyFactoryBean implements FactoryBean componentRegistrars = new HashMap(); private ResourceLoader resourceLoader; + private ClassLoader classLoader; + public IntegrationComponentScanRegistrar() { this.componentRegistrars.put(new AnnotationTypeFilter(MessagingGateway.class, true), new MessagingGatewayRegistrar()); } @@ -56,6 +61,11 @@ public class IntegrationComponentScanRegistrar implements ImportBeanDefinitionRe this.resourceLoader = resourceLoader; } + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { Map componentScan = importingClassMetadata @@ -84,7 +94,23 @@ public class IntegrationComponentScanRegistrar implements ImportBeanDefinitionRe @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { - return beanDefinition.getMetadata().isIndependent(); + if (beanDefinition.getMetadata().isIndependent()) { + // TODO until SPR-11711 will be resolved + if (beanDefinition.getMetadata().isInterface() && + beanDefinition.getMetadata().getInterfaceNames().length == 1 && + Annotation.class.getName().equals(beanDefinition.getMetadata().getInterfaceNames()[0])) { + try { + Class target = ClassUtils.forName(beanDefinition.getMetadata().getClassName(), classLoader); + return !target.isAnnotation(); + } + catch (Exception e) { + logger.error("Could not load target class: " + beanDefinition.getMetadata().getClassName(), e); + + } + } + return true; + } + return false; } }; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java b/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java index 835d041cb2..15479975e8 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java @@ -17,7 +17,14 @@ package org.springframework.integration.config; import java.beans.Introspector; +import java.util.ArrayList; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.config.BeanDefinition; @@ -31,9 +38,11 @@ import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; import org.springframework.expression.common.LiteralExpression; import org.springframework.integration.annotation.MessagingGateway; +import org.springframework.integration.config.annotation.MessagingAnnotationUtils; import org.springframework.integration.gateway.GatewayMethodMetadata; import org.springframework.integration.gateway.GatewayProxyFactoryBean; import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -42,16 +51,21 @@ import org.springframework.util.StringUtils; * and to register {@link BeanDefinition} {@link GatewayProxyFactoryBean}. * * @author Artem Bilan + * @author Gary Russell * @since 4.0 */ public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar { + private static final Log logger = LogFactory.getLog(MessagingGatewayRegistrar.class); + @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { if (importingClassMetadata != null && importingClassMetadata.isAnnotated(MessagingGateway.class.getName())) { - Assert.isTrue(importingClassMetadata.isInterface(), - "@MessagingGateway can only be specified on an interface"); - Map annotationAttributes = importingClassMetadata.getAnnotationAttributes(MessagingGateway.class.getName()); + Assert.isTrue(importingClassMetadata.isInterface(), "@MessagingGateway can only be specified on an interface"); + List> valuesHierarchy = captureMetaAnnotationValues(importingClassMetadata); + Map annotationAttributes = + importingClassMetadata.getAnnotationAttributes(MessagingGateway.class.getName()); + replaceEmptyOverrides(valuesHierarchy, annotationAttributes); annotationAttributes.put("serviceInterface", importingClassMetadata.getClassName()); BeanDefinitionReaderUtils.registerBeanDefinition(this.parse(annotationAttributes), registry); @@ -73,7 +87,8 @@ public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar boolean hasMapper = StringUtils.hasText(mapper); boolean hasDefaultPayloadExpression = StringUtils.hasText(defaultPayloadExpression); - Assert.state(!hasMapper || !hasDefaultPayloadExpression, "'defaultPayloadExpression' is not allowed when a 'mapper' is provided"); + Assert.state(!hasMapper || !hasDefaultPayloadExpression, + "'defaultPayloadExpression' is not allowed when a 'mapper' is provided"); boolean hasDefaultHeaders = !ObjectUtils.isEmpty(defaultHeaders); Assert.state(!hasMapper || !hasDefaultHeaders, "'defaultHeaders' are not allowed when a 'mapper' is provided"); @@ -92,11 +107,14 @@ public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar boolean hasValue = StringUtils.hasText(headerValue); if (!(hasValue ^ StringUtils.hasText(headerExpression))) { - throw new BeanDefinitionStoreException("exactly one of 'value' or 'expression' is required on a gateway's header."); + throw new BeanDefinitionStoreException("exactly one of 'value' or 'expression' " + + "is required on a gateway's header."); } - BeanDefinition expressionDef = new RootBeanDefinition(hasValue ? LiteralExpression.class : ExpressionFactoryBean.class); - expressionDef.getConstructorArgumentValues().addGenericArgumentValue(hasValue ? headerValue : headerExpression); + BeanDefinition expressionDef = + new RootBeanDefinition(hasValue ? LiteralExpression.class : ExpressionFactoryBean.class); + expressionDef.getConstructorArgumentValues() + .addGenericArgumentValue(hasValue ? headerValue : headerExpression); headerExpressions.put((String) header.get("name"), expressionDef); } @@ -140,4 +158,49 @@ public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar return new BeanDefinitionHolder(gatewayProxyBuilder.getBeanDefinition(), id); } + /** + * TODO until SPR-11710 will be resolved. + * Captures the meta-annotation attribute values, in order. + * @param importingClassMetadata The importing class metadata + * @return The captured values. + */ + private List> captureMetaAnnotationValues(AnnotationMetadata importingClassMetadata) { + Set directAnnotations = importingClassMetadata.getAnnotationTypes(); + List> valuesHierarchy = new ArrayList>(); + // Need to grab the values now; see SPR-11710 + for (String ann : directAnnotations) { + Set chain = importingClassMetadata.getMetaAnnotationTypes(ann); + if (chain.contains(MessagingGateway.class.getName())) { + for (String meta : chain) { + valuesHierarchy.add(importingClassMetadata.getAllAnnotationAttributes(meta)); + } + } + } + return valuesHierarchy; + } + + /** + * TODO until SPR-11709 will be resolved. + * For any empty values, traverses back up the meta-annotation hierarchy to + * see if a value has been overridden to empty, and replaces the first such value found. + * @param valuesHierarchy The values hierarchy in order. + * @param annotationAttributes The current attribute values. + */ + private void replaceEmptyOverrides(List> valuesHierarchy, + Map annotationAttributes) { + for (Entry entry : annotationAttributes.entrySet()) { + Object value = entry.getValue(); + if (!MessagingAnnotationUtils.hasValue(value)) { + // see if we overrode a value that was higher in the annotation chain + for (MultiValueMap metaAttributesMap : valuesHierarchy) { + Object newValue = metaAttributesMap.getFirst(entry.getKey()); + if (MessagingAnnotationUtils.hasValue(newValue)) { + annotationAttributes.put(entry.getKey(), newValue); + break; + } + } + } + } + } + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/ReleaseStrategyFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/config/ReleaseStrategyFactoryBean.java index 46d788c359..88b493f5df 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/ReleaseStrategyFactoryBean.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/ReleaseStrategyFactoryBean.java @@ -24,6 +24,7 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.integration.aggregator.MethodInvokingReleaseStrategy; import org.springframework.integration.aggregator.ReleaseStrategy; import org.springframework.integration.aggregator.SequenceSizeReleaseStrategy; +import org.springframework.integration.config.annotation.MessagingAnnotationUtils; import org.springframework.util.StringUtils; /** @@ -65,7 +66,8 @@ public class ReleaseStrategyFactoryBean implements FactoryBean this.delegate = new MethodInvokingReleaseStrategy(target, methodName); } else { - Method method = AnnotationFinder.findAnnotatedMethod(target, org.springframework.integration.annotation.ReleaseStrategy.class); + Method method = MessagingAnnotationUtils.findAnnotatedMethod(target, + org.springframework.integration.annotation.ReleaseStrategy.class); if (method != null) { this.delegate = new MethodInvokingReleaseStrategy(target, method); } 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 8279aefc53..9da21e50ab 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 @@ -28,6 +28,7 @@ import org.aopalliance.aop.Advice; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.GenericTypeResolver; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; @@ -77,19 +78,23 @@ public abstract class AbstractMethodAnnotationPostProcessor channelResolver; + protected final Class annotationType; + @SuppressWarnings("unchecked") public AbstractMethodAnnotationPostProcessor(ListableBeanFactory beanFactory, Environment environment) { Assert.notNull(beanFactory, "BeanFactory must not be null"); this.beanFactory = beanFactory; this.environment = environment; this.channelResolver = new BeanFactoryChannelResolver(beanFactory); + this.annotationType = (Class) GenericTypeResolver.resolveTypeArgument(this.getClass(), + MethodAnnotationPostProcessor.class); } @Override - public Object postProcess(Object bean, String beanName, Method method, T annotation) { - MessageHandler handler = this.createHandler(bean, method, annotation); - this.setAdviceChainIfPresent(beanName, annotation, handler); + public Object postProcess(Object bean, String beanName, Method method, List annotations) { + MessageHandler handler = this.createHandler(bean, method, annotations); + this.setAdviceChainIfPresent(beanName, annotations, handler); if (handler instanceof Orderable) { Order orderAnnotation = AnnotationUtils.findAnnotation(method, Order.class); if (orderAnnotation != null) { @@ -97,12 +102,12 @@ public abstract class AbstractMethodAnnotationPostProcessor annotations, MessageHandler handler) { + String[] adviceChainNames = MessagingAnnotationUtils.resolveAttribute(annotations, ADVICE_CHAIN_ATTRIBUTE, + String[].class); + /* + * Note: we don't merge advice chain contents; if the directAnnotation has a non-empty + * attribute, it wins. You cannot "remove" an advice chain from a meta-annotation + * by setting an empty array on the custom annotation. + */ if (adviceChainNames != null && adviceChainNames.length > 0) { if (!(handler instanceof AbstractReplyProducingMessageHandler)) { throw new IllegalArgumentException("Cannot apply advice chain to " + handler.getClass().getName()); @@ -141,9 +152,10 @@ public abstract class AbstractMethodAnnotationPostProcessor annotations) { AbstractEndpoint endpoint = null; - String inputChannelName = (String) AnnotationUtils.getValue(annotation, INPUT_CHANNEL_ATTRIBUTE); + String inputChannelName = MessagingAnnotationUtils.resolveAttribute(annotations, INPUT_CHANNEL_ATTRIBUTE, + String.class); if (StringUtils.hasText(inputChannelName)) { MessageChannel inputChannel; try { @@ -161,22 +173,22 @@ public abstract class AbstractMethodAnnotationPostProcessor annotations) { PollerMetadata pollerMetadata = null; - Poller[] pollers = (Poller[]) AnnotationUtils.getValue(annotation, "poller"); + Poller[] pollers = MessagingAnnotationUtils.resolveAttribute(annotations, "poller", Poller[].class); if (!ObjectUtils.isEmpty(pollers)) { Assert.state(pollers.length == 1, "The 'poller' for an Annotation-based endpoint can have only one '@Poller'."); Poller poller = pollers[0]; @@ -190,9 +202,9 @@ public abstract class AbstractMethodAnnotationPostProcessor annotationType) { - String baseName = originalBeanName + "." + method.getName() + "." + ClassUtils.getShortNameAsProperty(annotationType); + protected String generateHandlerBeanName(String originalBeanName, Method method) { + String baseName = originalBeanName + "." + method.getName() + "." + + ClassUtils.getShortNameAsProperty(this.annotationType); String name = baseName; int count = 1; while (this.beanFactory.containsBean(name)) { @@ -256,14 +270,20 @@ public abstract class AbstractMethodAnnotationPostProcessor annotations, AbstractReplyProducingMessageHandler handler) { + String outputChannelName = MessagingAnnotationUtils.resolveAttribute(annotations, "outputChannel", String.class); + if (StringUtils.hasText(outputChannelName)) { + handler.setOutputChannel(this.channelResolver.resolveDestination(outputChannelName)); + } + } + /** * Subclasses must implement this method to create the MessageHandler. * * @param bean The bean. * @param method The method. - * @param annotation The annotation. * @return The MessageHandler. */ - protected abstract MessageHandler createHandler(Object bean, Method method, T annotation); + protected abstract MessageHandler createHandler(Object bean, Method method, List annotations); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AggregatorAnnotationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AggregatorAnnotationPostProcessor.java index 5bea144f39..604e70e88c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AggregatorAnnotationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AggregatorAnnotationPostProcessor.java @@ -18,10 +18,9 @@ package org.springframework.integration.config.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.concurrent.atomic.AtomicReference; +import java.util.List; import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.Environment; import org.springframework.integration.aggregator.AggregatingMessageHandler; import org.springframework.integration.aggregator.MethodInvokingCorrelationStrategy; @@ -34,7 +33,6 @@ import org.springframework.integration.store.SimpleMessageStore; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -42,6 +40,8 @@ import org.springframework.util.StringUtils; * * @author Mark Fisher * @author Oleg Zhurakousky + * @author Gary Russell + * @author Artem Bilan */ public class AggregatorAnnotationPostProcessor extends AbstractMethodAnnotationPostProcessor { @@ -51,55 +51,47 @@ public class AggregatorAnnotationPostProcessor extends AbstractMethodAnnotationP @Override - protected MessageHandler createHandler(Object bean, Method method, Aggregator annotation) { + protected MessageHandler createHandler(Object bean, Method method, List annotations) { MethodInvokingMessageGroupProcessor processor = new MethodInvokingMessageGroupProcessor(bean, method); processor.setBeanFactory(this.beanFactory); - MethodInvokingReleaseStrategy releaseStrategy = getReleaseStrategy(bean); - MethodInvokingCorrelationStrategy correlationStrategy = getCorrelationStrategy(bean); - AggregatingMessageHandler handler = new AggregatingMessageHandler(processor, new SimpleMessageStore(), correlationStrategy, releaseStrategy); - String discardChannelName = annotation.discardChannel(); + + MethodInvokingReleaseStrategy releaseStrategy = null; + Method releaseStrategyMethod = MessagingAnnotationUtils.findAnnotatedMethod(bean, ReleaseStrategy.class); + if (releaseStrategyMethod != null) { + releaseStrategy = new MethodInvokingReleaseStrategy(bean, releaseStrategyMethod); + } + + MethodInvokingCorrelationStrategy correlationStrategy = null; + Method correlationStrategyMethod = MessagingAnnotationUtils.findAnnotatedMethod(bean, CorrelationStrategy.class); + if (correlationStrategyMethod != null) { + correlationStrategy = new MethodInvokingCorrelationStrategy(bean, correlationStrategyMethod); + } + + AggregatingMessageHandler handler = new AggregatingMessageHandler(processor, new SimpleMessageStore(), + correlationStrategy, releaseStrategy); + + String discardChannelName = MessagingAnnotationUtils.resolveAttribute(annotations, "discardChannel", String.class); if (StringUtils.hasText(discardChannelName)) { MessageChannel discardChannel = this.channelResolver.resolveDestination(discardChannelName); Assert.notNull(discardChannel, "failed to resolve discardChannel '" + discardChannelName + "'"); handler.setDiscardChannel(discardChannel); } - String outputChannelName = annotation.outputChannel(); + String outputChannelName = MessagingAnnotationUtils.resolveAttribute(annotations, "outputChannel", String.class); if (StringUtils.hasText(outputChannelName)) { handler.setOutputChannel(this.channelResolver.resolveDestination(outputChannelName)); } - handler.setSendTimeout(annotation.sendTimeout()); - handler.setSendPartialResultOnExpiry(annotation.sendPartialResultsOnExpiry()); + Long sendTimeout = MessagingAnnotationUtils.resolveAttribute(annotations, "sendTimeout", Long.class); + if (sendTimeout != null) { + handler.setSendTimeout(sendTimeout); + } + Boolean sendPartialResultsOnExpiry = MessagingAnnotationUtils.resolveAttribute(annotations, + "sendPartialResultsOnExpiry", Boolean.class); + if (sendPartialResultsOnExpiry != null) { + handler.setSendPartialResultOnExpiry(sendPartialResultsOnExpiry); + } handler.setBeanFactory(this.beanFactory); handler.afterPropertiesSet(); return handler; } - private MethodInvokingReleaseStrategy getReleaseStrategy(final Object bean) { - final AtomicReference reference = new AtomicReference(); - ReflectionUtils.doWithMethods(bean.getClass(), new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - Annotation annotation = AnnotationUtils.getAnnotation(method, ReleaseStrategy.class); - if (annotation != null) { - reference.set(new MethodInvokingReleaseStrategy(bean, method)); - } - } - }); - return reference.get(); - } - - private MethodInvokingCorrelationStrategy getCorrelationStrategy(final Object bean) { - final AtomicReference reference = new AtomicReference(); - ReflectionUtils.doWithMethods(bean.getClass(), new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - Annotation annotation = AnnotationUtils.getAnnotation(method, CorrelationStrategy.class); - if (annotation != null) { - reference.set(new MethodInvokingCorrelationStrategy(bean, method)); - } - } - }); - return reference.get(); - } - } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/FilterAnnotationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/FilterAnnotationPostProcessor.java index 0a3685850f..72bdd14c94 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/FilterAnnotationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/FilterAnnotationPostProcessor.java @@ -16,16 +16,17 @@ package org.springframework.integration.config.annotation; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.List; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.core.env.Environment; import org.springframework.integration.annotation.Filter; -import org.springframework.messaging.MessageHandler; import org.springframework.integration.filter.MessageFilter; import org.springframework.integration.filter.MethodInvokingSelector; +import org.springframework.messaging.MessageHandler; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * Post-processor for Methods annotated with {@link Filter @Filter}. @@ -42,16 +43,17 @@ public class FilterAnnotationPostProcessor extends AbstractMethodAnnotationPostP @Override - protected MessageHandler createHandler(Object bean, Method method, Filter annotation) { + protected MessageHandler createHandler(Object bean, Method method, List annotations) { Assert.isTrue(boolean.class.equals(method.getReturnType()) || Boolean.class.equals(method.getReturnType()), "The Filter annotation may only be applied to methods with a boolean return type."); MethodInvokingSelector selector = new MethodInvokingSelector(bean, method); MessageFilter filter = new MessageFilter(selector); - String outputChannelName = annotation.outputChannel(); - if (StringUtils.hasText(outputChannelName)) { - filter.setOutputChannel(this.channelResolver.resolveDestination(outputChannelName)); + this.setOutputChannelIfPresent(annotations, filter); + Boolean discardWithinAdvice = MessagingAnnotationUtils.resolveAttribute(annotations, "discardWithinAdvice", + Boolean.class); + if (discardWithinAdvice != null) { + filter.setDiscardWithinAdvice(discardWithinAdvice); } - filter.setDiscardWithinAdvice(annotation.discardWithinAdvice()); return filter; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/InboundChannelAdapterAnnotationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/InboundChannelAdapterAnnotationPostProcessor.java index 96837ae8b5..007591116e 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/InboundChannelAdapterAnnotationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/InboundChannelAdapterAnnotationPostProcessor.java @@ -18,6 +18,7 @@ package org.springframework.integration.config.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.List; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -35,6 +36,7 @@ import org.springframework.util.Assert; * Post-processor for Methods annotated with {@link InboundChannelAdapter @InboundChannelAdapter}. * * @author Artem Bilan + * @author Gary Russell * @since 4.0 */ public class InboundChannelAdapterAnnotationPostProcessor extends @@ -45,13 +47,13 @@ public class InboundChannelAdapterAnnotationPostProcessor extends } @Override - public Object postProcess(Object bean, String beanName, Method method, InboundChannelAdapter annotation) { + public Object postProcess(Object bean, String beanName, Method method, List annotations) { Assert.isTrue(!Void.class.isAssignableFrom(method.getReturnType()), "The method '" + method + "' for 'SourcePollingChannelAdapter' must not have 'void' return type."); Assert.isTrue(method.getParameterTypes().length == 0, "The method '" + method + "' for 'SourcePollingChannelAdapter' must not have any parameters."); - String channelName = (String) AnnotationUtils.getValue(annotation); + String channelName = MessagingAnnotationUtils.resolveAttribute(annotations, AnnotationUtils.VALUE, String.class); Assert.hasText(channelName, "The channel ('value' attribute of @InboundChannelAdapter) can't be empty."); MessageChannel channel = this.channelResolver.resolveDestination(channelName); @@ -60,7 +62,7 @@ public class InboundChannelAdapterAnnotationPostProcessor extends messageSource.setObject(bean); messageSource.setMethod(method); if (beanFactory instanceof ConfigurableListableBeanFactory) { - String handlerBeanName = this.generateHandlerBeanName(beanName, method, annotation.annotationType()); + String handlerBeanName = this.generateHandlerBeanName(beanName, method); ConfigurableListableBeanFactory listableBeanFactory = (ConfigurableListableBeanFactory) beanFactory; listableBeanFactory.registerSingleton(handlerBeanName, messageSource); messageSource = (MethodInvokingMessageSource) listableBeanFactory @@ -70,20 +72,19 @@ public class InboundChannelAdapterAnnotationPostProcessor extends SourcePollingChannelAdapter adapter = new SourcePollingChannelAdapter(); adapter.setOutputChannel(channel); adapter.setSource(messageSource); - this.configurePollingEndpoint(adapter, annotation); + this.configurePollingEndpoint(adapter, annotations); return adapter; } @Override - protected String generateHandlerBeanName(String originalBeanName, Method method, - Class annotationType) { - return super.generateHandlerBeanName(originalBeanName, method, annotationType) + protected String generateHandlerBeanName(String originalBeanName, Method method) { + return super.generateHandlerBeanName(originalBeanName, method) .replaceFirst(IntegrationConfigUtils.HANDLER_ALIAS_SUFFIX + "$", ".source"); } @Override - protected MessageHandler createHandler(Object bean, Method method, InboundChannelAdapter annotation) { + protected MessageHandler createHandler(Object bean, Method method, List annotations) { throw new UnsupportedOperationException(); } 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 3b3d33eabe..786a31257a 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 @@ -18,10 +18,10 @@ package org.springframework.integration.config.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -41,6 +41,7 @@ import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.EnvironmentAware; import org.springframework.context.Lifecycle; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.Environment; import org.springframework.integration.annotation.Aggregator; @@ -64,6 +65,7 @@ import org.springframework.util.StringUtils; * @author Mark Fisher * @author Marius Bogoevici * @author Artem Bilan + * @author Gary Russell */ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, BeanFactoryAware, InitializingBean, Lifecycle, ApplicationListener, EnvironmentAware { @@ -117,7 +119,7 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException { Assert.notNull(this.beanFactory, "BeanFactory must not be null"); final Class beanClass = this.getBeanClass(bean); - if (!this.isStereotype(beanClass)) { + if (AnnotationUtils.findAnnotation(beanClass, Component.class) == null) { // we only post-process stereotype components return bean; } @@ -125,20 +127,27 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - List annotations = new ArrayList(); - for (Class annotation : postProcessors.keySet()) { - Annotation result = AnnotationUtils.getAnnotation(method, annotation); - if (result != null) { - annotations.add(result); + Map, List> annotationChains = + new HashMap, List>(); + for (Class annotationType : postProcessors.keySet()) { + if (AnnotatedElementUtils.isAnnotated(method, annotationType.getName())) { + List annotationChain = getAnnotationChain(method, annotationType); + if (annotationChain.size() > 0) { + annotationChains.put(annotationType, annotationChain); + } } } - for (Annotation annotation : annotations) { - MethodAnnotationPostProcessor postProcessor = postProcessors.get(annotation.annotationType()); - if (postProcessor != null && shouldCreateEndpoint(annotation)) { - Object result = postProcessor.postProcess(bean, beanName, method, annotation); + + for (Map.Entry, List> entry : annotationChains.entrySet()) { + Class annotationType = entry.getKey(); + List annotations = entry.getValue(); + MethodAnnotationPostProcessor postProcessor = postProcessors.get(annotationType); + if (postProcessor != null && shouldCreateEndpoint(annotations)) { + Object result = postProcessor.postProcess(bean, beanName, method, annotations); if (result != null && result instanceof AbstractEndpoint) { AbstractEndpoint endpoint = (AbstractEndpoint) result; - String autoStartup = (String) AnnotationUtils.getValue(annotation, "autoStartup"); + String autoStartup = MessagingAnnotationUtils.resolveAttribute(annotations, "autoStartup", + String.class); if (StringUtils.hasText(autoStartup)) { if (environment != null) { autoStartup = environment.resolvePlaceholders(autoStartup); @@ -148,7 +157,7 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean } } - String phase = (String) AnnotationUtils.getValue(annotation, "phase"); + String phase = MessagingAnnotationUtils.resolveAttribute(annotations, "phase", String.class); if (StringUtils.hasText(phase)) { if (environment != null) { phase = environment.resolvePlaceholders(phase); @@ -158,7 +167,7 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean } } - String endpointBeanName = generateBeanName(beanName, method, annotation.annotationType()); + String endpointBeanName = generateBeanName(beanName, method, annotationType); endpoint.setBeanName(endpointBeanName); beanFactory.registerSingleton(endpointBeanName, endpoint); endpoint.setBeanFactory(beanFactory); @@ -179,17 +188,63 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean } } } - }); + }, ReflectionUtils.USER_DECLARED_METHODS); + return bean; } - private boolean shouldCreateEndpoint(Annotation annotation) { - Object inputChannel = AnnotationUtils.getValue(annotation, "inputChannel"); - if (inputChannel == null && annotation instanceof InboundChannelAdapter) { - inputChannel = AnnotationUtils.getValue(annotation); + /** + * @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) { + Annotation[] annotations = AnnotationUtils.getAnnotations(method); + List annotationChain = new LinkedList(); + Set visited = new HashSet(); + for (Annotation ann : annotations) { + this.recursiveFindAnnotation(annotationType, ann, annotationChain, visited); + if (annotationChain.size() > 0) { + Collections.reverse(annotationChain); + return annotationChain; + } } - return (inputChannel != null && inputChannel instanceof String - && StringUtils.hasText((String) inputChannel)); + return annotationChain; + } + + private boolean recursiveFindAnnotation(Class annotationType, Annotation ann, + List annotationChain, Set visited) { + if (ann.annotationType().equals(annotationType)) { + annotationChain.add(ann); + return true; + } + for (Annotation metaAnn : ann.annotationType().getAnnotations()) { + if (!ann.equals(metaAnn) && !visited.contains(metaAnn) + && !(metaAnn.annotationType().getPackage().getName().startsWith("java.lang"))) { + visited.add(metaAnn); // prevent infinite recursion if the same annotation is found again + if (this.recursiveFindAnnotation(annotationType, metaAnn, annotationChain, visited)) { + annotationChain.add(ann); + return true; + } + } + } + return false; + } + + private boolean shouldCreateEndpoint(List annotations) { + for (Annotation annotation : annotations) { + Object inputChannel = AnnotationUtils.getValue(annotation, "inputChannel"); + if (inputChannel == null && + (annotation instanceof InboundChannelAdapter || + AnnotationUtils.findAnnotation(annotation.annotationType(), InboundChannelAdapter.class) != null)) { + inputChannel = AnnotationUtils.getValue(annotation); + } + if (inputChannel != null && inputChannel instanceof String + && StringUtils.hasText((String) inputChannel)) { + return true; + } + } + return false; } private Class getBeanClass(Object bean) { @@ -197,21 +252,6 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean return (targetClass != null) ? targetClass : bean.getClass(); } - private boolean isStereotype(Class beanClass) { - List annotations = new ArrayList(Arrays.asList(beanClass.getAnnotations())); - Class[] interfaces = beanClass.getInterfaces(); - for (Class iface : interfaces) { - annotations.addAll(Arrays.asList(iface.getAnnotations())); - } - for (Annotation annotation : annotations) { - Class annotationType = annotation.annotationType(); - if (annotationType.equals(Component.class) || annotationType.isAnnotationPresent(Component.class)) { - return true; - } - } - return false; - } - private String generateBeanName(String originalBeanName, Method method, Class annotationType) { String baseName = originalBeanName + "." + method.getName() + "." + ClassUtils.getShortNameAsProperty(annotationType); String name = baseName; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/MessagingAnnotationUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/MessagingAnnotationUtils.java new file mode 100644 index 0000000000..1b108b697e --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/MessagingAnnotationUtils.java @@ -0,0 +1,108 @@ +/* + * Copyright 2014 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 java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Utility methods to support annotation processing. + * + * @author Gary Russell + * @author Dave Syer + * @author Gunnar Hillert + * @author Soby Chacko + * @author Artem Bilan + * @since 4.0 + */ +public final class MessagingAnnotationUtils { + + /** + * Get the attribute value from the annotation hierarchy, returning the first non-empty + * value closest to the annotated method. While traversing up the hierarchy, for string-valued + * attributes, an empty string is ignored. For array-valued attributes, an empty + * array is ignored. + * The overridden attribute must be the same type. + * @param annotations The meta-annotations in order (closest first). + * @param name The attribute name. + * @param requiredType The expected type. + * @return The value. + */ + @SuppressWarnings("unchecked") + public static T resolveAttribute(List annotations, String name, Class requiredType) { + for (Annotation annotation : annotations) { + if (annotation != null) { + Object value = AnnotationUtils.getValue(annotation, name); + if (value != null && value.getClass() == requiredType && hasValue(value)) { + return (T) value; + } + } + } + return null; + } + + public static boolean hasValue(Object value) { + return value != null && (!(value instanceof String) || (StringUtils.hasText((String) value))) + && (!value.getClass().isArray() || ((Object[]) value).length > 0); + } + + public static Method findAnnotatedMethod(Object target, final Class annotationType) { + final AtomicReference reference = new AtomicReference(); + + ReflectionUtils.doWithMethods(getTargetClass(target), new ReflectionUtils.MethodCallback() { + + @Override + public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + reference.compareAndSet(null, method); + } + }, new ReflectionUtils.MethodFilter() { + + @Override + public boolean matches(Method method) { + return ReflectionUtils.USER_DECLARED_METHODS.matches(method) && + AnnotatedElementUtils.isAnnotated(method, annotationType.getName()); + } + }); + + return reference.get(); + } + + private static Class getTargetClass(Object targetObject) { + Class targetClass = targetObject.getClass(); + if (AopUtils.isAopProxy(targetObject)) { + targetClass = AopUtils.getTargetClass(targetObject); + } + else if (ClassUtils.isCglibProxyClass(targetClass)) { + Class superClass = targetObject.getClass().getSuperclass(); + if (!Object.class.equals(superClass)) { + targetClass = superClass; + } + } + return targetClass; + } + + private MessagingAnnotationUtils() {} + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/MethodAnnotationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/MethodAnnotationPostProcessor.java index 932b0509af..82aed8d58c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/MethodAnnotationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/MethodAnnotationPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2014 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. @@ -18,14 +18,16 @@ package org.springframework.integration.config.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.List; /** * Strategy interface for post-processing annotated methods. - * + * * @author Mark Fisher + * @author Gary Russell */ public interface MethodAnnotationPostProcessor { - Object postProcess(Object bean, String beanName, Method method, T annotation); + Object postProcess(Object bean, String beanName, Method method, List annotations); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/RouterAnnotationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/RouterAnnotationPostProcessor.java index efc880c283..ed701fa18b 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/RouterAnnotationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/RouterAnnotationPostProcessor.java @@ -16,14 +16,16 @@ package org.springframework.integration.config.annotation; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.List; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.core.env.Environment; -import org.springframework.messaging.MessageChannel; import org.springframework.integration.annotation.Router; -import org.springframework.messaging.MessageHandler; import org.springframework.integration.router.MethodInvokingRouter; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -31,6 +33,7 @@ import org.springframework.util.StringUtils; * Post-processor for Methods annotated with {@link Router @Router}. * * @author Mark Fisher + * @author Gary Russell */ public class RouterAnnotationPostProcessor extends AbstractMethodAnnotationPostProcessor { @@ -40,10 +43,12 @@ public class RouterAnnotationPostProcessor extends AbstractMethodAnnotationPostP @Override - protected MessageHandler createHandler(Object bean, Method method, Router annotation) { + protected MessageHandler createHandler(Object bean, Method method, + List annotations) { MethodInvokingRouter router = new MethodInvokingRouter(bean, method); router.setBeanFactory(this.beanFactory); - String defaultOutputChannelName = annotation.defaultOutputChannel(); + String defaultOutputChannelName = MessagingAnnotationUtils.resolveAttribute(annotations, "defaultOutputChannel", + String.class); if (StringUtils.hasText(defaultOutputChannelName)) { MessageChannel defaultOutputChannel = this.channelResolver.resolveDestination(defaultOutputChannelName); Assert.notNull(defaultOutputChannel, "unable to resolve defaultOutputChannel '" + defaultOutputChannelName + "'"); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/ServiceActivatorAnnotationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/ServiceActivatorAnnotationPostProcessor.java index 00ece70c8c..3752919892 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/ServiceActivatorAnnotationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/ServiceActivatorAnnotationPostProcessor.java @@ -16,19 +16,21 @@ package org.springframework.integration.config.annotation; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.List; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.core.env.Environment; import org.springframework.integration.annotation.ServiceActivator; -import org.springframework.messaging.MessageHandler; import org.springframework.integration.handler.ServiceActivatingHandler; -import org.springframework.util.StringUtils; +import org.springframework.messaging.MessageHandler; /** * Post-processor for Methods annotated with {@link ServiceActivator @ServiceActivator}. * * @author Mark Fisher + * @author Gary Russell */ public class ServiceActivatorAnnotationPostProcessor extends AbstractMethodAnnotationPostProcessor { @@ -38,12 +40,9 @@ public class ServiceActivatorAnnotationPostProcessor extends AbstractMethodAnnot @Override - protected MessageHandler createHandler(Object bean, Method method, ServiceActivator annotation) { + protected MessageHandler createHandler(Object bean, Method method, List annotations) { ServiceActivatingHandler serviceActivator = new ServiceActivatingHandler(bean, method); - String outputChannelName = annotation.outputChannel(); - if (StringUtils.hasText(outputChannelName)) { - serviceActivator.setOutputChannel(this.channelResolver.resolveDestination(outputChannelName)); - } + this.setOutputChannelIfPresent(annotations, serviceActivator); return serviceActivator; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/SplitterAnnotationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/SplitterAnnotationPostProcessor.java index de72616593..21ca6d2da2 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/SplitterAnnotationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/SplitterAnnotationPostProcessor.java @@ -16,19 +16,21 @@ package org.springframework.integration.config.annotation; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.List; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.core.env.Environment; import org.springframework.integration.annotation.Splitter; -import org.springframework.messaging.MessageHandler; import org.springframework.integration.splitter.MethodInvokingSplitter; -import org.springframework.util.StringUtils; +import org.springframework.messaging.MessageHandler; /** * Post-processor for Methods annotated with {@link Splitter @Splitter}. * * @author Mark Fisher + * @author Gary Russell */ public class SplitterAnnotationPostProcessor extends AbstractMethodAnnotationPostProcessor { @@ -38,12 +40,9 @@ public class SplitterAnnotationPostProcessor extends AbstractMethodAnnotationPos @Override - protected MessageHandler createHandler(Object bean, Method method, Splitter annotation) { + protected MessageHandler createHandler(Object bean, Method method, List annotations) { MethodInvokingSplitter splitter = new MethodInvokingSplitter(bean, method); - String outputChannelName = annotation.outputChannel(); - if (StringUtils.hasText(outputChannelName)) { - splitter.setOutputChannel(this.channelResolver.resolveDestination(outputChannelName)); - } + this.setOutputChannelIfPresent(annotations, splitter); return splitter; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/TransformerAnnotationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/TransformerAnnotationPostProcessor.java index 7de1650f0b..da0db7e9fd 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/TransformerAnnotationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/TransformerAnnotationPostProcessor.java @@ -16,20 +16,22 @@ package org.springframework.integration.config.annotation; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.List; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.core.env.Environment; import org.springframework.integration.annotation.Transformer; -import org.springframework.messaging.MessageHandler; import org.springframework.integration.transformer.MessageTransformingHandler; import org.springframework.integration.transformer.MethodInvokingTransformer; -import org.springframework.util.StringUtils; +import org.springframework.messaging.MessageHandler; /** * Post-processor for Methods annotated with {@link Transformer @Transformer}. * * @author Mark Fisher + * @author Gary Russell */ public class TransformerAnnotationPostProcessor extends AbstractMethodAnnotationPostProcessor { @@ -39,13 +41,10 @@ public class TransformerAnnotationPostProcessor extends AbstractMethodAnnotation @Override - protected MessageHandler createHandler(Object bean, Method method, Transformer annotation) { + protected MessageHandler createHandler(Object bean, Method method, List annotations) { MethodInvokingTransformer transformer = new MethodInvokingTransformer(bean, method); MessageTransformingHandler handler = new MessageTransformingHandler(transformer); - String outputChannelName = annotation.outputChannel(); - if (StringUtils.hasText(outputChannelName)) { - handler.setOutputChannel(this.channelResolver.resolveDestination(outputChannelName)); - } + this.setOutputChannelIfPresent(annotations, handler); return handler; } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java index 94759fdafa..cb3e2c2a00 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java @@ -16,24 +16,20 @@ package org.springframework.integration.configuration; -import static org.hamcrest.Matchers.containsString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Date; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import org.aopalliance.intercept.MethodInterceptor; import org.hamcrest.Matchers; import org.junit.Test; import org.junit.runner.RunWith; @@ -53,6 +49,8 @@ import org.springframework.context.annotation.PropertySource; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.convert.converter.Converter; import org.springframework.core.serializer.support.SerializingConverter; +import org.springframework.integration.aggregator.AbstractCorrelatingMessageHandler; +import org.springframework.integration.annotation.Aggregator; import org.springframework.integration.annotation.Gateway; import org.springframework.integration.annotation.GatewayHeader; import org.springframework.integration.annotation.InboundChannelAdapter; @@ -76,6 +74,7 @@ import org.springframework.integration.config.GlobalChannelInterceptor; import org.springframework.integration.config.IntegrationConverter; import org.springframework.integration.endpoint.MethodInvokingMessageSource; import org.springframework.integration.endpoint.PollingConsumer; +import org.springframework.integration.gateway.GatewayProxyFactoryBean; import org.springframework.integration.history.MessageHistory; import org.springframework.integration.history.MessageHistoryConfigurer; import org.springframework.integration.scheduling.PollerMetadata; @@ -155,6 +154,9 @@ public class EnableIntegrationTests { @Autowired private TestGateway testGateway; + @Autowired + private TestGateway2 testGateway2; + @Autowired private TestChannelInterceptor testChannelInterceptor; @@ -297,6 +299,7 @@ public class EnableIntegrationTests { public void testMessagingGateway() { String payload = "bar"; assertEquals(payload.toUpperCase(), this.testGateway.echo(payload)); + assertEquals(payload.toUpperCase() + "2", this.testGateway2.echo2(payload)); } @Test @@ -336,6 +339,83 @@ public class EnableIntegrationTests { } + @Test + public void testMetaAnnotations() { + + assertEquals(2, this.context.getBeanNamesForType(GatewayProxyFactoryBean.class).length); + + PollingConsumer consumer = this.context.getBean( + "enableIntegrationTests.AnnotationTestService.annCount.serviceActivator", + PollingConsumer.class); + assertFalse(TestUtils.getPropertyValue(consumer, "autoStartup", Boolean.class)); + assertEquals(23, TestUtils.getPropertyValue(consumer, "phase")); + assertSame(context.getBean("annInput"), TestUtils.getPropertyValue(consumer, "inputChannel")); + assertSame(context.getBean("annOutput"), TestUtils.getPropertyValue(consumer, "handler.outputChannel")); + assertSame(context.getBean("annAdvice"), TestUtils.getPropertyValue(consumer, + "handler.adviceChain", List.class).get(0)); + assertEquals(1000L, TestUtils.getPropertyValue(consumer, "trigger.period")); + + consumer = this.context.getBean( + "enableIntegrationTests.AnnotationTestService.annCount1.serviceActivator", + PollingConsumer.class); + consumer.stop(); + assertTrue(TestUtils.getPropertyValue(consumer, "autoStartup", Boolean.class)); + assertEquals(23, TestUtils.getPropertyValue(consumer, "phase")); + assertSame(context.getBean("annInput1"), TestUtils.getPropertyValue(consumer, "inputChannel")); + assertSame(context.getBean("annOutput"), TestUtils.getPropertyValue(consumer, "handler.outputChannel")); + assertSame(context.getBean("annAdvice1"), TestUtils.getPropertyValue(consumer, + "handler.adviceChain", List.class).get(0)); + assertEquals(2000L, TestUtils.getPropertyValue(consumer, "trigger.period")); + + consumer = this.context.getBean( + "enableIntegrationTests.AnnotationTestService.annCount2.serviceActivator", + PollingConsumer.class); + assertFalse(TestUtils.getPropertyValue(consumer, "autoStartup", Boolean.class)); + assertEquals(23, TestUtils.getPropertyValue(consumer, "phase")); + assertSame(context.getBean("annInput"), TestUtils.getPropertyValue(consumer, "inputChannel")); + assertSame(context.getBean("annOutput"), TestUtils.getPropertyValue(consumer, "handler.outputChannel")); + assertSame(context.getBean("annAdvice"), TestUtils.getPropertyValue(consumer, + "handler.adviceChain", List.class).get(0)); + assertEquals(1000L, TestUtils.getPropertyValue(consumer, "trigger.period")); + + // Tests when the channel is in a "middle" annotation + consumer = this.context.getBean( + "enableIntegrationTests.AnnotationTestService.annCount5.serviceActivator", + PollingConsumer.class); + assertFalse(TestUtils.getPropertyValue(consumer, "autoStartup", Boolean.class)); + assertEquals(23, TestUtils.getPropertyValue(consumer, "phase")); + assertSame(context.getBean("annInput3"), TestUtils.getPropertyValue(consumer, "inputChannel")); + assertSame(context.getBean("annOutput"), TestUtils.getPropertyValue(consumer, "handler.outputChannel")); + assertSame(context.getBean("annAdvice"), TestUtils.getPropertyValue(consumer, + "handler.adviceChain", List.class).get(0)); + assertEquals(1000L, TestUtils.getPropertyValue(consumer, "trigger.period")); + + consumer = this.context.getBean( + "enableIntegrationTests.AnnotationTestService.annAgg1.aggregator", + PollingConsumer.class); + assertFalse(TestUtils.getPropertyValue(consumer, "autoStartup", Boolean.class)); + assertEquals(23, TestUtils.getPropertyValue(consumer, "phase")); + assertSame(context.getBean("annInput"), TestUtils.getPropertyValue(consumer, "inputChannel")); + assertSame(context.getBean("annOutput"), TestUtils.getPropertyValue(consumer, "handler.outputChannel")); + assertSame(context.getBean("annOutput"), TestUtils.getPropertyValue(consumer, "handler.discardChannel")); + assertEquals(1000L, TestUtils.getPropertyValue(consumer, "trigger.period")); + assertEquals(1000L, TestUtils.getPropertyValue(consumer, "handler.messagingTemplate.sendTimeout")); + assertFalse(TestUtils.getPropertyValue(consumer, "handler.sendPartialResultOnExpiry", Boolean.class)); + + consumer = this.context.getBean( + "enableIntegrationTests.AnnotationTestService.annAgg2.aggregator", + PollingConsumer.class); + assertFalse(TestUtils.getPropertyValue(consumer, "autoStartup", Boolean.class)); + assertEquals(23, TestUtils.getPropertyValue(consumer, "phase")); + assertSame(context.getBean("annInput"), TestUtils.getPropertyValue(consumer, "inputChannel")); + assertSame(context.getBean("annOutput"), TestUtils.getPropertyValue(consumer, "handler.outputChannel")); + assertSame(context.getBean("annOutput"), TestUtils.getPropertyValue(consumer, "handler.discardChannel")); + assertEquals(1000L, TestUtils.getPropertyValue(consumer, "trigger.period")); + assertEquals(75L, TestUtils.getPropertyValue(consumer, "handler.messagingTemplate.sendTimeout")); + assertTrue(TestUtils.getPropertyValue(consumer, "handler.sendPartialResultOnExpiry", Boolean.class)); + } + + @Configuration @ComponentScan @IntegrationComponentScan @@ -433,6 +513,38 @@ public class EnableIntegrationTests { }; } + // beans for metaAnnotation tests + + @Bean + public MethodInterceptor annAdvice() { + return mock(MethodInterceptor.class); + } + + @Bean + public QueueChannel annInput() { + return new QueueChannel(); + } + + @Bean + public QueueChannel annOutput() { + return new QueueChannel(); + } + + @Bean + public MethodInterceptor annAdvice1() { + return mock(MethodInterceptor.class); + } + + @Bean + public QueueChannel annInput1() { + return new QueueChannel(); + } + + @Bean + public QueueChannel annInput3() { + return new QueueChannel(); + } + } @Component @@ -492,6 +604,11 @@ public class EnableIntegrationTests { return new QueueChannel(); } + @Bean + public PollableChannel gatewayChannel2() { + return new QueueChannel(); + } + @Bean public PollableChannel counterChannel() { return new QueueChannel(); @@ -624,8 +741,16 @@ public class EnableIntegrationTests { return this.handle(message.getPayload()); } - @InboundChannelAdapter(value = "counterChannel", autoStartup = "false", - phase = "23") + @Transformer(inputChannel = "gatewayChannel2") + public String transform2(Message message) { + assertTrue(message.getHeaders().containsKey("foo")); + assertEquals("FOO", message.getHeaders().get("foo")); + assertTrue(message.getHeaders().containsKey("calledMethod")); + assertEquals("echo2", message.getHeaders().get("calledMethod")); + return this.handle(message.getPayload()) + "2"; + } + + @MyInboundChannelAdapter1 public Integer count() { return this.counter.incrementAndGet(); } @@ -656,6 +781,44 @@ public class EnableIntegrationTests { public void error2() { }*/ + // metaAnnotation tests + + @MyServiceActivator + public Integer annCount() { + return 0; + } + + @MyServiceActivator1(inputChannel = "annInput1", autoStartup = "true", + adviceChain = { "annAdvice1" }, poller = @Poller(fixedRate = "2000") ) + public Integer annCount1() { + return 0; + } + + @MyServiceActivatorNoLocalAtts() + public Integer annCount2() { + return 0; + } + + @MyServiceActivator5 + public Integer annCount5() { + return 0; + } + + @MyServiceActivator8 + public Integer annCount8() { + return 0; + } + + @MyAggregator + public Integer annAgg1(List messages) { + return 42; + } + + @MyAggregatorDefaultOverrideDefaults + public Integer annAgg2(List messages) { + return 42; + } + } @TestMessagingGateway @@ -666,13 +829,217 @@ public class EnableIntegrationTests { } - @Target(ElementType.TYPE) + @TestMessagingGateway2 + public static interface TestGateway2 { + + @Gateway(headers = @GatewayHeader(name = "calledMethod", expression = "#gatewayMethod.name")) + String echo2(String payload); + + } + + @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @MessagingGateway(defaultRequestChannel = "gatewayChannel", defaultHeaders = @GatewayHeader(name = "foo", value = "FOO")) public static @interface TestMessagingGateway { + + String defaultRequestChannel() default ""; + } + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @TestMessagingGateway(defaultRequestChannel = "gatewayChannel2") + public static @interface TestMessagingGateway2 { + + String defaultRequestChannel() default ""; + + } + + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @ServiceActivator(autoStartup = "false", + phase = "23", + inputChannel = "annInput", + outputChannel = "annOutput", + adviceChain = { "annAdvice" }, + poller = @Poller(fixedDelay = "1000")) + public static @interface MyServiceActivator { + + String inputChannel() default ""; + + String outputChannel() default ""; + + String[] adviceChain() default {}; + + String autoStartup() default ""; + + String phase() default ""; + + Poller[] poller() default {}; + } + + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @MyServiceActivator + public static @interface MyServiceActivator1 { + + String inputChannel() default ""; + + String outputChannel() default ""; + + String[] adviceChain() default {}; + + String autoStartup() default ""; + + String phase() default ""; + + Poller[] poller() default {}; + } + + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @MyServiceActivator1 + public static @interface MyServiceActivator2 { + + String inputChannel() default ""; + + } + + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @MyServiceActivator2 + public static @interface MyServiceActivator3 { + + String inputChannel() default ""; + + } + + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @MyServiceActivator3(inputChannel = "annInput3") + public static @interface MyServiceActivator4 { + + String inputChannel() default ""; + + } + + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @MyServiceActivator4 + public static @interface MyServiceActivator5 { + + String inputChannel() default ""; + + } + + // Test prevent infinite recursion + + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @MyServiceActivator5 + public static @interface MyServiceActivator6 { + + String inputChannel() default ""; + + } + + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @MyServiceActivator8 + public static @interface MyServiceActivator7 { + + String inputChannel() default ""; + + } + + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @MyServiceActivator7 + public static @interface MyServiceActivator8 { + + String inputChannel() default ""; + + } + // end test infinite recursion + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ServiceActivator(autoStartup = "false", + phase = "23", + inputChannel = "annInput", + outputChannel = "annOutput", + adviceChain = { "annAdvice" }, + poller = @Poller(fixedDelay = "1000")) + public static @interface MyServiceActivatorNoLocalAtts { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @Aggregator(autoStartup = "false", + phase = "23", + inputChannel = "annInput", + outputChannel = "annOutput", + discardChannel = "annOutput", + poller = @Poller(fixedDelay = "1000")) + public static @interface MyAggregator { + + String inputChannel() default ""; + + String outputChannel() default ""; + + String discardChannel() default ""; + + long sendTimeout() default AbstractCorrelatingMessageHandler.DEFAULT_SEND_TIMEOUT; + + boolean sendPartialResultsOnExpiry() default false; + + String autoStartup() default ""; + + String phase() default ""; + + Poller[] poller() default {}; + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @Aggregator(autoStartup = "false", + phase = "23", + inputChannel = "annInput", + outputChannel = "annOutput", + discardChannel = "annOutput", + sendPartialResultsOnExpiry = false, + sendTimeout = 1000L, + poller = @Poller(fixedDelay = "1000")) + public static @interface MyAggregatorDefaultOverrideDefaults { + + boolean sendPartialResultsOnExpiry() default true; + + long sendTimeout() default 75; + + } + + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @InboundChannelAdapter(value = "counterChannel", autoStartup = "false", phase = "23") + public static @interface MyInboundChannelAdapter { + + String value() default ""; + + String autoStartup() default ""; + + String phase() default ""; + + Poller[] poller() default {}; + + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @MyInboundChannelAdapter + public static @interface MyInboundChannelAdapter1 { + + } // Error because the annotation is on a class; it must be on an interface // @MessagingGateway(defaultRequestChannel = "gatewayChannel", defaultHeaders = @GatewayHeader(name = "foo", value = "FOO"))