1. 程式人生 > >曹工說Spring Boot原始碼(11)-- context:component-scan,你真的會用嗎(這次來說說它的奇技淫巧)

曹工說Spring Boot原始碼(11)-- context:component-scan,你真的會用嗎(這次來說說它的奇技淫巧)

寫在前面的話

相關背景及資源:

曹工說Spring Boot原始碼(1)-- Bean Definition到底是什麼,附spring思維導圖分享

曹工說Spring Boot原始碼(2)-- Bean Definition到底是什麼,咱們對著介面,逐個方法講解

曹工說Spring Boot原始碼(3)-- 手動註冊Bean Definition不比遊戲好玩嗎,我們來試一下

曹工說Spring Boot原始碼(4)-- 我是怎麼自定義ApplicationContext,從json檔案讀取bean definition的?

曹工說Spring Boot原始碼(5)-- 怎麼從properties檔案讀取bean

曹工說Spring Boot原始碼(6)-- Spring怎麼從xml檔案裡解析bean的

曹工說Spring Boot原始碼(7)-- Spring解析xml檔案,到底從中得到了什麼(上)

曹工說Spring Boot原始碼(8)-- Spring解析xml檔案,到底從中得到了什麼(util名稱空間)

曹工說Spring Boot原始碼(9)-- Spring解析xml檔案,到底從中得到了什麼(context名稱空間上)

曹工說Spring Boot原始碼(10)-- Spring解析xml檔案,到底從中得到了什麼(context:annotation-config 解析)

工程程式碼地址 思維導圖地址

工程結構圖:

概要

本篇已經是spring原始碼第11篇,最近都在講解:spring解析xml檔案,到底獲得了什麼?獲得了什麼呢,感興趣的可以挑選感興趣的看;目前呢,已經講到了context名稱空間,接下來準備講解component-scan,但是吧,這個真的是一個重量級的嘉賓,且不說原理,光是用法,就夠我們感受感受啥叫主角了。

常規用法

我們在package:org.springframework.contextnamespace.componentscantest下存放了以下幾個檔案:

MainClassForTestComponentScan.java 測試類,包含main方法,不是bean

PersonTestController.java 使用了@Controller註解,裡面使用@Autowired自動注入了PersonService

PersonService.java 使用了@Service註解

下邊看下程式碼:

//定義一個bean
package org.springframework.contextnamespace.componentscantest;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Slf4j
@Data
@Controller
public class PersonTestController {

    @Autowired
    private PersonService personService;
}
// 再一個bean
package org.springframework.contextnamespace.componentscantest;

import org.springframework.stereotype.Service;

@Service
public class PersonService {
    private String personname;
}
//測試程式碼
package org.springframework.contextnamespace.componentscantest;

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.util.MyFastJson;

import java.util.List;
import java.util.Map;

@Slf4j
public class MainClassForTestComponentScan {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
                new String[]{"classpath:context-namespace-test-component-scan.xml"},false);
        context.refresh();


        List<BeanDefinition> list =
                context.getBeanFactory().getBeanDefinitionList();
        // 我自己的工具類,使用json輸出bean definition
        MyFastJson.printJsonStringForBeanDefinitionList(list);
        
        Object bean = context.getBean(PersonTestController.class);
        System.out.println("PersonController bean:" + bean);

    }
}

xml檔案如下:

<?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:component-scan base-package="org.springframework.contextnamespace.componentscantest"/>
</beans>

輸出:

PersonController bean:PersonTestController(personService=org.springframework.contextnamespace.componentscantest.PersonService@3e11f9e9)

可以看到,注入成功。

我程式碼裡,其實還輸出了全部的beanDefinition,我簡單整理了一下,一共包含了如下幾個:

beanDefinition中的beanClass
org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
org.springframework.contextnamespace.componentscantest.PersonService 我們自己的業務bean
org.springframework.contextnamespace.componentscantest.PersonTestController 業務bean
org.springframework.context.annotation.ConfigurationClassPostProcessor
org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor

