Allow multiple @Filter 'value' args for concision

Prior to this change, to specify two or more annotation include/exclude
filters, one would declare @ComponentScan as follows:

    @ComponentScan(basePackages="example.scannable",
       useDefaultFilters=false,
       includeFilters={
           @Filter(MyStereotype.class),
           @Filter(MyComponent.class)
       })

This was because @Filter's 'value' attribute accepted exactly one
argument.

Now, any given @Filter may accept one or more value arguments, allowing
for more concise @ComponentScan declarations:

    @ComponentScan(basePackages="example.scannable",
       useDefaultFilters=false,
       includeFilters=@Filter({MyStereotype.class, MyComponent.class}))

Supplying multiple arguments in this way assumes that they are the same
type of filter, e.g. ANNOTATION, ASSIGNABLE_TYPE, or CUSTOM. To declare
multiple *different* types of filters, multiple @Filter annotations are
still required, e.g.:

    @ComponentScan(
        includeFilters={
            @Filter(type=ANNOTATION, value=MyStereotype.class),
            @Filter(type=ASSIGNABLE_TYPE, value={Foo.class, Bar.class})
        })

Note that specifying zero arguments, e.g. @Filter({}) is nonsensical; it
will have no effect on component scanning, but does not raise an error.

Issue: SPR-8881
This commit is contained in:
Chris Beams 2011-12-06 12:18:10 +00:00
parent df7bda5637
commit 28ff473091
3 changed files with 84 additions and 23 deletions

View File

@ -144,13 +144,18 @@ public @interface ComponentScan {
FilterType type() default FilterType.ANNOTATION;
/**
* The class to use as the filter. In the case of {@link FilterType#ANNOTATION},
* the class will be the annotation itself. In the case of
* {@link FilterType#ASSIGNABLE_TYPE}, the class will be the type that detected
* components should be assignable to. And in the case of {@link FilterType#CUSTOM},
* the class will be an implementation of {@link TypeFilter}.
* The class or classes to use as the filter. In the case of
* {@link FilterType#ANNOTATION}, the class will be the annotation itself. In the
* case of {@link FilterType#ASSIGNABLE_TYPE}, the class will be the type that
* detected components should be assignable to. And in the case of
* {@link FilterType#CUSTOM}, the class will be an implementation of
* {@link TypeFilter}.
* <p>When multiple classes are specified, OR logic is applied, e.g. "include
* types annotated with {@code @Foo} OR {@code @Bar}".
* <p>Specifying zero classes is permitted but will have no effect on component
* scanning.
*/
Class<?> value();
Class<?>[] value(); //doco
}
}

View File

