Spring 系列(四):我們來聊聊<context:component-scan/>

七分熟pizza發表於2019-04-25

1.背景

上篇最後給大家了一個建議,建議配置bean掃描包時使用如下寫法:

spring-mvc.xml

<!-- 只掃描@Controller註解 -->
<context:component-scan base-package="com.xxx.controller" use-default-filters="false"
 >
    <context:include-filter type="annotation"
        expression="org.springframework.stereotype.Controller" />
</context:component-scan>
複製程式碼

spring.xml

<!-- 配置掃描註解,不掃描@Controller註解 -->
<context:component-scan base-package="com.xxx">
    <context:exclude-filter type="annotation"
        expression="org.springframework.stereotype.Controller" />
</context:component-scan>
複製程式碼

文中提到通過以上配置,就可以在Spring MVC容器中只註冊有@Controller註解的bean,Spring容器註冊除了@Controller的其它bean。

有的同學留言問為什麼這樣寫就達到這種效果了呢?

也有人可能認為我是無腦從網上抄來的,我有什麼依據,憑什麼這麼說?經過ISO 9000認證了嗎?

為了維護文章的權威性以及我的臉面,本篇我就繼續帶大家從官網和原始碼兩方面進行分析。

2. < context:component-scan/>流程分析

2.1 Java註解

不是說好的講< context:component-scan>嗎,怎麼註解亂入了。

放心,雖然看原始碼累,寫讓大家看懂的文章更累,但是我還沒瘋。

為什麼講註解,因為Spring中很多地方用到註解,本文及前幾篇文章大家或多或少也都有看到。

因此在這裡加個小灶,和大家一起回顧一下註解的知識點。

先檢視官方文件

Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.
Annotations have a number of uses, among them:
*  Information for the compiler — Annotations can be used by the compiler to detect errors or suppress warnings.
*  Compile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth.
*  Runtime processing — Some annotations are available to be examined at runtime.
複製程式碼

上面一段話翻譯過來:

註解是原資料的一種形式,對標註的程式碼邏輯上沒有直接的影響,只是用來提供程式的一些資訊。
主要用處如下:
*  為編譯器提供資訊,比如錯誤檢測或者警告提示。
*  在編譯和部署期處理期,程式可以根據註解資訊生成程式碼、xml檔案。
*  在程式執行期用來做一些檢查。
複製程式碼

2.2 Java元註解

JAVA為了開發者能夠靈活定義自己的註解,因此在java.lang.annotation包中提供了4種元註解,用來註解其它註解。

檢視官方文件對這4種元註解的介紹:

    1. @Retention
@Retention annotation specifies how the marked annotation is stored:
*   RetentionPolicy.SOURCE – The marked annotation is retained only in the source level and is ignored by the compiler.
*   RetentionPolicy.CLASS – The marked annotation is retained by the compiler at compile time, but is ignored by the Java Virtual Machine (JVM).
*   RetentionPolicy.RUNTIME – The marked annotation is retained by the JVM so it can be used by the runtime environment.
複製程式碼

翻譯:指定標記的註解儲存範圍。可選範圍是原檔案、class檔案、執行期。

    1. @Documented
@Documented annotation indicates that whenever the specified annotation is used those elements should be documented using the Javadoc tool. (By default, annotations are not included in Javadoc.) For more information, see the Javadoc tools page.
複製程式碼

翻譯:因為註解預設是不會被JavaDoc工具處理的,因此@Documented用來要求註解能被JavaDoc工具處理並生成到API文件中 。

    1. @Target
@Target annotation marks another annotation to restrict what kind of Java elements the annotation can be applied to. A target annotation specifies one of the following element types as its value:
*   ElementType.ANNOTATION_TYPE can be applied to an annotation type.
*   ElementType.CONSTRUCTOR can be applied to a constructor.
*   ElementType.FIELD can be applied to a field or property.
*   ElementType.LOCAL_VARIABLE can be applied to a local variable.
*   ElementType.METHOD can be applied to a method-level annotation.
*   ElementType.PACKAGE can be applied to a package declaration.
*   ElementType.PARAMETER can be applied to the parameters of a method.
*   ElementType.TYPE can be applied to any element of a class.

複製程式碼

翻譯:用來標識註解的應用範圍。可選的範圍是註解、建構函式、類屬性、區域性變數、包、引數、類的任意元素。

    1. @Inherited
 @Inherited annotation indicates that the annotation type can be inherited from the super class. (This is not true by default.) When the user queries the annotation type and the class has no annotation for this type, the class' superclass is queried for the annotation type. This annotation applies only to class declarations.
複製程式碼

翻譯:預設情況下註解不會被子類繼承,被@Inherited標示的註解可以被子類繼承。

上面就是對4種元註解的介紹,其實大部分同學都知道,這裡只是一起做個回顧,接下來進入正體。

