diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java index 7c717714c75a..26e73448cf4f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java @@ -23,6 +23,7 @@ import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; @@ -49,6 +50,7 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationAttributesFilter; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.core.type.filter.TypeFilter; @@ -205,7 +207,7 @@ public void resetFilters(boolean useDefaultFilters) { */ @SuppressWarnings("unchecked") protected void registerDefaultFilters() { - this.includeFilters.add(new AnnotationTypeFilter(Component.class)); + this.includeFilters.add(new AnnotationAttributesFilter(Component.class, Map.of("scannable", true))); ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader(); try { this.includeFilters.add(new AnnotationTypeFilter( diff --git a/spring-context/src/main/java/org/springframework/stereotype/Component.java b/spring-context/src/main/java/org/springframework/stereotype/Component.java index a6e09edc6e79..f199e1a985a3 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Component.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Component.java @@ -52,4 +52,9 @@ */ String value() default ""; + /** + * return true if this component can be scanned. + */ + boolean scannable() default true; + } diff --git a/spring-context/src/main/java/org/springframework/stereotype/Controller.java b/spring-context/src/main/java/org/springframework/stereotype/Controller.java index 6629feea4b3e..fca7cb133a79 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Controller.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Controller.java @@ -53,4 +53,6 @@ @AliasFor(annotation = Component.class) String value() default ""; + @AliasFor(annotation = Component.class, attribute = "scannable") + boolean scannable() default true; } diff --git a/spring-context/src/main/java/org/springframework/stereotype/Repository.java b/spring-context/src/main/java/org/springframework/stereotype/Repository.java index 22e54ab8cd0f..e7d20396c781 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Repository.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Repository.java @@ -69,4 +69,7 @@ @AliasFor(annotation = Component.class) String value() default ""; + @AliasFor(annotation = Component.class, attribute = "scannable") + boolean scannable() default true; + } diff --git a/spring-context/src/main/java/org/springframework/stereotype/Service.java b/spring-context/src/main/java/org/springframework/stereotype/Service.java index 5c714540bbca..0ddc3ecb44a3 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Service.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Service.java @@ -55,4 +55,6 @@ @AliasFor(annotation = Component.class) String value() default ""; + @AliasFor(annotation = Component.class, attribute = "scannable") + boolean scannable() default true; } diff --git a/spring-context/src/test/java/example/scannable/UnscannableController.java b/spring-context/src/test/java/example/scannable/UnscannableController.java new file mode 100644 index 000000000000..d6254648c6f7 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/UnscannableController.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2012 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 + * + * https://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 example.scannable; + +import org.springframework.stereotype.Controller; + +/** + * @author Shen Feng + */ +@Controller(scannable = false) +public class UnscannableController { +} diff --git a/spring-context/src/test/java/example/scannable/UnscannableDao.java b/spring-context/src/test/java/example/scannable/UnscannableDao.java new file mode 100644 index 000000000000..bbc3bdefe7a6 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/UnscannableDao.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2012 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 + * + * https://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 example.scannable; + +import org.springframework.stereotype.Repository; + +/** + * @author Shen Feng + */ +@Repository(scannable = false) +public class UnscannableDao { +} diff --git a/spring-context/src/test/java/example/scannable/UnscannableSerivce.java b/spring-context/src/test/java/example/scannable/UnscannableSerivce.java new file mode 100644 index 000000000000..0a086206a719 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/UnscannableSerivce.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2012 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 + * + * https://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 example.scannable; + +import org.springframework.stereotype.Service; + +/** + * @author Shen Feng + */ +@Service(scannable = false) +public class UnscannableSerivce { +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java index a5e2d44ee210..396a21f85f2d 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -56,6 +57,7 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.type.filter.AnnotationAttributesFilter; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.core.type.filter.RegexPatternTypeFilter; @@ -204,7 +206,7 @@ void customAnnotationTypeIncludeFilterWithIndex() { } private void testCustomAnnotationTypeIncludeFilter(ClassPathScanningCandidateComponentProvider provider) { - provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + provider.addIncludeFilter(new AnnotationAttributesFilter(Component.class, Map.of("scannable", true))); testDefault(provider, false, false); } @@ -247,7 +249,7 @@ void customSupportedIncludeAndExcludeFilterWithIndex() { } private void testCustomSupportedIncludeAndExcludeFilter(ClassPathScanningCandidateComponentProvider provider) { - provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + provider.addIncludeFilter(new AnnotationAttributesFilter(Component.class, Map.of("scannable", true))); provider.addExcludeFilter(new AnnotationTypeFilter(Service.class)); provider.addExcludeFilter(new AnnotationTypeFilter(Repository.class)); Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); @@ -346,7 +348,7 @@ void withClassType() { @Test void withMultipleMatchingFilters() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); - provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + provider.addIncludeFilter(new AnnotationAttributesFilter(Component.class, Map.of("scannable", true))); provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class)); Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, FooServiceImpl.class, @@ -356,7 +358,7 @@ void withMultipleMatchingFilters() { @Test void excludeTakesPrecedence() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); - provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + provider.addIncludeFilter(new AnnotationAttributesFilter(Component.class, Map.of("scannable", true))); provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class)); provider.addExcludeFilter(new AssignableTypeFilter(FooService.class)); Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java index 015075456768..bfb3c714fd06 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java @@ -268,6 +268,17 @@ public void withBasePackagesAndValueAlias() { assertThat(ctx.containsBean("fooServiceImpl")).isTrue(); } + @Test + public void testScannableBean() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithScannable.class); + ctx.refresh(); + assertThat(ctx.containsBean("unscannableController")).isFalse(); + assertThat(ctx.containsBean("unscannableSerivce")).isFalse(); + assertThat(ctx.containsBean("unscannableDao")).isFalse(); + assertThat(ctx.containsBean("fooServiceImpl")).isTrue(); + } + @Configuration @ComponentScan @@ -461,4 +472,11 @@ class ComponentScanWithMultipleAnnotationIncludeFilters2 {} basePackageClasses = example.scannable.PackageMarker.class) class ComponentScanWithBasePackagesAndValueAlias {} +@ComponentScan( + value = "example.scannable") +class ComponentScanWithScannable{ + +} + + diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java index 96874edb2eac..46f0b1f6321e 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java @@ -73,7 +73,7 @@ public boolean match(MetadataReader metadataReader, MetadataReaderFactory metada // Optimization to avoid creating ClassReader for superclass. Boolean superClassMatch = matchSuperClass(superClassName); if (superClassMatch != null) { - if (superClassMatch.booleanValue()) { + if (superClassMatch) { return true; } } @@ -99,7 +99,7 @@ public boolean match(MetadataReader metadataReader, MetadataReaderFactory metada // Optimization to avoid creating ClassReader for superclass Boolean interfaceMatch = matchInterface(ifc); if (interfaceMatch != null) { - if (interfaceMatch.booleanValue()) { + if (interfaceMatch) { return true; } } diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationAttributesFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationAttributesFilter.java new file mode 100644 index 000000000000..2878a55c54ff --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationAttributesFilter.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2012 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 + * + * https://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.core.type.filter; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + + +/** + * A {@link TypeFilter} which matches classes with a given annotation and attributes. + * + * @author Shen Feng + */ + +public class AnnotationAttributesFilter extends AnnotationTypeFilter { + + private final Map attributes; + + public AnnotationAttributesFilter(Class annotationType) { + super(annotationType); + this.attributes = Collections.emptyMap(); + } + + public AnnotationAttributesFilter(Class annotationType, Map attributes) { + super(annotationType); + this.attributes = attributes; + } + + public AnnotationAttributesFilter(Class annotationType, boolean considerMetaAnnotations) { + super(annotationType, considerMetaAnnotations); + this.attributes = Collections.emptyMap(); + } + + public AnnotationAttributesFilter(Class annotationType, Map attributes, boolean considerMetaAnnotations) { + super(annotationType, considerMetaAnnotations); + this.attributes = attributes; + } + + public AnnotationAttributesFilter(Class annotationType, boolean considerMetaAnnotations, boolean considerInterfaces) { + super(annotationType, considerMetaAnnotations, considerInterfaces); + this.attributes = Collections.emptyMap(); + } + + public AnnotationAttributesFilter(Class annotationType, Map attributes, boolean considerMetaAnnotations, boolean considerInterfaces) { + super(annotationType, considerMetaAnnotations, considerInterfaces); + this.attributes = attributes; + } + + @Override + protected boolean matchSelf(MetadataReader metadataReader) { + if (!super.matchSelf(metadataReader)) { + return false; + } + AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata(); + + Map annotationAttributes = annotationMetadata.getAnnotationAttributes(getAnnotationType().getName()); + return matchAttributes(annotationAttributes); + + + } + + private boolean matchAttributes(Map annotationAttributes) { + if (annotationAttributes == null) { + return false; + } + Set keys = this.attributes.keySet(); + for (String key : keys) { + Object value = annotationAttributes.get(key); + //use default value when value is null + value = value == null ? AnnotationUtils.getDefaultValue(getAnnotationType(), key) : value; + if (!Objects.equals(value, this.attributes.get(key))) { + return false; + } + } + return true; + } + + @Override + protected Boolean matchSuperClass(String superClassName) { + return this.match(superClassName); + } + + @Override + protected Boolean matchInterface(String interfaceName) { + return this.match(interfaceName); + } + + @Nullable + protected Boolean match(String typeName) { + if (Object.class.getName().equals(typeName)) { + return false; + } + else if (typeName.startsWith("java")) { + if (!getAnnotationType().getName().startsWith("java")) { + // Standard Java types do not have non-standard annotations on them -> + // skip any load attempt, in particular for Java language interfaces. + return false; + } + try { + Class clazz = ClassUtils.forName(typeName, getClass().getClassLoader()); + Annotation annotation = this.considerMetaAnnotations ? AnnotationUtils.getAnnotation(clazz, getAnnotationType()) : + clazz.getAnnotation(getAnnotationType()); + if (annotation == null) { + return false; + } + Map annotationAttributes = AnnotationUtils.getAnnotationAttributes(annotation); + return matchAttributes(annotationAttributes); + } + catch (Throwable ex) { + // Class not regularly loadable - can't determine a match that way. + } + + } + return null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java index 5584edcb688a..a1f23eee73c3 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java @@ -46,7 +46,7 @@ public class AnnotationTypeFilter extends AbstractTypeHierarchyTraversingFilter private final Class annotationType; - private final boolean considerMetaAnnotations; + protected final boolean considerMetaAnnotations; /** diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java index 68abaf310f5a..0ef116ff18f1 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java @@ -23,6 +23,7 @@ import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; /** @@ -58,4 +59,6 @@ @AliasFor(annotation = Controller.class) String value() default ""; + @AliasFor(annotation = Component.class, attribute = "scannable") + boolean scannable() default true; }