Spring 中常用注解原理剖析

前言

Spring 框架核心組件之一是 IOC钞螟,IOC 則管理 Bean 的創(chuàng)建和 Bean 之間的依賴注入鳞滨,對于 Bean 的創(chuàng)建可以通過在 XML 里面使用 <bean/> 標(biāo)簽來配置拯啦,對于 Bean 之間的依賴可以使用構(gòu)造方法注入褒链、Set 方法注入在 XML 里面配置甫匹。但是使用這種方式會使 XML 變的比較臃腫龐大兵迅,并且還需要開發(fā)人員一個個的在 XML 里面配置 Bean 之間的依賴恍箭,這簡直是一個災(zāi)難季惯,還好 Spring 框架給我們提供了一系列的注解讓開發(fā)人員從這個災(zāi)難中解脫出來勉抓,比如在一個類的成員變量上標(biāo)注了一個簡單的 @Autowired 注解就可以實(shí)現(xiàn)了 Bean 之間的自動依賴注入藕筋,在一個類上標(biāo)注了一個簡單的 @Component 注解就可以讓一個 Bean 注入到 Spring 容器……而 Spring 框架是如何通過注解簡化我們的工作量隐圾,實(shí)現(xiàn)這些功能的暇藏。本 Chat 就來解開這神秘的面紗盐碱。

本 Chat 主要內(nèi)容如下:

  • 我們經(jīng)常使用 @Autowired瓮顽,進(jìn)行依賴注入暖混,那么為何能夠直接使用拣播?它又是如何工作的贮配?@Required 又是如何起到檢查 XML 里面屬性有沒有被配置呢牧嫉?
  • Spring 框架是如何把標(biāo)注 @Component 的 Bean 注入到容器酣藻?
  • 我們經(jīng)常使用的 @Configuration辽剧,@ComponentScan偷崩,@Import阐斜,@Bean 注解又是如何工作的谒出?
  • 我們經(jīng)常使用 @PropertySource 引入配置文件笤喳,那么配置文件里面的配置是如何被注冊到 Spring 環(huán)境里面的杀狡?
  • 最后講解如何通過自定義注解實(shí)現(xiàn)一個簡單的樹形文檔生成呜象。

注解 @Autowired董朝、@Required 的工作原理

注解 Autowired 的簡單使用

既然要研究多個 Bean 之間的依賴注入子姜,那么就先創(chuàng)建兩個 Bean哥捕,分別為 ServiceA 和 ServiceB。

ServiceA 代碼如下:

package com.jiaduo.test;

public class ServiceA {
    public String getServiceName() {
        return serviceName;
    }
    public void setServiceName(String serviceName) {
        this.serviceName = serviceName;
    }
    private String serviceName;
    public void sayHello() {
        System.out.println("serviceA sayHello " + serviceName);
    }
}

ServiceB凫佛,代碼如下:

package com.jiaduo.test;

import org.springframework.beans.factory.annotation.Autowired;

public class ServiceB {

    @Autowired
    private ServiceA serviceA;

    public ServiceA getServiceA() {
        return serviceA;
    }
    public void setServiceA(ServiceA serviceA) {
        this.serviceA = serviceA;
    }

    public void sayHello() {
        serviceA.sayHello();
    }
}

如上代碼可知 ServiceB 內(nèi)部使用注解 @Autowired 注入了 ServiceA 的實(shí)例愧薛。

然后使用下面 bean-test.xml 文件配置兩個 Bean 的實(shí)例:

<?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"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.0.xsd
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <bean id="serviceA" class="com.jiaduo.test.ServiceA"></bean>
    <bean id="serviceB" class="com.jiaduo.test.ServiceB"></bean>

</beans>

最后測試類代碼如下:

public class TestAuowired {

    public static void main(String[] arg) {

        ClassPathXmlApplicationContext cpxa = new ClassPathXmlApplicationContext("bean-test.xml");
        cpxa.getBean("serviceB", ServiceB.class).sayHello();
    }
}

運(yùn)行測試代碼,我們期望輸出 serviceA sayHello null瞄勾,但是結(jié)果卻是如下:

log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
Exception in thread "main" java.lang.NullPointerException
    at com.jiaduo.test.ServiceB.sayHello(ServiceB.java:11)
    at com.jiaduo.test.TestAuowired.main(TestAuowired.java:14)

從異常結(jié)果知道进陡,ServiceB 里面的 serviceA 對象為 null换况,也就是 XML 里面配置的 serviceA 對象并沒有被注入到 ServiceB 實(shí)例內(nèi)戈二。其實(shí)要想使用 @Autowired 需要顯示的注冊對應(yīng)的注解的處理器到 Spring 容器觉吭,具體是需要在 bean-test.xml 里面添加 <bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor" />鲜滩,添加后在運(yùn)行代碼就會輸出 serviceA sayHello null 了徙硅。

需要注意的是 @Autowired 除了可以標(biāo)注在變量上嗓蘑,還可以標(biāo)注在變量對應(yīng)的 set 訪問器上桩皿,比如下面代碼1和代碼2效果是一樣。

代碼1:

@Autowired
public void setServiceA(ServiceA serviceA) {
        this.serviceA = serviceA;
}

代碼2:

@Autowired
private ServiceA serviceA;

AutowiredAnnotationBeanPostProcessor 原理剖析

首先來了解下 AutowiredAnnotationBeanPostProcessor 的類圖結(jié)構(gòu)佛嬉,如下圖:


image.png

可知 AutowiredAnnotationBeanPostProcessor 直接或者間接實(shí)現(xiàn)了 Spring 框架的好多擴(kuò)展接口:

  • 實(shí)現(xiàn)了 BeanFactoryAware 接口巷燥,可以讓 AutowiredAnnotationBeanPostProcessor 獲取到當(dāng)前 Spring 應(yīng)用程序上下文管理的 BeanFactory,從而可以獲取到 BeanFactory 里面所有的 Bean钝腺。
  • 實(shí)現(xiàn)了 MergedBeanDefinitionPostProcessor 接口艳狐,可以讓 AutowiredAnnotationBeanPostProcessor 對 BeanFactory 里面的 Bean 在被實(shí)例化前對 Bean 定義進(jìn)行修改毫目。
  • 繼承了 InstantiationAwareBeanPostProcessorAdapter镀虐,可以讓 AutowiredAnnotationBeanPostProcessor 在 Bean 實(shí)例化后執(zhí)行屬性設(shè)置刮便。

