From 16be9fc47d0b9dc2dba1ec3aba98af46cef46d68 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 28 Oct 2016 13:09:36 -0400 Subject: [PATCH] INT-3502: filters for `@IntegrationComponentScan` JIRA: https://jira.spring.io/browse/INT-3502 For some use-cases it is really useful to filter `@MessagingGateway` components as any other `@ComponentScan` capable. * Add `useDefaultFilters()`, `includeFilters()` and `excludeFilters()` to the `@IntegrationComponentScan` * Modify `GatewayInterfaceTests` to reflect changes and be sure that logic is applied properly --- .../annotation/IntegrationComponentScan.java | 53 ++++++-- .../IntegrationComponentScanRegistrar.java | 116 +++++++++++++++++- .../gateway/GatewayInterfaceTests.java | 40 +++++- 3 files changed, 188 insertions(+), 21 deletions(-) diff --git a/spring-integration-core/src/main/java/org/springframework/integration/annotation/IntegrationComponentScan.java b/spring-integration-core/src/main/java/org/springframework/integration/annotation/IntegrationComponentScan.java index b8c66204e2..0f18cb4b63 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/annotation/IntegrationComponentScan.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/annotation/IntegrationComponentScan.java @@ -22,7 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; import org.springframework.integration.config.IntegrationComponentScanRegistrar; @@ -36,7 +36,7 @@ import org.springframework.integration.config.IntegrationComponentScanRegistrar; * @author Artem Bilan * @since 4.0 * - * @see ComponentScan + * @see org.springframework.context.annotation.ComponentScan * @see MessagingGateway */ @Retention(RetentionPolicy.RUNTIME) @@ -53,24 +53,53 @@ public @interface IntegrationComponentScan { * @return the array of 'basePackages'. */ @AliasFor("basePackages") - String[] value() default {}; + String[] value() default { }; /** - * Base packages to scan for annotated components. - * The {@link #value()} is an alias for (and mutually exclusive with) this attribute. - * Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names. + * Base packages to scan for annotated components. The {@link #value()} is an alias + * for (and mutually exclusive with) this attribute. Use {@link #basePackageClasses()} + * for a type-safe alternative to String-based package names. * @return the array of 'basePackages'. */ @AliasFor("value") - String[] basePackages() default {}; + String[] basePackages() default { }; /** - * Type-safe alternative to {@link #basePackages()} for specifying the packages - * to scan for annotated components. The package of each class specified will be scanned. - * Consider creating a special no-op marker class or interface in each package - * that serves no purpose other than being referenced by this attribute. + * Type-safe alternative to {@link #basePackages()} for specifying the packages to + * scan for annotated components. The package of each class specified will be scanned. + * Consider creating a special no-op marker class or interface in each package that + * serves no purpose other than being referenced by this attribute. * @return the array of 'basePackageClasses'. */ - Class[] basePackageClasses() default {}; + Class[] basePackageClasses() default { }; + + + /** + * Indicates whether automatic detection of classes annotated with + * {@code @MessagingGateway} should be enabled. + * @since 5.0 + */ + boolean useDefaultFilters() default true; + + /** + * Specifies which types are eligible for component scanning. + *

