From 73832f8c6e51eb305b88dfc11815801d18b4d567 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 6 Sep 2012 19:56:38 -0700 Subject: [PATCH] Support inferred base package for @ComponentScan Prior to this change, @ComponentScan required the declaration of exactly one of the #value, #basePackage or #basePackageClasses attributes in order to determine which package(s) to scan. This commit introduces support for base package inference, relaxing the above requirement and falling back to scanning the package in which the @ComponentScan-annotated class is declared. Issue: SPR-9586 --- .../context/annotation/ComponentScan.java | 4 ++- .../ComponentScanAnnotationParser.java | 4 +-- .../annotation/ConfigurationClassParser.java | 3 +- ...nnotatedConfigWithImplicitBasePackage.java | 27 +++++++++++++++++ .../ScannedComponent.java | 26 +++++++++++++++++ ...mponentScanAnnotationIntegrationTests.java | 29 +++++++++---------- .../org/springframework/util/ClassUtils.java | 23 +++++++++++---- 7 files changed, 91 insertions(+), 25 deletions(-) create mode 100644 spring-context/src/test/java/example/scannable_implicitbasepackage/ComponentScanAnnotatedConfigWithImplicitBasePackage.java create mode 100644 spring-context/src/test/java/example/scannable_implicitbasepackage/ScannedComponent.java diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java index 26a3a6cec9..ffd202737b 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java @@ -30,7 +30,9 @@ import org.springframework.core.type.filter.TypeFilter; * Provides support parallel with Spring XML's {@code } element. * *

One of {@link #basePackageClasses()}, {@link #basePackages()} or its alias - * {@link #value()} must be specified. + * {@link #value()} may be specified to define specific packages to scan. If specific + * packages are not defined scanning will occur from the package of the + * class with this annotation. * *

Note that the {@code } element has an * {@code annotation-config} attribute, however this annotation does not. This is because diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java index a549b8d0af..4993306cc7 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java @@ -65,7 +65,7 @@ class ComponentScanAnnotationParser { } - public Set parse(AnnotationAttributes componentScan) { + public Set parse(AnnotationAttributes componentScan, String declaringClass) { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, componentScan.getBoolean("useDefaultFilters")); @@ -118,7 +118,7 @@ class ComponentScanAnnotationParser { } if (basePackages.isEmpty()) { - throw new IllegalStateException("At least one base package must be specified"); + basePackages.add(ClassUtils.getPackageName(declaringClass)); } return scanner.doScan(basePackages.toArray(new String[]{})); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 3f92029066..cf6e12f296 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -210,7 +210,8 @@ class ConfigurationClassParser { AnnotationAttributes componentScan = attributesFor(metadata, ComponentScan.class); if (componentScan != null) { // the config class is annotated with @ComponentScan -> perform the scan immediately - Set scannedBeanDefinitions = this.componentScanParser.parse(componentScan); + Set scannedBeanDefinitions = + this.componentScanParser.parse(componentScan, metadata.getClassName()); // check the set of scanned definitions for any further config classes and parse recursively if necessary for (BeanDefinitionHolder holder : scannedBeanDefinitions) { diff --git a/spring-context/src/test/java/example/scannable_implicitbasepackage/ComponentScanAnnotatedConfigWithImplicitBasePackage.java b/spring-context/src/test/java/example/scannable_implicitbasepackage/ComponentScanAnnotatedConfigWithImplicitBasePackage.java new file mode 100644 index 0000000000..0df041daac --- /dev/null +++ b/spring-context/src/test/java/example/scannable_implicitbasepackage/ComponentScanAnnotatedConfigWithImplicitBasePackage.java @@ -0,0 +1,27 @@ +/* + * 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 + * + * 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 example.scannable_implicitbasepackage; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * @author Phillip Webb + */ +@Configuration +@ComponentScan +public class ComponentScanAnnotatedConfigWithImplicitBasePackage { +} diff --git a/spring-context/src/test/java/example/scannable_implicitbasepackage/ScannedComponent.java b/spring-context/src/test/java/example/scannable_implicitbasepackage/ScannedComponent.java new file mode 100644 index 0000000000..f47e5d3a5d --- /dev/null +++ b/spring-context/src/test/java/example/scannable_implicitbasepackage/ScannedComponent.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 + * + * 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 example.scannable_implicitbasepackage; + +import org.springframework.stereotype.Component; + +/** + * @author Phillip Webb + */ +@Component +public class ScannedComponent { + +} 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 9ed43a3bbf..7b461584a0 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * 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. @@ -21,9 +21,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.sameInstance; -import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; import java.io.IOException; @@ -47,6 +45,7 @@ import example.scannable.DefaultNamedComponent; import example.scannable.FooService; import example.scannable.MessageBean; import example.scannable.ScopedProxyTestBean; +import example.scannable_implicitbasepackage.ComponentScanAnnotatedConfigWithImplicitBasePackage; import example.scannable_scoped.CustomScopeAnnotationBean; import example.scannable_scoped.MyScope; @@ -93,6 +92,18 @@ public class ComponentScanAnnotationIntegrationTests { ctx.containsBean("fooServiceImpl"), is(true)); } + @Test + public void viaContextRegistration_FromPackageOfConfigClass() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanAnnotatedConfigWithImplicitBasePackage.class); + ctx.refresh(); + ctx.getBean(ComponentScanAnnotatedConfigWithImplicitBasePackage.class); + assertThat("config class bean not found", ctx.containsBeanDefinition("componentScanAnnotatedConfigWithImplicitBasePackage"), is(true)); + assertThat("@ComponentScan annotated @Configuration class registered directly against " + + "AnnotationConfigApplicationContext did not trigger component scanning as expected", + ctx.containsBean("scannedComponent"), is(true)); + } + @Test public void viaBeanRegistration() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); @@ -110,18 +121,6 @@ public class ComponentScanAnnotationIntegrationTests { ctx.containsBean("fooServiceImpl"), is(true)); } - @Test - public void invalidComponentScanDeclaration_noPackagesSpecified() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(ComponentScanWithNoPackagesConfig.class); - try { - ctx.refresh(); - fail("Expected exception when parsing @ComponentScan definition that declares no packages"); - } catch (IllegalStateException ex) { - assertThat(ex.getMessage(), containsString("At least one base package must be specified")); - } - } - @Test public void withCustomBeanNameGenerator() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index cbd6bae580..e321291620 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * 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. @@ -462,17 +462,28 @@ public abstract class ClassUtils { } /** - * Determine the name of the package of the given class: - * e.g. "java.lang" for the java.lang.String class. + * Determine the name of the package of the given class, + * e.g. "java.lang" for the {@code java.lang.String} class. * @param clazz the class * @return the package name, or the empty String if the class * is defined in the default package */ public static String getPackageName(Class clazz) { Assert.notNull(clazz, "Class must not be null"); - String className = clazz.getName(); - int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); - return (lastDotIndex != -1 ? className.substring(0, lastDotIndex) : ""); + return getPackageName(clazz.getName()); + } + + /** + * Determine the name of the package of the given fully-qualified class name, + * e.g. "java.lang" for the {@code java.lang.String} class name. + * @param fqClassName the fully-qualified class name + * @return the package name, or the empty String if the class + * is defined in the default package + */ + public static String getPackageName(String fqClassName) { + Assert.notNull(fqClassName, "Class name must not be null"); + int lastDotIndex = fqClassName.lastIndexOf(PACKAGE_SEPARATOR); + return (lastDotIndex != -1 ? fqClassName.substring(0, lastDotIndex) : ""); } /**