2.3 @Controller介紹

檢視官方文件

Indicates that an annotated class is a "Controller" (e.g. a web controller).
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.
複製程式碼

翻譯一下: @Controller註解用來標明一個類是Controller,使用該註解的類可以在掃描過程中被檢測到。通常@Controller和@RequestMapping註解一起使用來建立handler函式。

我們在來看看原始碼,在org.springframework.stereotype包下找到Controller類。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
   String value() default "";
}
複製程式碼

可以看到Controller宣告為註解型別,類上的@Target({ElementType.TYPE}) 註解表明@Controller可以用到任意元素上,@Retention(RetentionPolicy.RUNTIME)表明註解可以儲存到執行期,@Documented表明註解可以被生成到API文件裡。

除定義的幾個元註解外我們還看到有個@Component註解,這個註解是幹什麼的呢?

檢視官方文件

Indicates that an annotated class is a "component". Such classes are considered as candidates for auto-detection when using annotation-based configuration and classpath scanning.
複製程式碼

翻譯一下:被@Component註解標註的類代表該類為一個component,被標註的類可以在包掃描過程中被檢測到。

再看原始碼:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {
   String value() default "";
}
複製程式碼

可以看到@Component註解可以用在任意型別上,保留在執行期,能生成到API文件中。

再回到@Controller註解,正是因為@Controller被@Component標註,因此被@Controller標註的類也能在類掃描的過程中被發現並註冊。

另外Spring中還用@Service和@Repositor註解定義bean,@Service用來宣告service類,@Repository用來宣告DAO累。

其原始碼如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
   String value() default "";
}
複製程式碼
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
   String value() default "";
}
複製程式碼

2.4 原始碼剖析

鋪墊都結束了,現在開始重頭戲。

和< annotation-driven/>元素一樣, < component-scan/>也屬於自定義名稱空間,對應的解析器是ComponentScanBeanDefinitionParser。

自定義名稱空間的解析過程可以參考上篇,此處不再介紹。

我們進入CommponentScanBeanDefinitionParser類的parse()方法。

@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
    //此處 BASE_PACKAGE_ATTRIBUTE = "base-package";
    //1.獲取要掃描的包
    String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
    //此處CONFIG_LOCATION_DELIMITERS = ",; \t\n",
    //把,或者;分割符分割的包放到陣列裡面
   String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
         ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
    //2.建立掃描器
   ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
    //3.掃描包並註冊bean
   Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
   return null;
}
複製程式碼

上面掃描註冊過程可以分為3步。

(1)獲取要掃描的包。

(2)建立掃描器。

(3)掃描包並註冊bean。

第1步邏輯比較簡單,就是單純的讀取配置檔案的"base-package"屬性得到要掃描的包列表。

我們從第2步開始分析。

2.4.1 建立掃描器

進入configureScanner方法()。

protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) {
    //useDefaultFilters預設為true,即掃描所有型別bean
    boolean useDefaultFilters = true;
    //1.此處USE_DEFAULT_FILTERS_ATTRIBUTE = "use-default-filters",獲取其XML中設定的值
    if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) {
        useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE));
    }
   //2.建立掃描器
    ClassPathBeanDefinitionScanner scanner = createScanner(parserContext.getReaderContext(), useDefaultFilters);
    //3.解析過濾型別
    parseTypeFilters(element, scanner, parserContext);
    //4.返回掃描器
    return scanner;
}
複製程式碼

建立掃描器的方法分為4步。

(1)獲取掃描類範圍。

(2)根據掃描範圍初始化掃描器。

(3)設定掃描類的過濾器。

(4)返回建立的掃描器。

第1步也比較簡單,從配置檔案中獲得“use-default-filters”屬性的值,預設是true,即掃描所有型別的註解。

我們進入第2步的createScanner()方法,看看如何建立掃描器。

protected ClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext, boolean useDefaultFilters) {
    //新建一個掃描器
    return new ClassPathBeanDefinitionScanner(readerContext.getRegistry(), useDefaultFilters,
        readerContext.getEnvironment(),readerContext.getResourceLoader());
}
複製程式碼

沿呼叫棧進入ClassPathBeanDefinitionScanner()方法。

public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
//如果useDefaultFilters為true,註冊預設過濾器
    if (useDefaultFilters) {
        //註冊預設過濾器
        registerDefaultFilters();
   }
}
複製程式碼

進入registerDefaultFilters()方法。

protected void registerDefaultFilters() {
    this.includeFilters.add(new AnnotationTypeFilter(Component.class));
}
複製程式碼

可以看到上面方法把Component註解型別加入到了掃描白名單中,因此被@Component標註的類都會被掃描註冊。

在此,大家也明白為什麼@Controller、@service、@Repository標註的類會被註冊了吧,因為這些註解都用@Component標註了。