有關(guān)更多 Spring 框架擴(kuò)展接口的知識可以參考該 Chat:Spring 框架常用擴(kuò)展接口揭秘

OK恨旱,下面看看這些擴(kuò)展接口在 AutowiredAnnotationBeanPostProcessor 中調(diào)用時機(jī)谆沃,以及在實(shí)現(xiàn)依賴注入時候充當(dāng)了什么作用唁影,AutowiredAnnotationBeanPostProcessor 的代碼執(zhí)行時序圖如下:


image.png
  • 代碼(1)Spring 框架會在創(chuàng)建 AutowiredAnnotationBeanPostProcessor 實(shí)例過程中調(diào)用 setBeanFactory 方法注入 Spring 應(yīng)用程序上下文管理的 BeanFactory 到 AutowiredAnnotationBeanPostProcessor 中夭咬,所以 AutowiredAnnotationBeanPostProcessor 就可以操作 BeanFactory 里面的所有的 Bean 了南用。
  • 代碼(2)在 Spring 中每個 Bean 實(shí)例化前裹虫,Spring 框架都會調(diào)用 AutowiredAnnotationBeanPostProcessor 的 postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) 方法筑公,用來對當(dāng)前 Bean 的定義(beanDefinition)進(jìn)行修改匣屡,這里主要通過 findAutowiringMetadata 方法找到當(dāng)前 Bean 中標(biāo)注 @Autowired 注解的屬性變量和方法誉结。
private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
    //根據(jù)當(dāng)前bean信息生成緩存key
    String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
    //緩存中是否存在當(dāng)前bean的元數(shù)據(jù)
    InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
    if (InjectionMetadata.needsRefresh(metadata, clazz)) {
        synchronized (this.injectionMetadataCache) {
            metadata = this.injectionMetadataCache.get(cacheKey);
            if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                if (metadata != null) {
                    metadata.clear(pvs);
                }
                //不存在則收集惩坑,并放入緩存
                metadata = buildAutowiringMetadata(clazz);
                this.injectionMetadataCache.put(cacheKey, metadata);
            }
        }
    }
    return metadata;
}

這里是先通過 buildAutowiringMetadata 收集當(dāng)前 Bean 中的注解信息趾痘,其中會先查找當(dāng)前類里面的注解信息扼脐,對應(yīng)在變量上標(biāo)注 @Autowired 的變量會創(chuàng)建一個 AutowiredFieldElement 實(shí)例用來記錄注解信息瓦侮,對應(yīng)在 set 方法上標(biāo)注 @Autowired 的方法會創(chuàng)建一個 AutowiredMethodElement 對象來保存注解信息肚吏。然后會遞歸解析當(dāng)前類的直接父類里面的注解,并把最遠(yuǎn)父類到當(dāng)前類里面的注解信息依次存放到InjectionMetadata對象(內(nèi)部使用集合保存所有方法和屬性上的注解元素對象)斋泄,然后緩存起來以便后面使用炫掐,這里的緩存實(shí)際是個并發(fā) map:

private final Map<String, InjectionMetadata> injectionMetadataCache = new ConcurrentHashMap<>(256);
  • 代碼(11)則是在對Spring的BeanFactory里面的bean實(shí)例化后初始化前調(diào)用 AutowiredAnnotationBeanPostProcessor 的 PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) 方法設(shè)置依賴注入對象;首先代碼(12)獲取當(dāng)前 Bean 里面的依賴元數(shù)據(jù)信息痹束,由于在步驟(2)時候已經(jīng)收集到了緩存祷嘶,所以這里是直接從緩存獲取的论巍;這里獲取的就是步驟(2)緩存的 InjectionMetadata 對象环壤;步驟(13)則逐個調(diào)用 InjectionMetadata 內(nèi)部集合里面存放的屬性和方法注解對象的 inject 方法郑现,通過反射設(shè)置依賴的屬性值和反射調(diào)用 set 方法設(shè)置屬性值攒读。

如果注解加到了變量上則會調(diào)用 AutowiredFieldElement 的 inject 方法用來通過反射設(shè)置屬性值:

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {

 Field field = (Field) this.member;
    Object value;
    ...
    //解析依賴的bean
    value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
    ...
    //反射設(shè)置依賴屬性值
    if (value != null) {
        ReflectionUtils.makeAccessible(field);
        field.set(bean, value);
    }
}

如果注解加到了 set 方法上則調(diào)用 AutowiredMethodElement 的 inject 方法通過反射調(diào)用 set 方法設(shè)置依賴的變量值:

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    ...
    Method method = (Method) this.member;
    Object[] arguments;
    ...
    //解析依賴的bean
    value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
    ...
    //反射設(shè)置調(diào)用set方法設(shè)置屬性值
    if (arguments != null) {
        try {
            ReflectionUtils.makeAccessible(method);
            method.invoke(bean, arguments);
        }
        catch (InvocationTargetException ex){
            throw ex.getTargetException();
        }
    }
}

這里需要注意的是 @Autowired 注解有一個布爾變量的 required 屬性,用來決定在依賴注入時候是否檢測依賴的 Bean 在 BeanFactory 里面是否存在邓梅,默認(rèn)是 true日缨,就是如果不存在就會拋出下面異常:org.springframework.beans.factory.NoSuchBeanDefinitionException 異常,這個讀者可以把 bean-test.xml 里面的 serviceA 注入代碼去掉測試下哎壳。

如果 required 設(shè)置為 false毅待,則在依賴注入時候不去檢查依賴的 Bean 是否存在,而是在你具體使用依賴的 Bean 時候才會拋出 NPE 異常:

Exception in thread "main" java.lang.NullPointerException
    at com.jiaduo.test.ServiceB.sayHello(ServiceB.java:19)
    at com.jiaduo.test.TestAuowired.main(TestAuowired.java:14)

具體做檢驗(yàn)的地方就是上代碼的 resolveDependency 方法里面归榕。

注:@Autowired 的使用簡化了我們的開發(fā)尸红,其原理是使用 AutowiredAnnotationBeanPostProcessor 類來實(shí)現(xiàn),該類實(shí)現(xiàn)了 Spring 框架的一些擴(kuò)展接口驶乾,通過實(shí)現(xiàn) BeanFactoryAware 接口使其內(nèi)部持有了 BeanFactory(可輕松的獲取需要依賴的的 Bean)邑飒;通過實(shí)現(xiàn) MergedBeanDefinitionPostProcessor 擴(kuò)展接口循签,在 BeanFactory 里面的每個 Bean 實(shí)例化前獲取到每個 Bean 里面的 @Autowired 信息并緩存下來;通過實(shí)現(xiàn) Spring 框架的 postProcessPropertyValues 擴(kuò)展接口在 BeanFactory 里面的每個 Bean 實(shí)例后從緩存取出對應(yīng)的注解信息疙咸,獲取依賴對象县匠,并通過反射設(shè)置到 Bean 屬性里面。