看來,一個簡單的註解,背後卻默默做了很多騷操作啊,除了自己的業務bean外,還有5個框架自帶的bean,型別呢,從命名可以看出,都是些什麼PostProcessor,有興趣的,可以翻到我前一篇,裡面講解了AutowiredAnnotationBeanPostProcessor

閱讀理解

我們從spring-context.xsd檔案可以找到這個元素的官方說明。

Scans the classpath for annotated components that will be auto-registered as
Spring beans. By default, the Spring-provided @Component, @Repository,
@Service, and @Controller stereotypes will be detected.


Note: This tag implies the effects of the 'annotation-config' tag, activating @Required,
@Autowired, @PostConstruct, @PreDestroy, @Resource, @PersistenceContext and @PersistenceUnit
annotations in the component classes, which is usually desired for autodetected components
(without external configuration). Turn off the 'annotation-config' attribute to deactivate
this default behavior, for example in order to use custom BeanPostProcessor definitions
for handling those annotations.


Note: You may use placeholders in package paths, but only resolved against system
properties (analogous to resource paths). A component scan results in new bean definition
being registered; Spring's PropertyPlaceholderConfigurer will apply to those bean
definitions just like to regular bean definitions, but it won't apply to the component
scan settings themselves.

See Javadoc for org.springframework.context.annotation.ComponentScan for information
on code-based alternatives to bootstrapping component-scanning.

我用我的425分壓線4級翻譯了一下:

掃描類路徑下的註解元件,它們將會被主動註冊為spring bean。預設情況下,可以識別以下註解:
@Component, @Repository,@Service, and @Controller。

注意:這個元素隱含了的作用,會預設啟用bean class類裡的@Required,
@Autowired, @PostConstruct, @PreDestroy, @Resource, @PersistenceContext and @PersistenceUnit 註解,這個功能也是一般預設需要的。將annotation-config屬性,設為false,可以關閉這項功能,比如想要自己定製處理這些註解的BeanPostProcessor時。

注意:你可以使用在包路徑裡,使用placeholder,但是隻能引用system property。 component scan會導致新的bean definition被註冊,Spring的PropertyPlaceholderConfigurer對這些bean,依然生效,但是,PropertyPlaceholderConfigurer 不能對 component scan生效。

如果要基於註解啟動component-scan,請檢視org.springframework.context.annotation.ComponentScan

這個只是元素本身的介紹,你知道,這個元素的屬性還是有辣麼多的,我們用一個表格,來看看其屬性的意思:

annotation-config屬性的作用

這個屬性的意思是,本來,component-scan不是預設包含了的功能嗎,所以才能夠識別並解析@Autowired等註解,那要是我們關了這個功能,再試試還能不能注入呢?

<context:component-scan 
        //這裡設為false,關閉@autowired等註解的解析功能
        annotation-config="false"
        base-package="org.springframework.contextnamespace.componentscantest"/>

再次測試,輸出如下:

PersonController bean:PersonTestController(personService=null)

可以發現,注入沒成功。

而且,這次,我的beanDefinition輸出語句顯示,一共只有兩個beanDefinition,就是我們定義的那兩個業務bean。

這麼看來,annotation-config的魔術手被我們斬斷了,當然,代價就是,不能自動注入了。

use-default-filters屬性的作用

本來這個屬性的作用吧,從字面上看是說:

Indicates whether automatic detection of classes annotated with @Component, @Repository, @Service,
or @Controller should be enabled. Default is "true".

即:是否自動檢測註解了@Component, @Repository, @Service,or @Controller 的類。

後面翻看了一下原始碼,更加明確了意義:

在component-scan這個元素的解析器裡(ComponentScanBeanDefinitionParser),有個屬性:

private static final String USE_DEFAULT_FILTERS_ATTRIBUTE = "use-default-filters";

