第一步:在com.xxx.hyl.boot包下创建一个子包-beans,并在该包中创建一个Bean。
package com.xxx.hyl.boot.beans;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
/***
* 使用{@link ComponentScan}不指定包路径,当前类被扫描进IoC容器
* @author 君战
* */
@Component
public class TestBean {
}
第二步:在com.xxx.hyl.boot包下创建一个类,使用@ComponentScan注解,然后创建注解驱动 Spring 应用上下文。
package com.xxx.hyl.boot;
import com.xxx.hyl.boot.beans.TestBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
/***
* 演示使用{@link ComponentScan}注解时不指定包路径,IoC容器依然可以扫描当前包下及其子包下的类。
* @author 君战
*
* */
@ComponentScan
public class ComponentScanDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 使用当前类来作为配置类
context.register(ComponentScanDemo.class);
// 调用 refresh方法来刷新应用上下文
context.refresh();
// 通过getBean方法来获取TestBean在IoC容器中的实例
TestBean testBean = context.getBean(TestBean.class);
System.out.println("TestBean : " + testBean);
}
}
第三步:查看控制台
可以发现,我们做到了和SpringBoot一样的功能,只添加了一个@ComponentScan注解,并未指定任何包路径,TestBean依然被装配进IoC容器中。
其实这并不是Spring Boot独创的,而是Spring Framework原本就有的功能,只不过因为Spring Boot的风靡而让这种用法走入大家的视野。废话不多说,接下来就进入底层源码分析。在ComponentScanAnnotationParser的parse方法中,针对@ComponentScan注解的方法返回值进行解析,并将解析到的数据设置到ClassPathBeanDefinitionScanner中。这里面有一个扩展知识点,basepackages是可以设置占位符的,具体分析请查看 你所不知道的@ComponentScan注解用法之包路径占位符 。重点是接下来的判断basePackages集合是否为空(如果未配置basePackages和basePackageClasses,那么basePackages集合就是为空),如果为空,则去获取配置类所在包来作为要扫描的包路径。重点就是使用basePackages将ClassUtils的getPackageName方法返回值保存。
// ComponentScanAnnotationParser#parse
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
// 获取@ComponentScan注解nameGenerator()方法返回值
Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
BeanUtils.instantiateClass(generatorClass));
// 获取@ComponentScan注解scopedProxy()方法返回值
ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
scanner.setScopedProxyMode(scopedProxyMode);
} else {
Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
}
// 获取@ComponentScan注解resourcePattern()方法返回值
scanner.setResourcePattern(componentScan.getString("resourcePattern"));
// 获取@ComponentScan注解includeFilters()方法返回值
for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addIncludeFilter(typeFilter);
}
}
// 获取@ComponentScan注解excludeFilters()方法返回值
for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addExcludeFilter(typeFilter);
}
}
// 获取@ComponentScan注解lazyInit()方法返回值
boolean lazyInit = componentScan.getBoolean("lazyInit");
if (lazyInit) {
scanner.getBeanDefinitionDefaults().setLazyInit(true);
}
Set<String> basePackages = new LinkedHashSet<>();
// 获取@ComponentScan注解basePackages()方法返回值
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
// 获取@ComponentScan注解basePackageClasses()方法返回值
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
// 如果未指定@ComponentScan注解的basePackages()以及basePackageClasses(),那么basePackages将为空
if (basePackages.isEmpty()) {
// 获取配置类所在包并添加到basePackages集合中。
basePackages.add(ClassUtils.getPackageName(declaringClass));
}
scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
@Override
protected boolean matchClassName(String className) {
return declaringClass.equals(className);
}
});
return scanner.doScan(StringUtils.toStringArray(basePackages));
}
ClassUtils的getPackageName方法实现也很简单,就是通过获取最后一个“.”的下标,然后从类的全限定名截取获得包名。
// ClassUtils#getPackageName(java.lang.String)
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) : "");
}
接下来,我们就DEBUG验证下猜想,不出所料,結果正如我们所猜想的那样。
Spring Boot中只添加一个@SpringBootApplication注解就可以扫描启动所在包及其子包中所有添加了@Component注解及其派生注解的Bean这样的功能归根结底还是Spring Framework原有的功能,并不是Spring Boot所独创。
Spring在处理@ComponentScan注解时,如果未指定包路径或者class路径,默认以配置类所在的包路径作为资源扫描路径。