注解 Required 的簡單使用

“注解Autowired的簡單使用”小節(jié)運(yùn)行后結(jié)果會輸出 serviceA sayHello null,其中 null 是因?yàn)闆]給 serviceA 里面的屬性 serviceName 賦值的原因乞旦,在開發(fā)時候開發(fā)人員也會比較容易犯這個錯誤贼穆,而要等運(yùn)行時使用該屬性的時候才知道沒有賦值。那么有沒有辦法在 Spring 框架進(jìn)行 Bean 創(chuàng)建時候就進(jìn)行檢查某些必要的屬性是否被設(shè)置了呢兰粉?

其實(shí) @Required 就是做這個的故痊,比如如果你想在 Spring 創(chuàng)建 ServiceA 時候就檢查 serviceName 有沒有被設(shè)置,你需要在 serviceName 的 set 方法上加入 @Required 注解:

@Required
    public void setServiceName(String serviceName) {
        this.serviceName = serviceName;
    }

并且需要在 XML 里面添加下面配置玖姑,它是 @Required 注解的處理器:

<bean class="org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor" />

加上這些后在運(yùn)行代碼會輸出下面結(jié)果:


image.png

可見 Spring 在設(shè)置 ServiceA 的實(shí)例的屬性時候會檢查該屬性是否被設(shè)置愕秫,如果沒有則會拋出異常。

通過在 bean-test.xml 里面添加屬性設(shè)置如下:

<bean id="serviceA" class="com.jiaduo.test.ServiceA">
        <property name="serviceName" value="Service Name" />
    </bean>

然后在運(yùn)行焰络,輸出結(jié)果如下:

serviceA sayHello Service Name

RequiredAnnotationBeanPostProcessor 原理剖析

RequiredAnnotationBeanPostProcessor 類似 AutowiredAnnotationBeanPostProcessor 也是間接或者直接實(shí)現(xiàn)了 Spring 框架相同的接口戴甩。通過實(shí)現(xiàn) BeanFactoryAware 接口內(nèi)部持有了 BeanFactory(可輕松的獲取需要依賴的Bean);通過實(shí)現(xiàn) Spring 框架的 postProcessPropertyValues 擴(kuò)展接口在 BeanFactory 里面的每個 Bean 實(shí)例后設(shè)置屬性前闪彼,檢查標(biāo)注 @Required 的 set 訪問器對應(yīng)的屬性是否被設(shè)置甜孤。

這個邏輯比較簡單,直接看下 RequiredAnnotationBeanPostProcessor 的 postProcessPropertyValues 方法:

public PropertyValues postProcessPropertyValues(
        PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {

    if (!this.validatedBeanNames.contains(beanName)) {
        if (!shouldSkip(this.beanFactory, beanName)) {
            List<String> invalidProperties = new ArrayList<>();
            for (PropertyDescriptor pd : pds) {
                //判斷屬性的set方法是否標(biāo)注了@Required注解畏腕,并且是否該屬性沒有被設(shè)置
                if (isRequiredProperty(pd) && !pvs.contains(pd.getName())) {
                    invalidProperties.add(pd.getName());
                }
            }
            //如果發(fā)現(xiàn)屬性的set方法標(biāo)注了@Required注解缴川,但是屬性沒有被設(shè)置,則拋出異常
            if (!invalidProperties.isEmpty()) {
                throw new BeanInitializationException(buildExceptionMessage(invalidProperties, beanName));
            }
        }
        this.validatedBeanNames.add(beanName);
    }
    return pvs;
}

其中 isRequiredProperty 作用是判斷當(dāng)前屬性的 set 方法是否標(biāo)注了 @Required 注解描馅,代碼如下:

protected boolean isRequiredProperty(PropertyDescriptor propertyDescriptor) {
        Method setter = propertyDescriptor.getWriteMethod();
        return (setter != null && AnnotationUtils.getAnnotation(setter, getRequiredAnnotationType()) != null);
    }

注:使用 @Autowired 和 @Required 時候需要注入對應(yīng)的注解處理器二跋,這很麻煩,所以 Spring 框架添加了一個 <context:annotation-config /> 標(biāo)簽流昏,當(dāng)你在 XML 里面引入這個標(biāo)簽后扎即,就默認(rèn)注入了 AutowiredAnnotationBeanPostProcessor 和 RequiredAnnotationBeanPostProcessor 。

注解 @Component 的工作原理

在第二節(jié)中况凉,我們通過在 bean-test.xml 里面配置 <bean id="serviceA" class="com.jiaduo.test.ServiceA"></bean> 的方式注入 ServiceA 的一個實(shí)例谚鄙;其實(shí)可以避免在 XML 里面使用 <bean/> 這種方式,可以直接在 ServiceA 類上標(biāo)注 @Component 注解方式:

@Component("serviceA")
public class ServiceA {
...
}

當(dāng)一個類上標(biāo)注 @Component 注解時候刁绒,Spring 框架會自動注冊該類的一個實(shí)例到 Spring 容器闷营,但是我們需要告訴 Spring 框架需要到去哪里查找標(biāo)注該注解的類,所以需要在 bean-test.xml 里面配置如下:

<context:component-scan base-package="com.jiaduo.test" />

其中 base-package 就是告訴 Spring 框架要去查找哪些包下的標(biāo)注 @Component 注解的類知市。

下面我們來研究下 Spring 框架是如何解析 <context:component-scan/> 標(biāo)簽并掃描標(biāo)注 @Component 注解的 Bean 注冊到 Spring 容器的傻盟。

首先看下解析 <context:component-scan/> 標(biāo)簽的 ComponentScanBeanDefinitionParser 類的時序圖:


image.png
  • 如上時序圖步驟(3)創(chuàng)建了一個 ClassPathBeanDefinitionScanner 掃描器,代碼如下
protected ClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext, boolean useDefaultFilters) {
        return new ClassPathBeanDefinitionScanner(readerContext.getRegistry(), useDefaultFilters,
                readerContext.getEnvironment(), readerContext.getResourceLoader());
    }