關鍵程式碼如下:


    protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) {
        XmlReaderContext readerContext = parserContext.getReaderContext();

        boolean useDefaultFilters = true;
        if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) {
            useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE));
        }

        // 1.建立ClassPathBeanDefinitionScanner,下面的 2 3 4 等,代表一步一步跟程式碼的跳轉順序
        ClassPathBeanDefinitionScanner scanner = createScanner(readerContext, useDefaultFilters);
        ...
    }

    // 2.
    protected ClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext, boolean useDefaultFilters) {
        // 3 
        return new ClassPathBeanDefinitionScanner(readerContext.getRegistry(), useDefaultFilters);
    }
    // 3 
    public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) {
        this(registry, useDefaultFilters, getOrCreateEnvironment(registry));
    }

    // 4
public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters, Environment environment) {
         // 5
        super(useDefaultFilters, environment);

        Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
        this.registry = registry;

        // Determine ResourceLoader to use.
        if (this.registry instanceof ResourceLoader) {
            setResourceLoader((ResourceLoader) this.registry);
        }
    }
    
    // 第5處,進入以下邏輯
    public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters, Environment environment) {
         // 如果使用預設filter,則註冊預設filter
        if (useDefaultFilters) {
            registerDefaultFilters();
        }
        this.environment = environment;
    }

下邊就是核心了:


