Skip to content

Commit 73832f8

Browse files
philwebbcbeams
authored andcommitted
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
1 parent 512ffbb commit 73832f8

File tree

7 files changed

+91
-25
lines changed

7 files changed

+91
-25
lines changed

spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
* Provides support parallel with Spring XML's {@code <context:component-scan>} element.
3131
*
3232
* <p>One of {@link #basePackageClasses()}, {@link #basePackages()} or its alias
33-
* {@link #value()} must be specified.
33+
* {@link #value()} may be specified to define specific packages to scan. If specific
34+
* packages are not defined scanning will occur from the package of the
35+
* class with this annotation.
3436
*
3537
* <p>Note that the {@code <context:component-scan>} element has an
3638
* {@code annotation-config} attribute, however this annotation does not. This is because

spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public ComponentScanAnnotationParser(
6565
}
6666

6767

68-
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan) {
68+
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, String declaringClass) {
6969
ClassPathBeanDefinitionScanner scanner =
7070
new ClassPathBeanDefinitionScanner(registry, componentScan.getBoolean("useDefaultFilters"));
7171

@@ -118,7 +118,7 @@ public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan) {
118118
}
119119

120120
if (basePackages.isEmpty()) {
121-
throw new IllegalStateException("At least one base package must be specified");
121+
basePackages.add(ClassUtils.getPackageName(declaringClass));
122122
}
123123

124124
return scanner.doScan(basePackages.toArray(new String[]{}));

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,8 @@ protected AnnotationMetadata doProcessConfigurationClass(
210210
AnnotationAttributes componentScan = attributesFor(metadata, ComponentScan.class);
211211
if (componentScan != null) {
212212
// the config class is annotated with @ComponentScan -> perform the scan immediately
213-
Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan);
213+
Set<BeanDefinitionHolder> scannedBeanDefinitions =
214+
this.componentScanParser.parse(componentScan, metadata.getClassName());
214215

215216
// check the set of scanned definitions for any further config classes and parse recursively if necessary
216217
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.scannable_implicitbasepackage;
17+
18+
import org.springframework.context.annotation.ComponentScan;
19+
import org.springframework.context.annotation.Configuration;
20+
21+
/**
22+
* @author Phillip Webb
23+
*/
24+
@Configuration
25+
@ComponentScan
26+
public class ComponentScanAnnotatedConfigWithImplicitBasePackage {
27+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.scannable_implicitbasepackage;
17+
18+
import org.springframework.stereotype.Component;
19+
20+
/**
21+
* @author Phillip Webb
22+
*/
23+
@Component
24+
public class ScannedComponent {
25+
26+
}

spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2011 the original author or authors.
2+
* Copyright 2002-2012 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,9 +21,7 @@
2121
import static org.hamcrest.CoreMatchers.not;
2222
import static org.hamcrest.CoreMatchers.notNullValue;
2323
import static org.hamcrest.CoreMatchers.sameInstance;
24-
import static org.hamcrest.Matchers.containsString;
2524
import static org.junit.Assert.assertThat;
26-
import static org.junit.Assert.fail;
2725
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition;
2826

2927
import java.io.IOException;
@@ -47,6 +45,7 @@
4745
import example.scannable.FooService;
4846
import example.scannable.MessageBean;
4947
import example.scannable.ScopedProxyTestBean;
48+
import example.scannable_implicitbasepackage.ComponentScanAnnotatedConfigWithImplicitBasePackage;
5049
import example.scannable_scoped.CustomScopeAnnotationBean;
5150
import example.scannable_scoped.MyScope;
5251

@@ -93,6 +92,18 @@ public void viaContextRegistration_WithValueAttribute() {
9392
ctx.containsBean("fooServiceImpl"), is(true));
9493
}
9594

95+
@Test
96+
public void viaContextRegistration_FromPackageOfConfigClass() {
97+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
98+
ctx.register(ComponentScanAnnotatedConfigWithImplicitBasePackage.class);
99+
ctx.refresh();
100+
ctx.getBean(ComponentScanAnnotatedConfigWithImplicitBasePackage.class);
101+
assertThat("config class bean not found", ctx.containsBeanDefinition("componentScanAnnotatedConfigWithImplicitBasePackage"), is(true));
102+
assertThat("@ComponentScan annotated @Configuration class registered directly against " +
103+
"AnnotationConfigApplicationContext did not trigger component scanning as expected",
104+
ctx.containsBean("scannedComponent"), is(true));
105+
}
106+
96107
@Test
97108
public void viaBeanRegistration() {
98109
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
@@ -110,18 +121,6 @@ public void viaBeanRegistration() {
110121
ctx.containsBean("fooServiceImpl"), is(true));
111122
}
112123

113-
@Test
114-
public void invalidComponentScanDeclaration_noPackagesSpecified() {
115-
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
116-
ctx.register(ComponentScanWithNoPackagesConfig.class);
117-
try {
118-
ctx.refresh();
119-
fail("Expected exception when parsing @ComponentScan definition that declares no packages");
120-
} catch (IllegalStateException ex) {
121-
assertThat(ex.getMessage(), containsString("At least one base package must be specified"));
122-
}
123-
}
124-
125124
@Test
126125
public void withCustomBeanNameGenerator() {
127126
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();

spring-core/src/main/java/org/springframework/util/ClassUtils.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2011 the original author or authors.
2+
* Copyright 2002-2012 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -462,17 +462,28 @@ public static String getClassFileName(Class<?> clazz) {
462462
}
463463

464464
/**
465-
* Determine the name of the package of the given class:
466-
* e.g. "java.lang" for the <code>java.lang.String</code> class.
465+
* Determine the name of the package of the given class,
466+
* e.g. "java.lang" for the {@code java.lang.String} class.
467467
* @param clazz the class
468468
* @return the package name, or the empty String if the class
469469
* is defined in the default package
470470
*/
471471
public static String getPackageName(Class<?> clazz) {
472472
Assert.notNull(clazz, "Class must not be null");
473-
String className = clazz.getName();
474-
int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR);
475-
return (lastDotIndex != -1 ? className.substring(0, lastDotIndex) : "");
473+
return getPackageName(clazz.getName());
474+
}
475+
476+
/**
477+
* Determine the name of the package of the given fully-qualified class name,
478+
* e.g. "java.lang" for the {@code java.lang.String} class name.
479+
* @param fqClassName the fully-qualified class name
480+
* @return the package name, or the empty String if the class
481+
* is defined in the default package
482+
*/
483+
public static String getPackageName(String fqClassName) {
484+
Assert.notNull(fqClassName, "Class name must not be null");
485+
int lastDotIndex = fqClassName.lastIndexOf(PACKAGE_SEPARATOR);
486+
return (lastDotIndex != -1 ? fqClassName.substring(0, lastDotIndex) : "");
476487
}
477488

478489
/**

0 commit comments

Comments
 (0)