我們再進入第3步的parseTypeFilters()方法,看如何設定過濾器。

protected void parseTypeFilters(Element element, ClassPathBeanDefinitionScanner scanner, ParserContext parserContext) {
    //解析exclude-filter和include-filter元素
    //獲取元素所有子節點
    NodeList nodeList = element.getChildNodes();
    //遍歷元素子節點
    for (int i = 0; i < nodeList.getLength(); i++) {
        Node node = nodeList.item(i);
        if (node.getNodeType() == Node.ELEMENT_NODE) {
        String localName = parserContext.getDelegate().getLocalName(node);
        //解析include-filter元素 ,此處 INCLUDE_FILTER_ELEMENT = "include-filter"
        if (INCLUDE_FILTER_ELEMENT.equals(localName)) {
            //建立型別過濾器
            TypeFilter typeFilter = createTypeFilter((Element) node, classLoader, parserContext);
            //把解析出來的型別加入白名單
            scanner.addIncludeFilter(typeFilter);
        }
        //解析exclude-filter元素,此處EXCLUDE_FILTER_ELEMENT = "exclude-filter"
        else if (EXCLUDE_FILTER_ELEMENT.equals(localName)) {
            //建立型別過濾器
            TypeFilter typeFilter = createTypeFilter((Element) node, classLoader, parserContext);
            //把解析出來的型別加入黑名單
            scanner.addExcludeFilter(typeFilter);
        }
    }
}
複製程式碼

進入createTypeFilter()方法檢視實現邏輯。

protected TypeFilter createTypeFilter(Element element, ClassLoader classLoader, ParserContext parserContext) {
    //獲取xml中type屬性值,此處FILTER_TYPE_ATTRIBUTE = "type"   
    String filterType = element.getAttribute(FILTER_TYPE_ATTRIBUTE);
    //獲取xml中expression屬性值,此處FILTER_EXPRESSION_ATTRIBUTE = "expression",獲取xml中該屬性值
    String expression = element.getAttribute(FILTER_EXPRESSION_ATTRIBUTE);
    expression = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(expression);
    //如果是註解型別,建立註解型別過濾器,並把需要過濾的註解類設定進去
    if ("annotation".equals(filterType)) {
        return new AnnotationTypeFilter((Class<Annotation>) ClassUtils.forName(expression, classLoader));
    }
}
複製程式碼

上面就是建立掃描器的過程,主要是將XML檔案中設定的型別新增到白名單和黑名單中。

2.4.2 掃描註冊bean

得到掃描器後,開始掃描註冊流程。

進入doScan()方法。

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
    //遍歷所有需要掃描的包
    for (String basePackage : basePackages) {
        //1.在該包中找出用@Component註解的類,放到候選列表中
        Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
        for (BeanDefinition candidate : candidates) {
        //2.判斷容器中是否已經有bean資訊,如果沒有就註冊
        if (checkCandidate(beanName, candidate)) {
            //生成bean資訊
            BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
            //新增bean資訊到bean定義列表中
            beanDefinitions.add(definitionHolder);
            //3.把bean註冊到IOC容器中
            registerBeanDefinition(definitionHolder, this.registry);
        }
    }
}
複製程式碼

掃描註冊過程分為3步。

(1)從包中找出需要註冊的bean並放到候選列表中。

(2)遍歷候選列表中的所有bean,判斷容器中是否已經存在bean。

(3)如果不存在bean,就把bean資訊註冊到容器中。

接下來依次分析上面掃描註冊流程。

2.4.2.1 查詢候選bean

我們先看第1步,查詢候選bean的過程。進入findCandidateComponents()方法。

public Set<BeanDefinition> findCandidateComponents(String basePackage) {
    Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();
    //1.獲取包的classpath
    String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
        resolveBasePackage(basePackage) + '/' + this.resourcePattern;
    //2.把包下的所有class解析成resource資源   
    Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
    //遍歷所有類resource
    for (Resource resource : resources) {
        if (resource.isReadable()) {
            //3.獲取類的元資訊
            MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
            //4.判斷是否候選component
            if (isCandidateComponent(metadataReader)) {
                //5.根據類元資訊生成beanDefinition
                ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                sbd.setResource(resource);
                sbd.setSource(resource);
                //6.判斷該bean是否能例項化
                if (isCandidateComponent(sbd)) {
                    //7.加入候選類列表
                    candidates.add(sbd);
                 }
    //8.返回候選components選列表
    return candidates;
}
複製程式碼

查詢bean的流程比較繁瑣,可以分為以下8步。

(1)獲取包掃描路徑。

(2)把包路徑下的所有類解析成resource類。

(3)解析resource類,獲取類的元資訊。

(4)根據類元資訊判斷該類是否在白名單中。

(5)如果在白名單中,生成beanDefinition資訊。

(6)根據beanDefinition資訊判斷類是否能例項化。

(7)如果可以例項化,將beanDefinition資訊加入到候選列表中。

(8)返回儲存beanDefinition資訊的候選列表。

還記得BeanDefinition是什麼吧,主要是儲存bean的資訊。如果不記得看看Spring註冊流程

因為其它邏輯比較簡單,在此我們重點分析第4步和第6步。

先看第4步,進入isCandidateComponent()方法。

protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
    //1.遍歷黑名單,若傳入的類元資訊在黑名單中返回false
    for (TypeFilter tf : this.excludeFilters) {
        //判斷是否和傳入的類匹配
        if (tf.match(metadataReader, this.metadataReaderFactory)) {
            return false;
        }
    }
    //2.遍歷白名單,若傳入的類元資訊在白名單中返回true
    for (TypeFilter tf : this.includeFilters) {
        if (tf.match(metadataReader, this.metadataReaderFactory)) {
            //根據@Conditional註解判斷是否註冊bean,如果沒有@Conditional註解,返回true.
            return isConditionMatch(metadataReader);
        }
    }
    return false;
}
複製程式碼

可以看到上面主要邏輯是判斷該類是否在白名單或黑名單列表中,如果在白名單,則返回true,在黑名單返回false。黑、白名單的值就是建立掃描流程中通過parseTypeFilters()方法設定進去的。

再稍微提一下上面@Conditional註解,此註解是Spring 4中加入的,作用是根據設定的條件來判斷要不要註冊bean,如果沒有標註該註解,預設註冊。我們在這裡不展開細說,有興趣的同學可以自己查閱相關資料。

我們再看第6步,進入isCandidateComponent()方法。

protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
    //獲取元類資訊
    AnnotationMetadata metadata = beanDefinition.getMetadata();
    //判斷是否可以例項化
    return (metadata.isIndependent() && (metadata.isConcrete() ||
        (metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));
}
複製程式碼

可以看到上面是根據該類是不是介面、抽象類、巢狀類等資訊來判斷能否例項化的。

2.4.2.2 判斷bean是否已經註冊

候選bean列表資訊已經得到,再看看如何對列表中的bean做進一步判斷。

進入checkCandiates()方法。

protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) {
    if (!this.registry.containsBeanDefinition(beanName)) {
        return true;
   }
    return false;
}
複製程式碼

上面方法比較簡單,主要是檢視容器中是否已經有bean的定義資訊。

2.4.2.3 註冊bean

對bean資訊判斷完成後,如果bean有效,就開始註冊bean。

進入registerBeanDefinition()方法。

protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) {
    BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
}
複製程式碼

再進入registerBeanDefinition()方法。

public static void registerBeanDefinition(
    BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) {
    //得到beanname
    String beanName = definitionHolder.getBeanName();
    //註冊bean資訊
    registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
    //註冊bean的別名
    String[] aliases = definitionHolder.getAliases();
    if (aliases != null) {
    for (String alias : aliases) {
        registry.registerAlias(beanName, alias);
    }

}
複製程式碼

上面流程大家有沒有似曾相識,和Spring解析註冊流程文中註冊bean的邏輯一樣。

到此就完成了掃描註冊bean流程的分析。接下來就是bean的例項化等流程,大家可以參考Spring解析註冊流程一文。

3.小結

看完上面的分析,相信大家對< context:component-scan/>有了深入的瞭解。

現在回到開頭的那段程式碼。會不會有“誠不我欺也”的感覺。

最後,我再把那段程式碼貼出來,大家對著程式碼在腦海裡想象一下其解析流程,檢驗一下掌握程度。

如果有哪一步卡住了,建議再回頭看看我的文章,直至能在腦海中有一個完整的流程圖,甚至能想到對應的原始碼段。

如果能做到這樣,說明你真正理解了< context:component-scan/>,接下來就可以愉快的和小夥伴炫技或者和麵試官去侃大山了。

spring-mvc.xml

<!-- 只掃描@Controller註解 -->
<context:component-scan base-package="com.xxx.controller" use-default-filters="false"
 >
    <context:include-filter type="annotation"
        expression="org.springframework.stereotype.Controller" />
</context:component-scan>
複製程式碼

spring.xml

<!-- 配置掃描註解,不掃描@Controller註解 -->
<context:component-scan base-package="com.xxx">
    <context:exclude-filter type="annotation"
        expression="org.springframework.stereotype.Controller" />
</context:component-scan>
複製程式碼

本文完。

如果想獲得更多,歡迎關注公眾號:七分熟pizza

公眾號裡我會分享更多技術以及職場方面的經驗,大家有什麼問題也可以直接在公眾號向我提問交流。

Spring 系列(四):我們來聊聊<context:component-scan/>

相關文章