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
This commit is contained in:
Artem Bilan
2016-10-28 13:09:36 -04:00
committed by Gary Russell
parent 49638fdea2
commit 16be9fc47d
3 changed files with 188 additions and 21 deletions

View File

@@ -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.
* <p>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.
* <p>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 { };
}

View File

@@ -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<TypeFilter, ImportBeanDefinitionRegistrar> componentRegistrars = new HashMap<TypeFilter, ImportBeanDefinitionRegistrar>();
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<String, Object> 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<BeanDefinition> 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<TypeFilter> typeFiltersFor(AnnotationAttributes filter, BeanDefinitionRegistry registry) {
List<TypeFilter> 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<Annotation> annotationType = (Class<Annotation>) 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);
}
}
}
}

View File

@@ -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 {
}
}