文章目录
- <component-scan>概述
- 1. XML: Spring 配置文件
- 2. Parser: ComponentScanBeanDefinitionParser
- 3. Scanner: ClassPathBeanDefinitionScanner
- 4. Provider: ClassPathScanningCandidateComponentProvider
- 总结
本文根据 《Spring Boot 编程思想(核心篇)》第7章 走向注解驱动编程 的内容进行编写。
<component-scan>概述
<component-scan>
的作用是扫描指定路径下的spring组件,包括@Component、@Controller、@RestController、@Service、@Repository等,并将它们加入spring应用上下文中。
由于 Spring Framework 的 component-scan 标签需要写在XML配置文件中,所以我们先从Spring配置文件说起。
在此先说明,本篇文章使用 spring 版本: 3.0.0。
1. XML: Spring 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 激活注解驱动特性 -->
<context:annotation-config />
<!-- 找寻被@Component或者其派生 Annotation 标记的类(Class),将它们注册为 Spring Bean -->
<context:component-scan base-package="thinking.in.spring.boot.samples.spring3" />
</beans>
可以发现,component-scan是以<context:component-scan>
的形式出现的,前面的context是命名空间。根据XML Schema规范,元素前缀需要显式地关联命名空间。又由于Spring可扩展的XML编写机制,命名空间需要与某个具体的类 (xxxHandler) 相关联。这个类存在于classpath:/META-INF/spring-handlers文件中:
里面的内容不多:
http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
从这里可以看到,context
这个namespace关联了org.springframework.context.config.ContextNamespaceHandler这个Handler类。
于是我们看下ContextNamespaceHandler这个类:
public class ContextNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
// component-scan的parser在这里⬇
registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
}
}
在Spring应用上下文 (Application Context) 启动时,上面那个类的init方法会自动调用,于是注册了很多Parser。其中包括component-scan的parser:ComponentScanBeanDefinitionParser。
Parse是解析的意思。顾名思义,ComponentScanBeanDefinitionParser能够解析我们一开始写的Spring XML中的<component-scan>
元素。
接下来,我们查看ComponentScanBeanDefinitionParser这个Parser类。
2. Parser: ComponentScanBeanDefinitionParser
这个类里面的内容有点多,我们看核心方法:parse方法。在看具体内容前,最好先看一下方法参数和返回值,如果能知道这个方法是干什么的那就更好了。
public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser {
private static final String BASE_PACKAGE_ATTRIBUTE = "base-package";
...
public BeanDefinition parse(Element element, ParserContext parserContext) {
String[] basePackages = StringUtils.tokenizeToStringArray(element.getAttribute(BASE_PACKAGE_ATTRIBUTE),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
// Actually scan for bean definitions and register them.
ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
registerComponents(parserContext.getReaderContext(), beanDefinitions, element);
return null;
}
...
}
这里的方法参数又两个: Element 和 parserContext ,element 指的应该是 xml 里面的元素,parserContext 这里好像看不出来,貌似是 spring 的应用上下文,不管它。
然后看该方法的内容:
首先,把 basePackage 读出来,做了一些处理。这里是把我们在 xml 文件component-scan
里,base-package 里的内容读出来并转换成 Spring 数组。转换成数组的原因是可能有多个 base-package 值。当然我们这里只写了一个,所以 basePackage 现在是一个长度为1的array,内容为[“thinking.in.spring.boot.samples.spring3”]。
然后,使用 scanner 把 basePackages 里面的所有 bean 扫描出来。最后,将这些 bean 注册到 parserContext 中。现在就比较确定,parserContext 是spring的应用上下文。
我们主要看 scanner 扫描的过程,所以注册的过程我们就不看了。Ctrl+鼠标单击 doScan 方法,来到 ClassPathBeanDefinitionScanner 这个 Scanner 类。
3. Scanner: ClassPathBeanDefinitionScanner
看的就是它:doScan。
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
for (String basePackage : basePackages) {
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
}
在看源码的时候不要只看方法的具体过程,这是我比较喜欢犯的“错误”。所谓面向对象,最小单元是”类“,也正是因为”类“,才有封装、继承和多态。
这个类的名字我感觉有点奇怪,它的父类叫做ClassPathScanningCandidateComponentProvider
,为啥是Provider?一个xxxScanner
的父类叫xxxProvider
?
回到 doScan 方法,它做了几件事情:
数据结构:使用名为beanDefinitions
的Set<BeanDefinitionHolder>
存放扫描到的 Component 组件。
算法:
- 对 base-packages 中的每个 package 进行逐一扫描
- 将每个 package 扫描到的结果放到另外一个 Set 容器中
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
- 对容器里的内容进行一些处理
我们主要看扫描的过程,因此只看 findCandidateComponents 方法。点击它,来到父类 ClassPathScanningCandidateComponentProvider 。
4. Provider: ClassPathScanningCandidateComponentProvider
public class ClassPathScanningCandidateComponentProvider implements ResourceLoaderAware {
private static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
...
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + "/" + this.resourcePattern;
Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
if (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
candidates.add(sbd);
}
else {
if (debugEnabled) {
logger.debug("Ignored because not a concrete top-level class: " + resource);
}
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate component class: " + resource, ex);
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
return candidates;
}
}
这个方法有一点点长,我们慢慢分析。
数据结构:
- 使用名为
candidates
的Set<BeanDefinition>
存放扫描到的 Component 组件。 - 使用
Resource[] resources
保存包扫描路径(packageSearchPath)下的 Resouce 。这里有两个问题:什么是包扫描路径?什么是Resouce?稍后解答。 - 使用
MetadataReader
接口保存单个 resource(resource是.class文件,都是 java 的类)下的元信息。 - 使用
ScannedGenericBeanDefinition
类保存扫描到的bean。
这里对上述数据结构重要且不太清楚的地方做一些说明:
- 包扫描路径,是将包的路径转变成 .class 文件的路径。我们的 basePackage 现在是 thinking.in.spring.boot.samples.spring3,那么packageSearchPath 就是 classpath*:/thinking.in.spring.boot.samples.spring3/**.class。这里,
classpath*:
叫做CLASSPATH_ALL_URL_PREFIX,查找的类路径下的全部符合条件的文件,如果是classpath:
就只查找一个符合条件的文件。如果查到了多个还要报错。 Resource
的全限定类名是.Resource
,从 javadoc 得知,该接口类的作用:
Interface for a resource descriptor that abstracts from the actual type of underlying resource, such as a file or class path resource.
An InputStream can be opened for every resource if it exists in physical form, but a URL or File handle can just be returned for certain resources. The actual behavior is implementation-specific.
- 简而言之,它就是一个资源描述符的接口(Interface for a resource descriptor)。这里的资源就是转换得到的 .class 资源。
MetadataReader
接口能够获取的元信息包括三个内容:资源Resouce
、关于类的元信息ClassMetadata
,关于注解的元信息AnnotationMetadata
。
算法:
- 遍历每个资源文件 resource 得到 MetaDataReader 接口
- 若 isCandidateComponent(metadataReader) 为 true
- 若 isCandidateComponent(sbd) 为 true
- 将对应的 bean 放入返回的集合当中
这里主要是看第2步:isCandidateComponent(metadataReader)。很明显,它是本类ClassPathScanningCandidateComponentProvider
的方法:
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, this.metadataReaderFactory)) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, this.metadataReaderFactory)) {
return true;
}
}
return false;
}
把 metadataReader 传进来,要知道,MetadataReader 具有读取类的元信息的能力,包括原 .class 文件,类元信息,注解元信息。如果 metadataReader 一旦满足 excludeFilter 则返回 false ,如果 metadata 一旦满足 includeFilter 返回 true 。而这些 filter 又是什么呢?
查看构造方法,发现类初始化的时候:判断 useDefaultFilters 是否为true,若为 true ,执行 registerDefaultFilter 方法,而该方法把 Component 注解类放进去了。
public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters) {
if (useDefaultFilters) {
registerDefaultFilters();
}
}
...
protected void registerDefaultFilters() {
this.includeFilters.add(new AnnotationTypeFilter(Component.class));
...
}
于是我们可以得知,带有@component
注解的类在扫描后,成功判断它为 spring 的 bean 组件,随后由 Parser 注册到 spring 应用上下文中,与<context:component-scan base-package="...">
功能相符。
总结
<context:component-scan>
的工作原理:
- 根据 XML Schema 和 可扩展的XML 确定
<context:component-scan>
映射的方法为ComponentScanBeanDefinitionParser
类中的parse
方法。 - 通过
ClassPathBeanDefinitionScanner
以及其父类ClassPathScanningCandidateComponentProvider
扫描需要注册到 spring application context (spring 应用上下文) 中的 bean,以set集合的方式交给 parser 。判断 Component 组件的过程,由MetadataReader
和includeFilters
、excludeFilters
配合完成。 -
ComponentScanBeanDefinitionParser
类接着完成注册过程。