變量 useDefaultFilters 說明是否使用默認(rèn)的 filters嫂丙,所謂 filter 也就是過濾器娘赴,這里 ClassPathBeanDefinitionScanner 會掃描指定包路徑里面的類,但是那些需要的類跟啤,就是通過 filter 進(jìn)行過濾的诽表,默認(rèn) useDefaultFilters 為 true唉锌,<context:component-scan base-package="com.jiaduo.test"/> 等價于 <context:component-scan base-package="com.jiaduo.test" use-default-filters="true"/>,會使用下面代碼注冊默認(rèn) filters:

protected void registerDefaultFilters() {
    //這里決定指定包路徑下標(biāo)注@Component注解的類是我們想要的
    this.includeFilters.add(new AnnotationTypeFilter(Component.class));
    ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
    try {
        this.includeFilters.add(new AnnotationTypeFilter(
                ((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
    }
    catch (ClassNotFoundException ex) {
    ...
    }
    try {
        this.includeFilters.add(new AnnotationTypeFilter(
                ((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
    }
    catch (ClassNotFoundException ex) {
...    }
}
  • 步驟(4)解析用戶自定義的 BeanNameGenerator 的實(shí)現(xiàn)類竿奏,用來給掃描的類起一個在 BeanFactory 里面的名字袄简,配置如下:
<context:component-scan base-package="com.jiaduo.test"  name-generator="com.my.name.generator.MyBeanNameGenerator"/>

其中 name-generator 指定 BeanNameGenerator 的實(shí)現(xiàn)類的包路徑+類名,內(nèi)部會創(chuàng)建一個該類的實(shí)例泛啸。

  • 步驟(5)主要用來設(shè)置是否對掃描類進(jìn)行 scope-proxy绿语,我們知道在 XML 里面配置 Bean 的時候可以指定 scop 屬性來配置該 Bean 的作用域?yàn)?singleton、prototype候址、request汞舱、session等,對應(yīng)后三者來說宗雇,Spring 的實(shí)現(xiàn)是對標(biāo)注該作用域的 Bean 進(jìn)行代理來實(shí)現(xiàn)的昂芜,而我們知道 Spring 代理為 JDK 代理和 CGLIB 代理(可以參考 Chat:Spring 框架之 AOP 原理剖析),所以步驟(5)作用就是讓用戶通過 scoped-proxy 指定代理方式:<context:component-scan base-package="com.jiaduo.test" scoped-proxy="no"/>赔蒲,這是默認(rèn)方式不進(jìn)行代理泌神;scoped-proxy="interfaces" 標(biāo)示對接口進(jìn)行代理,也就是使用 JDK 動態(tài)代理舞虱;scoped-proxy="targetClass" 標(biāo)示對目標(biāo)對象進(jìn)行代理欢际,也就是使用 CGLIB 進(jìn)行代理。

  • 步驟(6)解析用戶自定義過濾器矾兜,前面我們說了损趋,默認(rèn)下 use-default-filters=true,默認(rèn)掃描之后只會注入標(biāo)注 @Component 的元素椅寺;這里則允許用戶自定義攔截器浑槽,設(shè)置需要注冊掃描到的那些類和排除掃描到的那些類,如下配置:

<context:component-scan base-package="com.jiaduo.test"
        use-default-filters="false">
        <context:include-filter type="annotation"
            expression="org.springframework.stereotype.Component" />

        <context:exclude-filter type="annotation"
            expression="org.springframework.stereotype.Controller" />
    </context:component-scan>

需要注意的是返帕,當(dāng)使用子標(biāo)簽 <context:include-filter/> 和 <context:exclude-filter/> 自定義用戶過濾器時候需要這樣設(shè)置:use-default-filters="false" 才會生效桐玻。

  • 代碼(7)則執(zhí)行具體掃描,其中 basePackages 是注解里面 base-package 解析后的包路徑列表荆萤,我們在指定base-package時候可以通過,; \t\n中其中一個分隔符指定多個包路徑镊靴,比如: <context:component-scan base-package="com.jiaduo.test;com.jiaduo.test1"/>。步驟(8)查找當(dāng)前包路徑下滿足過濾器列表的候選bean链韭,默認(rèn)是查找所有標(biāo)注了@Component注解的Bean偏竟。步驟(13)則注冊滿足條件的bean到Spring容器。

  • 步驟(14)注冊一些組元敞峭,比如步驟(15)默認(rèn)情況下會注冊的前面提到的 <context:annotation-config /> 標(biāo)簽實(shí)現(xiàn)的內(nèi)容踊谋,你可以通過下面方式關(guān)閉該功能:

<context:component-scan base-package="com.jiaduo.test" annotation-config="false" />

注:當(dāng)我們在 XML 里面配置 <context:component-scan/> 標(biāo)簽后,Spring 框架會根據(jù)標(biāo)簽內(nèi)指定的包路徑下查找指定過濾條件的 Bean儡陨,并可以根據(jù)標(biāo)簽內(nèi)配置的 BeanNameGenerator 生成 Bean 的名稱褪子,根據(jù)標(biāo)簽內(nèi)配置的 scope-proxy 屬性配置 Bean 被代理的方式,根據(jù)子標(biāo)簽 <context:include-filter/>,<context:exclude-filter/> 配置自定義過濾條件骗村。

注解 @Configuration嫌褪、@ComponentScan、@Import胚股、@PropertySource笼痛、@Bean工作原理

一個簡單 Demo

《Spring 框架常用擴(kuò)展接口揭秘》 Chat 中講到在 Spring 框架中,每個應(yīng)用程序上下文(ApplicationContext)管理著一個 BeanFactory琅拌,應(yīng)用程序上下文則是對 BeanFactory 和 Bean 的生命周期中的各個環(huán)節(jié)進(jìn)行管理缨伊。

而應(yīng)用程序上下文的子類除了有解析 XML 作為 Bean 來源的 ClassPathXmlApplicationContext,還有基于掃描注解類作為 Bean 來源的 AnnotationConfigApplicationContext进宝,本節(jié)就結(jié)合 AnnotationConfigApplicationContext 應(yīng)用程序上下文來講解 @Configuration刻坊、@ComponentScan、@Import党晋、@PropertySource谭胚、@Bean注解的使用與原理。

其中 ServiceA 和 ServiceB 代碼修改如下:

public class ServiceB {
    public ServiceA getServiceA() {
        return serviceA;
    }

    public void setServiceA(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
    @Autowired
    private ServiceA serviceA;

    public void sayHello() {
        serviceA.sayHello();
    }
}
public class ServiceA {

    public String getServiceName() {
        return serviceName;
    }
    public void setServiceName(String serviceName) {
        this.serviceName = serviceName;
    }
    //這里加了一個注解
    @Value("${service.name}")
    private String serviceName;

    public void sayHello() {
        System.out.println("serviceA sayHello " + serviceName);
    }
}

其中 ConfigBean 代碼如下:

package com.jiaduo.test.annotation.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.jiaduo.test.annotation.ServiceA;
@Configuration
public class ConfigBean {

    @Bean
    public ServiceA serviceA() {
        return new ServiceA();
    }
}

其中 ConfigBean2 代碼如下:

package com.jiaduo.test.annotation.sdk.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import com.jiaduo.test.annotation.ServiceA;
import com.jiaduo.test.annotation.ServiceB;

@Configuration
public class ConfigBean2 {

    @Bean
    public ServiceB serviceB() {
        return new ServiceB();
    }
}

配置文件 config.properties 內(nèi)容如下:

service.name=Annotation Learn

其中測試類 TestAnnotation 代碼如下:

@Configuration//(1)
@ComponentScan(basePackages = "com.jiaduo.test.annotation.config") // (2)
@Import(com.jiaduo.test.annotation.sdk.config.ConfigBean2.class) // (3)
@PropertySource(value={"classpath:config.properties"})//(4)
public class TestAnnotaion {

    public static void main(String[] args) {

        // (5)
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TestAnnotaion.class);
        ctx.getBean("serviceB", ServiceB.class).sayHello();// (6)
    }
}

運(yùn)行 TestAnnotaion 的 main 方代碼輸出:serviceA sayHello Annotation Learn未玻。

代碼(5)創(chuàng)建一個 AnnotationConfigApplicationContext 類型的應(yīng)用程序上下文灾而,構(gòu)造函數(shù)參數(shù)為 TestAnnotaion.class。

其內(nèi)部會解析 TestAnnotaion 類上的 ComponentScan 注解扳剿,并掃描 basePackages 指定的包里面的所有標(biāo)注 @Configuration 注解的類(這里會注入 ConfigBean 類到 Spring 容器)旁趟,然后解析 ConfigBean 內(nèi)部標(biāo)注有 @Bean 的方法,把方法內(nèi)創(chuàng)建的對象注入到 Spring 容器(這里是把 serviceA 注入到了 Spring 容器)庇绽。

然后會解析 TestAnnotaion 類上的 Import 注解锡搜,Import 注解作用是把標(biāo)注 @Configuration 注解里面創(chuàng)建的 Bean 注入到 Spring 容器,這里是把 ConfigBean2 里面創(chuàng)建的 serviceB 注入到了 Spring 容器瞧掺。

原理剖析

從 Demo 可知一切源于 AnnotationConfigApplicationContext余爆,那么就從 AnnotationConfigApplicationContext 的構(gòu)造函數(shù)開始,代碼如下:

public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
        //調(diào)用無參構(gòu)造函數(shù)
        this();
        //注冊含有注解的類
        register(annotatedClasses);
        //刷新應(yīng)用程序上下文
        refresh();
    }

其調(diào)用時序圖如下:


image.png
  • 如上時序圖步驟(1)調(diào)用了 AnnotationConfigApplicationContext 的無參構(gòu)造函數(shù)夸盟,其內(nèi)部創(chuàng)建了一個 AnnotatedBeanDefinitionReader 對象蛾方,該對象構(gòu)造函數(shù)內(nèi)部調(diào)用 AnnotationConfigUtil.registerAnnotationConfigProcessors 方法注冊了注解處理器(其作用等價于在 XML 里面配置 <context:annotation-config />),其中就注冊了 ConfigurationClassPostProcessor 處理器上陕,該處理器就是專門用來處理 @Configuration 注解的桩砰,這個后面再講。

  • 步驟(4)則是注冊 AnnotationConfigApplicationContext 構(gòu)造函數(shù)里面?zhèn)鬟f的含有注解的類到 Spring 容器(這里是注冊 TestAnnotaion 類到 Spring 容器)释簿。

  • 步驟(6)刷新應(yīng)用程序上下文亚隅,使用注冊的 ConfigurationClassPostProcessor 處理器解析 TestAnnotaion 上的注解,并注冊相應(yīng)的 Bean 到 Spring 容器庶溶。
    下面主要來看下 ConfigurationClassPostProcessor 的處理時序圖:


    image.png
  • ConfigurationClassPostProcessor 實(shí)現(xiàn)了 Spring 框架的 BeanDefinitionRegistryPostProcessor 接口所以具有 postProcessBeanDefinitionRegistry 方法 煮纵,具體可以參考 Chat:Spring 框架和 Tomcat 容器擴(kuò)展接口揭秘懂鸵。

  • 其中步驟(2)遍歷應(yīng)用程序上下文中的 Bean 查找標(biāo)注 @Configuration 的 Bean 定義,具體是使用 checkConfigurationClassCandidate 方法檢測行疏,代碼如下:

public static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
        ...
        //該類上是否標(biāo)注了@Configuration注解
        if (isFullConfigurationCandidate(metadata)) {
            beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
        }
        //該類上是否標(biāo)注了@Component匆光,@ComponentScan,@Import注解
        else if (isLiteConfigurationCandidate(metadata)) {
            beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
        }
        else {
            return false;
        }
        ...
        return true;
}

可知只有類上標(biāo)注 @Configuration酿联、@Component终息、@ComponentScan、@Import 注解的 Bean 才是候選 Bean贞让。

  • 步驟(4)創(chuàng)建了一個 ConfigurationClassParser 對象周崭,這個對象就是專門用來解析標(biāo)注 @Configuration 注解的 Bean 的。這里首先調(diào)用 parse 方法對步驟(2)產(chǎn)生的候選 Bean 進(jìn)行解析喳张,本文例子是先對 TestAnnotaion 類解析续镇,并會對 TestAnnotaion 上的 @ComponentScan 和 @Import 進(jìn)行解析,解析出來的 Bean 可能又含有了 @Configuration 注解销部,那么把這些新的包含 @Configuration 的 Bean 作為候選 Bean 后然后調(diào)用 parse 方法磨取,依次類推直到 parse 解析出來的 Bean 不在包含 @Configuration 注解。其中步驟(7)則是注冊解析到的標(biāo)注 @Import 的 Bean 和 @Bean 的 Bean 到 Spring 容器柴墩。

下面著重講解下 ConfigurationClassParser 的 parse 方法:


image.png
  • 其中最外層循環(huán)是遞歸解析 configuration 類和它的超類中標(biāo)注 @Configuration 的類忙厌,也就是解析完當(dāng)前類,會設(shè)置 sourceClass=sourceClass.getSuperClass()江咳;

  • 循環(huán)內(nèi)步驟(5)逢净、(6)是解析并處理所有標(biāo)注 @PropertySources 注解的Bean,具體代碼如下:

private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
        ...
        //獲取注解上的值
        String[] locations = propertySource.getStringArray("value");
        ...

        for (String location : locations) {
            try {
                String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
                Resource resource = this.resourceLoader.getResource(resolvedLocation);
                //設(shè)置location里面的屬性到Spring的環(huán)境environment
                addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
            }
            catch (IllegalArgumentException | FileNotFoundException | UnknownHostException ex) {
                ...
            }
        }
    }
private void addPropertySource(PropertySource<?> propertySource) {
        //獲取Spring環(huán)境environment里面的屬性集
        String name = propertySource.getName();
        MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();

        ...
        //添加注解@PropertySource里面的配置文件信息到Spring環(huán)境
        if (this.propertySourceNames.isEmpty()) {
            propertySources.addLast(propertySource);
        }
        else {
            String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1);
            propertySources.addBefore(firstProcessed, propertySource);
        }
        this.propertySourceNames.add(name);
    }
  • 循環(huán)內(nèi)步驟(7)歼指、(8)是解析標(biāo)注 @ComponentScan 的類爹土,并注冊掃描到的類到 Spring 容器,并且對掃描到含有 @Configuration 的類在進(jìn)行解析踩身,具體解析 @ComponentScan 的邏輯如下:
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
        //創(chuàng)建一個掃描器
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
                componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);

        //配置掃描器中bean的BeanNameGenerator
        Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
        boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
        scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
                BeanUtils.instantiateClass(generatorClass));

        //設(shè)置scopedProxy
        ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
        if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
            scanner.setScopedProxyMode(scopedProxyMode);
        }
        else {
            Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
            scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
        }
        //設(shè)置資源匹配模式
        scanner.setResourcePattern(componentScan.getString("resourcePattern"));

        //設(shè)置掃描器的過濾條件
        for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
            for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                scanner.addIncludeFilter(typeFilter);
            }
        }
        for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
            for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                scanner.addExcludeFilter(typeFilter);
            }
        }
        //是否延遲初始化
        boolean lazyInit = componentScan.getBoolean("lazyInit");
        if (lazyInit) {
            scanner.getBeanDefinitionDefaults().setLazyInit(true);
        }
        //解析掃描包路徑
        Set<String> basePackages = new LinkedHashSet<>();
        String[] basePackagesArray = componentScan.getStringArray("basePackages");
        for (String pkg : basePackagesArray) {
            String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
                    ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
            basePackages.addAll(Arrays.asList(tokenized));
        }
        for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
            basePackages.add(ClassUtils.getPackageName(clazz));
        }
        if (basePackages.isEmpty()) {
            basePackages.add(ClassUtils.getPackageName(declaringClass));
        }

        scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
            @Override
            protected boolean matchClassName(String className) {
                return declaringClass.equals(className);
            }
        });
        //執(zhí)行掃描
        return scanner.doScan(StringUtils.toStringArray(basePackages));
    }