protected void registerDefaultFilters() {
        /**
         * 預設掃描Component註解
         */
        this.includeFilters.add(new AnnotationTypeFilter(Component.class));
        ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
        try {
            // 這裡可以看到,還支援 ManagedBean 註解
            this.includeFilters.add(new AnnotationTypeFilter(
                    ((Class<? extends Annotation>) cl.loadClass("javax.annotation.ManagedBean")), false));
            logger.info("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
        }
        try {
             // 還支援 javax.inject.Named 註解
            this.includeFilters.add(new AnnotationTypeFilter(
                    ((Class<? extends Annotation>) cl.loadClass("javax.inject.Named")), false));
            logger.info("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
        }
    }

所以,這個屬性的作用就是:假設指定的掃描包內有20個類,其中2個class註解了@component,則這兩個類才是真正被掃描的類,至於具體的解析,這個屬性就不關心了。

context:exclude-filter屬性的作用

為什麼不分析context:include-filter,因為假設某個類沒有註解@component,按理說,是不加入掃描範圍的;

如果我們的include-filter把這個類納入範圍,則還要自定義bean definition的解析邏輯才能將這個類變成bean。

我們這裡有個demo,其中TeacherController和TeacherService是註解了ShouldExclude的。

xml如下:

<context:component-scan
        use-default-filters="true"
        base-package="org.springframework.contextnamespace">
    // 我們這裡使用了annotation型別,要把包含了ShouldExclude註解的,全部排除
    <context:exclude-filter type="annotation" expression="org.springframework.contextnamespace.ShouldExclude"></context:exclude-filter>
    // 這裡使用regex型別,排除掉TestController
    <context:exclude-filter type="regex" expression="org.springframework.contextnamespace.TestController"></context:exclude-filter>
</context:component-scan>

所以,上面的xml,我們可以將3個bean全部排除。

context:include-filter屬性的作用

在前面,我們說,這個屬性不好測試,但我想到也許可以這樣測:

   <context:component-scan
           use-default-filters="false"
           base-package="org.springframework.contextnamespace">
       <context:include-filter
               type="annotation"
               expression="org.springframework.stereotype.Component"/>
       <context:exclude-filter type="regex" expression="org.springframework.contextnamespace.TestController"/>
   </context:component-scan>

use-default-filters 這裡設為false,排除掉預設的@component的include filter;

但是我們在下面,再通過include-filter來達到同樣效果。

<context:include-filter
                type="annotation"
                expression="org.springframework.stereotype.Component"/>

經過上述改造後,執行正常。

注:以上部分是前兩天寫的(程式碼要後邊上傳,在家裡電腦上),以下部分是公司電腦寫的,前面的程式碼在家裡,忘記提交了。所以demo會略微不一樣,不過不影響實驗。

前面我說不好測試,但我發現還是可以搞。我們將會單獨定義一個自定義註解:

package org.springframework.contextnamespace;

import org.springframework.stereotype.Component;

import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 大家注意,這裡的@component我注掉了
//@Component
public @interface DerivedComponent {

    /**
     * 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
     */
    String value() default "";

}

然後呢,下面這兩個類我是使用上面的註解來標註了的:

@DerivedComponent
public class PersonService {
    private String personname1;
}
@DerivedComponent
public class PersonTestController {

//    @Autowired
    @Resource
    private PersonService personService;
}

xml如下:

<context:component-scan
        use-default-filters="false"
        base-package="org.springframework.contextnamespace.componentscan">
    <context:include-filter type="annotation" expression="org.springframework.contextnamespace.DerivedComponent"/>
</context:component-scan>

有必要解釋下:

use-default-filters="false":為true時,會將註解了@component或者@controller等註解的class包含進候選bean;這裡設為false,就不會進行上述行為;

:這裡呢,型別為註解,註解類就是我們自定義的那個。

總體意思就是,掃描指定包下面的,帶有@DerivedComponent註解的類;忽略帶有@component等註解的類。

這樣設定,我們的測試程式會如何:

PersonController bean:PersonTestController(personService=org.springframework.contextnamespace.componentscan.PersonService@4f615685)

it works!沒想到,這樣都可以。

我們看看他們的bean definition:

{
        "abstract":false,
        "autowireCandidate":true,
        "autowireMode":0,
        "beanClassName":"org.springframework.contextnamespace.componentscan.PersonService",
        "constructorArgumentValues":{
            "argumentCount":0,
            "empty":true,
            "genericArgumentValues":[],
            "indexedArgumentValues":{}
        },
        "dependencyCheck":0,
        "enforceDestroyMethod":false,
        "enforceInitMethod":false,
        "lazyInit":false,
        "lenientConstructorResolution":true,
        "metadata":{
            "abstract":false,
             // 這裡可以看到,註解確實是DerivedComponent
            "annotationTypes":["org.springframework.contextnamespace.DerivedComponent"],
            "className":"org.springframework.contextnamespace.componentscan.PersonService",
            "concrete":true,
            "final":false,
            "independent":true,
            "interface":false,
            "interfaceNames":[],
            "memberClassNames":[],
            "superClassName":"java.lang.Object"
        },
        "methodOverrides":{
            "empty":true,
            "overrides":[]
        },
        "nonPublicAccessAllowed":true,
        "primary":false,
        "propertyValues":{
            "converted":false,
            "empty":true,
            "propertyValueList":[]
        },
        "prototype":false,
        "qualifiers":[],
        "resolvedAutowireMode":0,
        
        "resourceDescription":"file [F:\\work_java_projects\\spring-boot-first-version-learn\\all-demo-in-spring-learning\\spring-xml-demo\\target\\classes\\org\\springframework\\contextnamespace\\componentscan\\PersonService.class]",
        "role":0,
        "scope":"singleton",
        "singleton":true,
        
        "synthetic":false
    }

具體原理,下節具體分析,主要呢, 的解析程式碼,主要就是負責收集beandefinition,

而上面這種自定義註解收集的方式的缺點在於,不能像@component等註解那樣,有很多的屬性可以設定。我們的自定義註解,只能是使用預設的beanDefinition配置,比如預設單例,等等。當然,你也可以直接使用和@component一模一樣的屬性,不過那也沒啥必要了,對吧。

這部分的原始碼,我放在了:

https://gitee.com/ckl111/spring-boot-first-version-learn/tree/master/all-demo-in-spring-learning/spring-xml-demo/src/main/java/org/springframework/contextnamespace/componentscan

最後這個自定義註解的內容,小馬哥的spring boot程式設計思想裡也提到了,在161頁,我手邊沒有電子版本,所以抱歉了。

總結

component-scan,用了這麼些年,看來真的只是用,裡面的原理還是一知半解,經過上面的分析,我也自己系統梳理了一遍。大家看看有啥問題的,歡迎指出來,一起進步