Further narrows the set of candidate components from everything in + * {@link #basePackages} to everything in the base packages that matches the given + * filter or filters. + *

Note that these filters will be applied in addition to the default filters, if + * specified. Any type under the specified base packages which matches a given filter + * will be included, even if it does not match the default filters (i.e. is not + * annotated with {@code @MessagingGateway}). + * @since 5.0 + * @see #excludeFilters() + */ + Filter[] includeFilters() default { }; + + /** + * Specifies which types are not eligible for component scanning. + * @since 5.0 + * @see #includeFilters() + */ + Filter[] excludeFilters() default { }; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationComponentScanRegistrar.java b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationComponentScanRegistrar.java index b73ed7d28b..6657938c3a 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationComponentScanRegistrar.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationComponentScanRegistrar.java @@ -16,22 +16,40 @@ package org.springframework.integration.config; +import java.lang.annotation.Annotation; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.Aware; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AspectJTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.RegexPatternTypeFilter; import org.springframework.core.type.filter.TypeFilter; import org.springframework.integration.annotation.MessagingGateway; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -43,14 +61,17 @@ import org.springframework.util.StringUtils; * @since 4.0 */ public class IntegrationComponentScanRegistrar implements ImportBeanDefinitionRegistrar, - ResourceLoaderAware { + ResourceLoaderAware, EnvironmentAware { private final Map componentRegistrars = new HashMap(); private ResourceLoader resourceLoader; + private Environment environment; + public IntegrationComponentScanRegistrar() { - this.componentRegistrars.put(new AnnotationTypeFilter(MessagingGateway.class, true), new MessagingGatewayRegistrar()); + this.componentRegistrars.put(new AnnotationTypeFilter(MessagingGateway.class, true), + new MessagingGatewayRegistrar()); } @Override @@ -58,6 +79,11 @@ public class IntegrationComponentScanRegistrar implements ImportBeanDefinitionRe this.resourceLoader = resourceLoader; } + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { Map componentScan = importingClassMetadata @@ -91,18 +117,32 @@ public class IntegrationComponentScanRegistrar implements ImportBeanDefinitionRe } }; - for (TypeFilter typeFilter : this.componentRegistrars.keySet()) { - scanner.addIncludeFilter(typeFilter); + if ((boolean) componentScan.get("useDefaultFilters")) { + for (TypeFilter typeFilter : this.componentRegistrars.keySet()) { + scanner.addIncludeFilter(typeFilter); + } } + for (AnnotationAttributes filter : (AnnotationAttributes[]) componentScan.get("includeFilters")) { + for (TypeFilter typeFilter : typeFiltersFor(filter, registry)) { + scanner.addIncludeFilter(typeFilter); + } + } + for (AnnotationAttributes filter : (AnnotationAttributes[]) componentScan.get("excludeFilters")) { + for (TypeFilter typeFilter : typeFiltersFor(filter, registry)) { + scanner.addExcludeFilter(typeFilter); + } + } + + scanner.setResourceLoader(this.resourceLoader); for (String basePackage : basePackages) { Set candidateComponents = scanner.findCandidateComponents(basePackage); for (BeanDefinition candidateComponent : candidateComponents) { if (candidateComponent instanceof AnnotatedBeanDefinition) { - for (ImportBeanDefinitionRegistrar importBeanDefinitionRegistrar : this.componentRegistrars.values()) { - importBeanDefinitionRegistrar.registerBeanDefinitions(((AnnotatedBeanDefinition) candidateComponent).getMetadata(), + for (ImportBeanDefinitionRegistrar registrar : this.componentRegistrars.values()) { + registrar.registerBeanDefinitions(((AnnotatedBeanDefinition) candidateComponent).getMetadata(), registry); } } @@ -110,4 +150,68 @@ public class IntegrationComponentScanRegistrar implements ImportBeanDefinitionRe } } + private List typeFiltersFor(AnnotationAttributes filter, BeanDefinitionRegistry registry) { + List typeFilters = new ArrayList<>(); + FilterType filterType = filter.getEnum("type"); + + for (Class filterClass : filter.getClassArray("classes")) { + switch (filterType) { + case ANNOTATION: + Assert.isAssignable(Annotation.class, filterClass, + "An error occurred while processing a @IntegrationComponentScan ANNOTATION type filter: "); + @SuppressWarnings("unchecked") + Class annotationType = (Class) filterClass; + typeFilters.add(new AnnotationTypeFilter(annotationType)); + break; + case ASSIGNABLE_TYPE: + typeFilters.add(new AssignableTypeFilter(filterClass)); + break; + case CUSTOM: + Assert.isAssignable(TypeFilter.class, filterClass, + "An error occurred while processing a @IntegrationComponentScan CUSTOM type filter: "); + TypeFilter typeFilter = BeanUtils.instantiateClass(filterClass, TypeFilter.class); + invokeAwareMethods(filter, this.environment, this.resourceLoader, registry); + typeFilters.add(typeFilter); + break; + default: + throw new IllegalArgumentException("Filter type not supported with Class value: " + filterType); + } + } + + for (String expression : filter.getStringArray("pattern")) { + switch (filterType) { + case ASPECTJ: + typeFilters.add(new AspectJTypeFilter(expression, this.resourceLoader.getClassLoader())); + break; + case REGEX: + typeFilters.add(new RegexPatternTypeFilter(Pattern.compile(expression))); + break; + default: + throw new IllegalArgumentException("Filter type not supported with String pattern: " + filterType); + } + } + + return typeFilters; + } + + private static void invokeAwareMethods(Object parserStrategyBean, Environment environment, + ResourceLoader resourceLoader, BeanDefinitionRegistry registry) { + if (parserStrategyBean instanceof Aware) { + if (parserStrategyBean instanceof BeanClassLoaderAware) { + ClassLoader classLoader = (registry instanceof ConfigurableBeanFactory ? + ((ConfigurableBeanFactory) registry).getBeanClassLoader() : resourceLoader.getClassLoader()); + ((BeanClassLoaderAware) parserStrategyBean).setBeanClassLoader(classLoader); + } + if (parserStrategyBean instanceof BeanFactoryAware && registry instanceof BeanFactory) { + ((BeanFactoryAware) parserStrategyBean).setBeanFactory((BeanFactory) registry); + } + if (parserStrategyBean instanceof EnvironmentAware) { + ((EnvironmentAware) parserStrategyBean).setEnvironment(environment); + } + if (parserStrategyBean instanceof ResourceLoaderAware) { + ((ResourceLoaderAware) parserStrategyBean).setResourceLoader(resourceLoader); + } + } + } + } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayInterfaceTests.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayInterfaceTests.java index 67603539fa..b4a7c2aa52 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayInterfaceTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayInterfaceTests.java @@ -30,6 +30,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.Collections; import java.util.Map; @@ -110,13 +114,15 @@ public class GatewayInterfaceTests { @Autowired @Qualifier("&gatewayInterfaceTests$NoExecGateway") private GatewayProxyFactoryBean noExecGatewayFB; - @Autowired private SimpleAsyncTaskExecutor exec; @Autowired private AutoCreateChannelService autoCreateChannelService; + @Autowired(required = false) + private NotAGatewayByScanFilter notAGatewayByScanFilter; + @Test public void testWithServiceSuperclassAnnotatedMethod() throws Exception { @@ -383,6 +389,13 @@ public class GatewayInterfaceTests { }); assertTrue(latch.await(10, TimeUnit.SECONDS)); assertThat(result2.get().getName(), startsWith("exec-")); + + /* + @IntegrationComponentScan(useDefaultFilters = false, + includeFilters = @ComponentScan.Filter(TestMessagingGateway.class)) + excludes this a candidate + */ + assertNull(this.notAGatewayByScanFilter); } @Test @@ -402,6 +415,7 @@ public class GatewayInterfaceTests { } public interface Bar extends Foo { + @Gateway(requestChannel = "requestChannelBar") void bar(String payload); @@ -409,7 +423,9 @@ public class GatewayInterfaceTests { } public static class NotAnInterface { - public void fail(String payload) { } + + public void fail(String payload) { + } } public interface Baz { @@ -441,7 +457,8 @@ public class GatewayInterfaceTests { @Configuration @ComponentScan(includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = AutoCreateChannelService.class)) - @IntegrationComponentScan + @IntegrationComponentScan(useDefaultFilters = false, + includeFilters = @ComponentScan.Filter(TestMessagingGateway.class)) @EnableIntegration public static class TestConfig { @@ -492,6 +509,7 @@ public class GatewayInterfaceTests { } @MessagingGateway + @TestMessagingGateway public interface Int2634Gateway { @Gateway(requestChannel = "gatewayChannel", payloadExpression = "#args[0]") @@ -506,6 +524,7 @@ public class GatewayInterfaceTests { } @MessagingGateway(asyncExecutor = "exec") + @TestMessagingGateway public interface ExecGateway { @Gateway(requestChannel = "gatewayThreadChannel") @@ -517,6 +536,7 @@ public class GatewayInterfaceTests { } @MessagingGateway(asyncExecutor = AnnotationConstants.NULL) + @TestMessagingGateway public interface NoExecGateway { @Gateway(requestChannel = "gatewayThreadChannel") @@ -526,10 +546,24 @@ public class GatewayInterfaceTests { @MessagingGateway(defaultRequestChannel = "autoCreateChannel") + @TestMessagingGateway public interface AutoCreateChannelGateway { String foo(String payload); } + @MessagingGateway + public interface NotAGatewayByScanFilter { + + String foo(String payload); + + } + + @Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE }) + @Retention(RetentionPolicy.RUNTIME) + public @interface TestMessagingGateway { + + } + }