其內(nèi)部邏輯與 <context:component-scan/> 相似胀茵,這里不再累述了。

  • 步驟(10)解析所有標(biāo)注 @Import 的 Bean挟阻,具體注入到 Spring 容器實(shí)際是在 ConfigurationClassPostProcessor 的時序圖的步驟(7)琼娘,其中掃描 @Import 注解的遞歸代碼如下:
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
    Set<SourceClass> imports = new LinkedHashSet<>();
    Set<SourceClass> visited = new LinkedHashSet<>();
    collectImports(sourceClass, imports, visited);
    return imports;
}

private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
        throws IOException {

    if (visited.add(sourceClass)) {
        for (SourceClass annotation : sourceClass.getAnnotations()) {
            String annName = annotation.getMetadata().getClassName();
            if (!annName.startsWith("java") && !annName.equals(Import.class.getName())) {
                collectImports(annotation, imports, visited);
            }
        }
        imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
    }
}
  • 步驟(11)解析所有標(biāo)注 @Bean 的方法,具體注入操作是在 ConfigurationClassPostProcessor 的時序圖的步驟(7)附鸽,解析 @Bean 的代碼如下:
private Set<MethodMetadata> retrieveBeanMethodMetadata(SourceClass sourceClass) {
        AnnotationMetadata original = sourceClass.getMetadata();
        //獲取所有標(biāo)注@Bean的方法元數(shù)據(jù)脱拼,這些方法返回順序是任意的
        Set<MethodMetadata> beanMethods = original.getAnnotatedMethods(Bean.class.getName());
        if (beanMethods.size() > 1 && original instanceof StandardAnnotationMetadata) {

            try {
                //使用asm讀取字節(jié)碼文件,并獲取標(biāo)注@Bean的方法到asmMethods坷备,返回的方法的順序和聲明的一樣
                AnnotationMetadata asm =
                        this.metadataReaderFactory.getMetadataReader(original.getClassName()).getAnnotationMetadata();
                Set<MethodMetadata> asmMethods = asm.getAnnotatedMethods(Bean.class.getName());
                if (asmMethods.size() >= beanMethods.size()) {
                    Set<MethodMetadata> selectedMethods = new LinkedHashSet<>(asmMethods.size());
                    for (MethodMetadata asmMethod : asmMethods) {
                        for (MethodMetadata beanMethod : beanMethods) {
                            if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) {
                                selectedMethods.add(beanMethod);
                                break;
                            }
                        }
                    }
                    if (selectedMethods.size() == beanMethods.size()) {
                        beanMethods = selectedMethods;
                    }
                }
            }
            catch (IOException ex) {
            }
        }
        return beanMethods;
    }

