前言
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)佛嬉,如下圖:
可知 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í)行時序圖如下:
- 代碼(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é)果:
可見 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 類的時序圖:
- 如上時序圖步驟(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)用時序圖如下:
如上時序圖步驟(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 方法:
其中最外層循環(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),就可以生成一個文檔纺铭,類似下圖:
如上圖抒和,樹根為 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 里面一些注解原理。