Spring component-scan 原理解析

本文根据 《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 方法,它做了几件事情:

数据结构:使用名为beanDefinitionsSet<BeanDefinitionHolder>存放扫描到的 Component 组件。

算法:

  1. 对 base-packages 中的每个 package 进行逐一扫描

  2. 将每个 package 扫描到的结果放到另外一个 Set 容器中

    Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
    
  3. 对容器里的内容进行一些处理

我们主要看扫描的过程,因此只看 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;
    }
}

这个方法有一点点长,我们慢慢分析。

数据结构:

  1. 使用名为candidatesSet<BeanDefinition>存放扫描到的 Component 组件。

  2. 使用Resource[] resources保存包扫描路径(packageSearchPath)下的 Resouce 。这里有两个问题:什么是包扫描路径?什么是Resouce?稍后解答。

  3. 使用MetadataReader接口保存单个 resource(resource是.class文件,都是 java 的类)下的元信息。

  4. 使用ScannedGenericBeanDefinition类保存扫描到的bean。

这里对上述数据结构重要且不太清楚的地方做一些说明:

  1. 包扫描路径,是将包的路径转变成 .class 文件的路径。我们的 basePackage 现在是 thinking.in.spring.boot.samples.spring3,那么packageSearchPath 就是 classpath*:/thinking.in.spring.boot.samples.spring3/**.class。这里, classpath*:叫做CLASSPATH_ALL_URL_PREFIX,查找的类路径下的全部符合条件的文件,如果是classpath:就只查找一个符合条件的文件。如果查到了多个还要报错。

  2. Resource的全限定类名是org.springframework.core.io.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 资源。

  3. MetadataReader接口能够获取的元信息包括三个内容:资源Resouce、关于类的元信息ClassMetadata,关于注解的元信息AnnotationMetadata

算法:

  1. 遍历每个资源文件 resource 得到 MetaDataReader 接口
  2. 若 isCandidateComponent(metadataReader) 为 true
  3. 若 isCandidateComponent(sbd) 为 true
  4. 将对应的 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>的工作原理:

  1. 根据 XML Schema 和 可扩展的XML 确定<context:component-scan>映射的方法为ComponentScanBeanDefinitionParser类中的parse方法。
  2. 通过ClassPathBeanDefinitionScanner以及其父类ClassPathScanningCandidateComponentProvider扫描需要注册到 spring application context (spring 应用上下文) 中的 bean,以set集合的方式交给 parser 。判断 Component 组件的过程,由MetadataReaderincludeFiltersexcludeFilters配合完成。
  3. ComponentScanBeanDefinitionParser类接着完成注册过程。
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值