注: ConfigurationClassPostProcessor 處理器是 Spring 框架處理本節(jié)這些注解的關(guān)鍵類熄浓,本節(jié)內(nèi)容較為復(fù)雜,在解析注解使用運(yùn)用了大量的循環(huán)嵌套和遞歸算法省撑,代碼研究起來還是有一定難度的赌蔑,希望讀者結(jié)合時序圖慢慢理解俯在。

基于自定義注解實(shí)現(xiàn)樹形業(yè)務(wù)文檔生成

自定義注解

當(dāng)一個系統(tǒng)隨著不斷迭代的需求累加后,業(yè)務(wù)邏輯就會變得錯綜復(fù)雜娃惯,新人接手時候就會顯得很吃力跷乐;一個辦法是采用模塊化思想,每個模塊提供一個獨(dú)立業(yè)務(wù)功能石景,分清業(yè)務(wù)邊界劈猿,也就是使用領(lǐng)域模型拙吉;在領(lǐng)域模型中一個系統(tǒng)可以劃分為若干模塊潮孽,每個模塊可以對應(yīng)多個域,域與域之間通過對外暴露的唯一的 Service 進(jìn)行通信筷黔,每個域下面有可能對于多個子域往史。

本節(jié)就通過在每個域與子域提供的服務(wù)上添加自定義注釋,并收集這些注解來生成一個樹形的文檔來顯示整個系統(tǒng)里面都有哪些域服務(wù)佛舱。