@ -78,11 +78,15 @@ class ComponentScanAnnotationParser {
scanner.setResourcePattern((String)componentScanAttributes.get("resourcePattern"));
for (Filter filter : (Filter[])componentScanAttributes.get("includeFilters")) {
scanner.addIncludeFilter(createTypeFilter(filter));
for (Filter filterAnno : (Filter[])componentScanAttributes.get("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filterAnno)) {
scanner.addIncludeFilter(typeFilter);
}
}
for (Filter filter : (Filter[])componentScanAttributes.get("excludeFilters")) {
scanner.addExcludeFilter(createTypeFilter(filter));
for (Filter filterAnno : (Filter[])componentScanAttributes.get("excludeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filterAnno)) {
scanner.addExcludeFilter(typeFilter);
}
}
List<String> basePackages = new ArrayList<String>();
@ -110,18 +114,31 @@ class ComponentScanAnnotationParser {
return scanner.doScan(basePackages.toArray(new String[]{}));
}
private TypeFilter createTypeFilter(Filter filter) {
switch (filter.type()) {
case ANNOTATION:
@SuppressWarnings("unchecked")
Class<Annotation> filterClass = (Class<Annotation>)filter.value();
return new AnnotationTypeFilter(filterClass);
case ASSIGNABLE_TYPE:
return new AssignableTypeFilter(filter.value());
case CUSTOM:
return BeanUtils.instantiateClass(filter.value(), TypeFilter.class);
default:
throw new IllegalArgumentException("unknown filter type " + filter.type());
private List<TypeFilter> typeFiltersFor(Filter filterAnno) {
List<TypeFilter> typeFilters = new ArrayList<TypeFilter>();
for (Class<?> filterClass : (Class<?>[])filterAnno.value()) {
switch (filterAnno.type()) {
case ANNOTATION:
Assert.isAssignable(Annotation.class, filterClass,
"An error occured when processing a @ComponentScan " +
"ANNOTATION type filter: ");
@SuppressWarnings("unchecked")
Class<Annotation> annoClass = (Class<Annotation>)filterClass;
typeFilters.add(new AnnotationTypeFilter(annoClass));
break;
case ASSIGNABLE_TYPE:
typeFilters.add(new AssignableTypeFilter(filterClass));
break;
case CUSTOM:
Assert.isAssignable(TypeFilter.class, filterClass,
"An error occured when processing a @ComponentScan " +
"CUSTOM type filter: ");
typeFilters.add(BeanUtils.instantiateClass(filterClass, TypeFilter.class));
break;
default:
throw new IllegalArgumentException("unknown filter type " + filterAnno.type());
}
}
return typeFilters;
}
}

View File

@ -41,7 +41,11 @@ import org.springframework.context.annotation.ComponentScanParserTests.CustomAnn
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.util.SerializationTestUtils;
import example.scannable.CustomComponent;
import example.scannable.CustomStereotype;
import example.scannable.DefaultNamedComponent;
import example.scannable.FooService;
import example.scannable.MessageBean;
import example.scannable.ScopedProxyTestBean;
import example.scannable_scoped.CustomScopeAnnotationBean;
import example.scannable_scoped.MyScope;
@ -159,6 +163,24 @@ public class ComponentScanAnnotationIntegrationTests {
assertThat(deserialized.foo(1), equalTo("bar"));
}
@Test
public void withMultipleAnnotationIncludeFilters1() throws IOException, ClassNotFoundException {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ComponentScanWithMultipleAnnotationIncludeFilters1.class);
ctx.refresh();
ctx.getBean(DefaultNamedComponent.class); // @CustomStereotype-annotated
ctx.getBean(MessageBean.class); // @CustomComponent-annotated
}
@Test
public void withMultipleAnnotationIncludeFilters2() throws IOException, ClassNotFoundException {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ComponentScanWithMultipleAnnotationIncludeFilters2.class);
ctx.refresh();
ctx.getBean(DefaultNamedComponent.class); // @CustomStereotype-annotated
ctx.getBean(MessageBean.class); // @CustomComponent-annotated
}
@Test
public void withBasePackagesAndValueAlias() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
@ -222,7 +244,7 @@ class MyScopeMetadataResolver extends AnnotationScopeMetadataResolver {
class ComponentScanWithCustomTypeFilter {
@Bean
@SuppressWarnings({ "rawtypes", "serial", "unchecked" })
public CustomAutowireConfigurer customAutowireConfigurer() {
public static CustomAutowireConfigurer customAutowireConfigurer() {
CustomAutowireConfigurer cac = new CustomAutowireConfigurer();
cac.setCustomQualifierTypes(new HashSet() {{ add(ComponentScanParserTests.CustomAnnotation.class); }});
return cac;
@ -240,6 +262,23 @@ class ComponentScanWithCustomTypeFilter {
includeFilters=@Filter(type=FilterType.ASSIGNABLE_TYPE, value=ScopedProxyTestBean.class))
class ComponentScanWithScopedProxy { }
@Configuration
@ComponentScan(basePackages="example.scannable",
useDefaultFilters=false,
includeFilters={
@Filter(CustomStereotype.class),
@Filter(CustomComponent.class)
}
)
class ComponentScanWithMultipleAnnotationIncludeFilters1 { }
@Configuration
@ComponentScan(basePackages="example.scannable",
useDefaultFilters=false,
includeFilters=@Filter({CustomStereotype.class, CustomComponent.class})
)
class ComponentScanWithMultipleAnnotationIncludeFilters2 { }
@Configuration
@ComponentScan(
value="example.scannable",