Spring源码系列(一):<context:component-scan />

1,036 阅读5分钟

Spring源码系列(一):<context:component-scan />

1. 基本使用

​ 我们在创建spring项目时会在xml配置文件中配置<context:component-scan base-package="com.test"/>后,spring就会自动扫描com.test包下的java类,如果java类被@Controller、@Service、@Repository、@Component注解修饰,那么就会把这些类注册进容器中。

2. 实现原理

2.1 @Controller、@Service、@Repository注解解析

​ 要想明白<context:component-scan base-package="com.test"/>的实现原理,首先要明白这几个注解是干嘛的。以@Controller为例。

  1. 先来看看官方的解释

    This annotation serves as a specialization of @Component, allowing for implementation classes to be autodetected through classpath scanning. It is typically used in combination with annotated handler methods based on the RequestMapping annotation.
    
    翻译:
    此注释用作@Component的专用化,允许通过类路径扫描自动检测实现类。它通常与基于 RequestMapping 注释的带批注的处理程序方法结合使用。
    
  2. @Controller是@Component注解的一个专用化,我们点开@Controller注解(windows下 ctrl + 鼠标左键进入该注解)。会发现@Controller注解里面套了一个@Component注解。实际上@Controller、@Service、@Repository、@Component注解,打开以后都是@Component。我们再扫描过程中,也是扫描该类是不是被@Component注解修饰,如果被修饰则进入到容器中。

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    public @interface Controller {
    
    	/**
    	 * The value may indicate a suggestion for a logical component name,
    	 * to be turned into a Spring bean in case of an autodetected component.
    	 * @return the suggested component name, if any (or empty String otherwise)
    	 */
    	@AliasFor(annotation = Component.class)
    	String value() default "";
    
    }
    

    2.2 源码剖析

    xml文件中的默认标签和自定义标签

    在spring解析xml文件的过程中,默认的标签有四个:import、alias、bean、beans,例如我们自定义一个bean时常写的。这些xml中标签都会调用parseDefaultElement()方法来解析。

    	<bean id="person" class="com.test.entity.Person">
    		<property name="id" value="1"/>
    		<property name="name" value="zhangsan"/>
      	</bean>
    

    其余的spring自带的标签,例如<context:component-scan />、< annotation-driven/>都是属于自定义命名空间。解析它们的时候会调用parseCustomElement()方法。所以这里有一个spring的拓展点:自定义标签解析。

    标签有了,我们需要一个类解析它。spring中解析<context:component-scan />的类是ComponentScanBeanDefinitionParser。我把主要的代码放到下面。

    目录结构

    我们有两个包:com.ming.shouldSkip, com.ming.config,下面主要以config包为例子。

    config包的类:

    • PersonService
    • PersonService2
    /**
     * 被@Service注解修饰,可被识别到
     */
    @Service
    public class PersonService {
    }
    
    /**
     * 没有被@Service注解(@Component)修饰,不可被识别到
     */
    @Service
    public class PersonService2 {
    }
    
    
    进入源码

    当我们进入一个类,不知道该类是干什么用的时候,先看注解,比如当前类,我们通过注解可以了解到。这个类是__解析<context:component-scan />元素的__。

    applicationContext.xml中的配置:
        <context:component-scan base-package="com.ming.shouldSkip, com.ming.config"/>
            
    Spring源码:  
    /**
     * Parser for the {@code <context:component-scan/>} element.
     * @since 2.5
     */
    public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser {
        public BeanDefinition parse(Element element, ParserContext parserContext) {
            // 获取<context:component-scan />节点的base-package属性值
            // 根据我们的配置,basePackage的值为 com.ming.shouldSKip,com.ming.config
            String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
            // 解析占位符
            basePackage = parserContext.getReaderContext().getEnvironment().
                			.resolvePlaceholders(basePackage);
            // 解析base-package(允许通过,;\t\n中的任一符号填写多个)
            // 因为我们写了两个包路径,所有这里的basePackages的size为2。
            String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
    				ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
            // 构建和配置ClassPathBeanDefinitionScanner
    		ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
    		// 使用scanner在执行的basePackages包中执行扫描,返回已注册的bean定义
            // 重点,doScan方法是扫描包下的java类有没有被@Component注解修饰的。我们ctrl + 鼠标左键,进入。
    		Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
    		// 组件注册(包括注册一些内部的注解后置处理器,触发注册事件)
    		registerComponents(parserContext.getReaderContext(), beanDefinitions, element);
            
    		return null;
        }
    }
    
    doScan方法

    前置知识:What is BeanDefiniton?

    ​ spring初始化容器时,会加载配置文件(applicationContext.xml),然后把配置文件转化成一个document文档对象,然后解析里面的标签(自定义标签,默认标签)。然后生成一个BeanDefinition对象来保存bean的信息,被注解修饰的类比如@Component,也会先转换成一个BeanDefinition对象。

    之后spring在根据BeanDefinition的信息来生成这个bean,并放入到容器中,提供给我们使用。

    这里比较复杂,我们只看加注释的代码即可。

    /**
        * Perform a scan within the specified base packages,
        * returning the registered bean definitions.
        * 翻译:在指定的基包内执行扫描,返回已注册的 Bean 定义。
    */
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        // 创建一个集合,用来储存bean定义信息。
        Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
        // 遍历basePackages
        for (String basePackage : basePackages) {
            // 扫描basePackage,将符合要求的bean定义全部找出来
            // findCandidateComponents()方法是检查这个包下的类,是不是有被@Component修饰的类。下面会看源码
            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
            // 遍历所有候选的bean定义
            for (BeanDefinition candidate : candidates) {
                ......
                    // 注册beanDefinition
                    registerBeanDefinition(definitionHolder, this.registry);
            }
        }
        
        // 返回beanDefinitions集合
        return beanDefinitions;               
    }
    
    findCandidateComponents()方法

    主要逻辑是里面的scanCandidateComponents(basePackage)方法,我们来看它的源码。

    private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
        Set<BeanDefinition> candidates = new LinkedHashSet<>();
        // 1. 获取包的classpath路径,因为我们的上一步是循环包路径,所有得一个个的来。
        // 这里先处理com.ming.config,并以这个为例子。
        // 经过拼接等操作,packageSearchPath的值为 classpath*:com/ming/config/**/*.class
        // 这里的com/ming/config/**/*.class,意思是config包下的类以后其中的内部类。都会被扫描到。
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
            resolveBasePackage(basePackage) + '/' + this.resourcePattern;
        // 2. 把包下的所有class解析成resource资源
        Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
        // 3. 遍历所有resource
        for (Resource resource : resources) {
            // 4. 获取类的元信息
            MetadataReader metadataReader = getMetadataReaderFactory()
                .getMetadataReader(resource);
            // 5. 判断是否是候选component
            // 核心步骤,我们已经获取到类的元信息了,判断是否@Component注解修饰
            // 也就是isCandidateComponent()方法,如果为真,则说明这个类需要被注入到容器中
            if (isCandidateComponent(metadataReader)) {
                ScannedGenericBeanDefinition sbd = new
                    ScannedGenericBeanDefinition(metadataReader);
                sbd.setSource(resource);
                // 6. 判断该bean是否能实例化
                if (isCandidateComponent(sbd)) {
                    // 7. 加入候选类列表
                    candidates.add(sbd);
                }
            }
        }
            
        // 8. 返回候选components列表,这些类会被注入到ApplicationContext容器中,供我们使用。
        return candidates;
    }
    
    执行结果

    com.ming.config包下的PersonService类因为加了@Service注解,所有最终会被加载到ApplicationContext容器中,我们可以这样使用:

    ApplicationContext context = new MyClassPathXmlApplicationContext( "applicationContext.xml");
    PersonService personService = (PersonService) context.getBean("personService");