根據(jù)上面介紹椎例,我們設(shè)計(jì)三類注解。

  • 在模塊類上面加的注解请祖。
@Target({ ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModuleAnnotation {
    String moduleName() default "";
    String moduleDesc() default "";
}

其中 moduleName 是模塊的名字要保證應(yīng)用唯一订歪,moduleDesc 是當(dāng)前模塊的描述增淹。

  • 域服務(wù)類或者方法上面添加的注解衔掸。
@Target({ ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DomainAnnotation {
    String moduleName() default "";
    String rootDomainName() default "";
    String rootDomainDesc() default "";
    String subDomainName() default "";
    String subDomainDesc() default "";
    String returnDesc() default "void";
}
  • 在域服務(wù)接口的參數(shù)上添加的注解祝闻,為了獲取參數(shù)名字和描述使用声滥。
@Target({ElementType.METHOD,})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Param {
    String paramName() default "";
    String paramType() default "";
    String paramDesc() default "";
}

其中 moduleName 說明當(dāng)前域服務(wù)屬于哪個模塊钦勘;rootDomainName 和 rootDomainDesc 是根域服務(wù)名稱和描述讥蟆;subDomainName马胧、subDomainDesc 為子域名稱和描述僵朗,多個中間用英文逗號分隔席纽;returnDesc 是返回值說明捏悬。

例如模塊注解加在類上:

@ModuleAnnotation(moduleName="trialing",moduleDesc="庭審模塊")
public class moduleclass{
}

域服務(wù)注解加在方法上,沒有子域時候:

@DomainAnnotation(moduleName="trialing",rootDomainName="seaDomain",rootDomainDesc="純語音庭審服務(wù)")
   public void m2New() {
  }

域服務(wù)加載方法上润梯,有子域時候:

@DomainAnnotation(moduleName="trialing",rootDomainName="videoDoamin",rootDomainDesc="視頻庭審服務(wù)",subDomainName="speechDoamin,speechVideoDoamin")
    public String hello(@Param(paramName="type",paramDesc="案件類型")String type,@Param(paramName="num",paramDesc="案件個數(shù)")String num){
    }

@DomainAnnotation(rootDomainName="speechDoamin",rootDomainDesc="語音識別服務(wù)")
    public String hello2(@Param(paramName="caseId",paramDesc="案號")Long caseId){
    }

@DomainAnnotation(rootDomainName="speechVideoDoamin",rootDomainDesc="視頻+語音識別服務(wù)")
    public String hello3(@Param(paramName="name",paramDesc="姓名")String name,@Param(paramName="address",paramDesc="地址")String address){
    }

如果我們能拿到所有類的注解信息过牙,然后根據(jù)模塊注解與域名注解的關(guān)聯(lián),就可以生成一個文檔纺铭,類似下圖:


image.png

如上圖抒和,樹根為 application 說明當(dāng)前是什么應(yīng)用,它下面有兩個 module 分別為證據(jù)模塊和庭審模塊彤蔽;在證據(jù)模塊下有兩個域服務(wù)分別為質(zhì)證服務(wù)和舉證服務(wù)摧莽,并且有具體服務(wù)的函數(shù)簽名;在庭審模塊下有兩個域服務(wù)分別為純語音庭審服務(wù)和視頻庭審服務(wù)顿痪;視頻庭審服務(wù)下面有兩個字域服務(wù)分泌為語音識別服務(wù)和視頻+語音識別服務(wù)镊辕,并且有對應(yīng)的服務(wù)的函數(shù)簽名油够。

自定義注解的收集

本文選擇實(shí)現(xiàn) Spring 框架的 InstantiationAwareBeanPostProcessor 擴(kuò)展接口來做自定義注解信息收集,該擴(kuò)展的接口如下:

public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
}

在 Spring 容器中每個 Bean 實(shí)例化前都會調(diào)用這個擴(kuò)展接口征懈,而該接口里面有 Bean 的 Class 對象石咬,所以可以方便獲取它方法和類上的注解信息。

有關(guān) Spring 框架擴(kuò)展接口可以參考 Chat:Spring 框架常用擴(kuò)展接口揭秘卖哎。

我們自定義 AnnotationInstantiationAwareBeanPostProcessor 類的 postProcessBeforeInstantiation 的實(shí)現(xiàn)為:

public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
        if (!isOpenAnnotation) {
            return null;
        }

        try {
            // 模塊類注解收集
            ModuleAnnotation moduleAnnotation = beanClass.getAnnotation(ModuleAnnotation.class);
            if (null != moduleAnnotation) {
                moudleAnnotationList.add(moduleAnnotation);

            }

            DomainAnnotation domainAnnotation = beanClass.getAnnotation(DomainAnnotation.class);

            // 域服務(wù)類注解收集
            if (null != domainAnnotation) {
                domainAnnotationList.add(domainAnnotation);

            }

            // 域服務(wù)方法注解收集
            for (Method method : beanClass.getDeclaredMethods()) {
                domainAnnotation = method.getAnnotation(DomainAnnotation.class);
                if (null != domainAnnotation) {
                    domainAnnotationList.add(domainAnnotation);

                    // 獲取參數(shù)類型鬼悠,名稱,函數(shù)簽名
                getMethodInfoFromParam(method,domainAnnotation);
                }

            }
        } catch (Exception e) {

            System.out.println("----------------error:" + e.getLocalizedMessage());
        }

        return null;
    }  
private void getMethodInfoFromParam(Method method,DomainAnnotation domainAnnotation) {
        //參數(shù)上的注解的獲取
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        //保存參數(shù)名稱
        String[] parameterNames = new String[parameterAnnotations.length];
        //參數(shù)類型
        Class<?>[] parameterTypes = method.getParameterTypes();
        int index = 0;
        //拼接函數(shù)簽名
        StringBuffer sb = new StringBuffer();
        String methodName = method.getName();
        String returnType = method.getReturnType().getName();
        sb.append(returnType).append(" ").append(methodName).append("(");

        Map<String, String> map = new HashMap<String, String>();
        //獲取方法的參數(shù)名字和參數(shù)描述保存到map
        for (Annotation[] parameterAnnotation : parameterAnnotations) {
            for (Annotation annotation : parameterAnnotation) {
                if (annotation instanceof Param) {
                    Param param = (Param) annotation;
                    String paramName = param.paramName();
                    String paramDesc = param.paramDesc();
                    sb.append(parameterTypes[index++].getName()).append(" ").append(paramName).append(",");

                    map.put(paramName, paramDesc);
                }
            }
        }

        AnnotationInfo annotationInfo = new AnnotationInfo();
        domainMethodMap.put(domainAnnotation, annotationInfo);

        String str = sb.toString();
        if (sb.toString().lastIndexOf(',') >= 0) {
            str = sb.substring(0, sb.length() - 1);
        }
        str = str + ")";

        annotationInfo.setMethodSign(str);
        annotationInfo.setParamsDesc(map);
}

注解信息的打印

上一小節(jié)收集到了所有的注解亏娜,本節(jié)就簡單介紹下如何打印出樹形文檔焕窝,代碼如下:

public ActionResult generateDocument(ErrorContext context) {
        ActionResult result = new ActionResult();
        //遞歸打印
        printTree();
        return result;
    }
private void printTree() {
    //獲取收集的注解信息
    List<DomainAnnotation> domainList = AnnotationInstantiationAwareBeanPostProcessor.getDomainAnnotationList();
    List<ModuleAnnotation> moudleList = AnnotationInstantiationAwareBeanPostProcessor.getMoudleAnnotationList();
    //打印模塊
    System.out.println("application:onlinecourt");
    for (ModuleAnnotation ma : moudleList) {

        String moudleName = ma.moduleName();
        String moudleDesc = ma.moduleDesc();
        StringBuffer sb = new StringBuffer();
        sb.append("-mouduleName:" + moudleName).append(",moudleDesc:" + moudleDesc);
        System.out.println(sb.toString());
        //打印模塊下的域服務(wù)
        for (DomainAnnotation da : domainList) {

            if (da.moduleName().equals(moudleName)) {
                // 打印當(dāng)前域服務(wù)
                printMethodInfo(da, 2, '-');
                // 打印子域服務(wù)
                generateSubDoamin(domainList, da.subDomainName(), 3);
            }
        }
        System.out.println();
    }
}

本節(jié)使用簡單的打印輸出樹形文檔,其實(shí)既然已經(jīng)獲得了注解信息维贺,我們可以根據(jù)需要比如生成 Markdown 文件它掂,PDF,或者直接把數(shù)據(jù)扔給前端溯泣,前端按照需要格式渲染都可以虐秋。

總結(jié)

本文講解了 Spring 框架中常用注解的原理實(shí)現(xiàn),希望讀者能參考本文對著源碼自己 Debug 跟入一下垃沦,以便加深理解客给;另外目前比較火的微服務(wù)框架 SpringBoot 中提倡使用注解,不再建議使用 XML 配置肢簿,如果你對本文能夠很好掌握靶剑,可以嘗試去研究下 SpringBoot 里面一些注解原理。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末译仗,一起剝皮案震驚了整個濱河市抬虽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纵菌,老刑警劉巖阐污,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異咱圆,居然都是意外死亡笛辟,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門序苏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來手幢,“玉大人,你說我怎么就攤上這事忱详∥Ю矗” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長监透。 經(jīng)常有香客問我桶错,道長,這世上最難降的妖魔是什么胀蛮? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任院刁,我火速辦了婚禮,結(jié)果婚禮上粪狼,老公的妹妹穿的比我還像新娘退腥。我一直安慰自己,他們只是感情好再榄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布狡刘。 她就那樣靜靜地躺著,像睡著了一般不跟。 火紅的嫁衣襯著肌膚如雪颓帝。 梳的紋絲不亂的頭發(fā)上米碰,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天窝革,我揣著相機(jī)與錄音,去河邊找鬼吕座。 笑死虐译,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的吴趴。 我是一名探鬼主播漆诽,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼锣枝!你這毒婦竟也來了厢拭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤撇叁,失蹤者是張志新(化名)和其女友劉穎供鸠,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體陨闹,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡楞捂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了趋厉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寨闹。...
    茶點(diǎn)故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖君账,靈堂內(nèi)的尸體忽然破棺而出繁堡,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布椭蹄,位于F島的核電站矮瘟,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏塑娇。R本人自食惡果不足惜澈侠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望埋酬。 院中可真熱鬧哨啃,春花似錦、人聲如沸写妥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽珍特。三九已至祝峻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扎筒,已是汗流浹背莱找。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嗜桌,地道東北人奥溺。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像骨宠,于是被迫代替她去往敵國和親浮定。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1.1 Spring IoC容器和bean簡介 本章介紹了Spring Framework實(shí)現(xiàn)的控制反轉(zhuǎn)(IoC)...
    起名真是難閱讀 2,584評論 0 8
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理层亿,服務(wù)發(fā)現(xiàn)桦卒,斷路器,智...
    卡卡羅2017閱讀 134,665評論 18 139
  • 1.1 spring IoC容器和beans的簡介 Spring 框架的最核心基礎(chǔ)的功能是IoC(控制反轉(zhuǎn))容器匿又,...
    simoscode閱讀 6,717評論 2 22
  • Spring 框架核心組件之一是 IOC方灾,IOC 則管理 Bean 的創(chuàng)建和 Bean 之間的依賴注入,對于 Be...
    阿里加多閱讀 1,092評論 0 0
  • 她清晨七點(diǎn)迷迷糊糊從床里起身的時候琳省,其實(shí)思想已經(jīng)在一小時前被每天準(zhǔn)時準(zhǔn)點(diǎn)為她準(zhǔn)備早飯的Big Daddy給喚醒迎吵。今...
    鯤九閱讀 377評論 0 1