Spring 常用註解原理剖析
前言
Spring 框架核心元件之一是 IOC,IOC 則管理 Bean 的建立和 Bean 之間的依賴注入,對於 Bean 的建立可以通過在 XML 裡面使用<bean/>標籤來配置,對於 Bean 之間的依賴可以使用構造方法注入、Set 方法注入在 XML 裡面配置。但是使用這種方式會使 XML 變的比較臃腫龐大,並且還需要開發人員一個個的在 XML 裡面配置 Bean 之間的依賴,這簡直是一個災難,還好 Spring 框架給我們提供了一系列的註解讓開發人員從這個災難中解脫出來,比如在一個類的成員變數上標註了一個簡單的 @Autowired 註解就可以實現了 Bean 之間的自動依賴注入,在一個類上標註了一個簡單的 @Component 註解就可以讓一個 Bean 注入到 Spring 容器……而 Spring 框架是如何通過註解簡化我們的工作量,實現這些功能的。本 Chat 就來解開這神祕的面紗。
本 Chat 主要內容如下:
- 我們經常使用 @Autowired,進行依賴注入,那麼為何能夠直接使用?它又是如何工作的?@Required 又是如何起到檢查 XML 裡面屬性有沒有被配置呢?
- Spring 框架是如何把標註 @Component 的 Bean 注入到容器?
- 我們經常使用的 @Configuration,@ComponentScan,@Import,@Bean 註解又是如何工作的?
- 我們經常使用 @PropertySource 引入配置檔案,那麼配置檔案裡面的配置是如何被註冊到 Spring 環境裡面的?
- 最後講解如何通過自定義註解實現一個簡單的樹形文件生成。
註解 @Autowired、@Required 的工作原理
註解 Autowired 的簡單使用
既然要研究多個 Bean 之間的依賴注入,那麼就先建立兩個 Bean,分別為 ServiceA 和 ServiceB。
ServiceA 程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 | publicclassServiceA { publicString getServiceName() { returnserviceName; } publicvoidsetServiceName(String serviceName) { this.serviceName = serviceName; } privateString serviceName; publicvoidsayHello() { System.out.println("serviceA sayHello "+ serviceName); } } |
ServiceB,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | importorg.springframework.beans.factory.annotation.Autowired; publicclassServiceB { @Autowired privateServiceA serviceA; publicServiceA getServiceA() { returnserviceA; } publicvoidsetServiceA(ServiceA serviceA) { this.serviceA = serviceA; } publicvoidsayHello() { serviceA.sayHello(); } } |
如上程式碼可知 ServiceB 內部使用註解 @Autowired 注入了 ServiceA 的例項。
然後使用下面 bean-test.xml 檔案配置兩個 Bean 的例項:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?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> |
最後測試類程式碼如下:
1 2 3 4 5 6 | publicclassTestAuowired { publicstaticvoidmain(String[] arg) { ClassPathXmlApplicationContext cpxa =newClassPathXmlApplicationContext("bean-test.xml"); cpxa.getBean("serviceB", ServiceB.class).sayHello(); } } |
執行測試程式碼,我們期望輸出 serviceA sayHello null,但是結果卻是如下:
1 2 3 4 | 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) |
從異常結果知道,ServiceB 裡面的 serviceA 物件為 null,也就是 XML 裡面配置的 serviceA 物件並沒有被注入到 ServiceB 例項內。其實要想使用 @Autowired 需要顯示的註冊對應的註解的處理器到 Spring 容器,具體是需要在 bean-test.xml 裡面新增<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor" />,新增後在執行程式碼就會輸出 serviceA sayHello null 了。
需要注意的是 @Autowired 除了可以標註在變數上,還可以標註在變數對應的 set 訪問器上,比如下面程式碼1和程式碼2效果是一樣。
程式碼1:
1 2 3 4 | @Autowired publicvoidsetServiceA(ServiceA serviceA) { this.serviceA = serviceA; } |
程式碼2:
1 2 | @Autowired privateServiceA serviceA; |
AutowiredAnnotationBeanPostProcessor 原理剖析
首先來了解下 AutowiredAnnotationBeanPostProcessor 的類圖結構,如下圖:
可知 AutowiredAnnotationBeanPostProcessor 直接或者間接實現了 Spring 框架的好多擴充套件介面:
- 實現了 BeanFactoryAware 介面,可以讓 AutowiredAnnotationBeanPostProcessor 獲取到當前 Spring 應用程式上下文管理的 BeanFactory,從而可以獲取到 BeanFactory 裡面所有的 Bean。
- 實現了 MergedBeanDefinitionPostProcessor 介面,可以讓 AutowiredAnnotationBeanPostProcessor 對 BeanFactory 裡面的 Bean 在被例項化前對 Bean 定義進行修改。
- 繼承了 InstantiationAwareBeanPostProcessorAdapter,可以讓 AutowiredAnnotationBeanPostProcessor 在 Bean 例項化後執行屬性設定。
有關更多 Spring 框架擴充套件介面的知識可以參考:Spring 框架常用擴充套件介面揭祕。
OK,下面看看這些擴充套件介面在 AutowiredAnnotationBeanPostProcessor 中呼叫時機,以及在實現依賴注入時候充當了什麼作用,AutowiredAnnotationBeanPostProcessor 的程式碼執行時序圖如下:
- 程式碼(1)Spring 框架會在建立 AutowiredAnnotationBeanPostProcessor 例項過程中呼叫 setBeanFactory 方法注入 Spring 應用程式上下文管理的 BeanFactory 到 AutowiredAnnotationBeanPostProcessor 中,所以 AutowiredAnnotationBeanPostProcessor 就可以操作 BeanFactory 裡面的所有的 Bean 了。
- 程式碼(2)在 Spring 中每個 Bean 例項化前,Spring 框架都會呼叫 AutowiredAnnotationBeanPostProcessor 的postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName)方法,用來對當前 Bean 的定義(beanDefinition)進行修改,這裡主要通過 findAutowiringMetadata 方法找到當前 Bean 中標註 @Autowired 註解的屬性變數和方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | privateInjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz,@NullablePropertyValues pvs) { //根據當前bean資訊生成快取key String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName()); //快取中是否存在當前bean的元資料 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); } } } returnmetadata; } |
這裡是先通過 buildAutowiringMetadata 收集當前 Bean 中的註解資訊,其中會先查詢當前類裡面的註解資訊,對應在變數上標註 @Autowired 的變數會建立一個 AutowiredFieldElement 例項用來記錄註解資訊,對應在 set 方法上標註 @Autowired 的方法會建立一個 AutowiredMethodElement 物件來儲存註解資訊。然後會遞迴解析當前類的直接父類裡面的註解,並把最遠父類到當前類裡面的註解資訊依次存放到InjectionMetadata物件(內部使用集合儲存所有方法和屬性上的註解元素物件),然後快取起來以便後面使用,這裡的快取實際是個併發 map:
1 | privatefinalMap<String, InjectionMetadata> injectionMetadataCache =newConcurrentHashMap<>(256); |
- 程式碼(11)則是在對Spring的BeanFactory裡面的bean例項化後初始化前呼叫 AutowiredAnnotationBeanPostProcessor 的PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName)方法設定依賴注入物件;首先程式碼(12)獲取當前 Bean 裡面的依賴元資料資訊,由於在步驟(2)時候已經收集到了快取,所以這裡是直接從快取獲取的;這裡獲取的就是步驟(2)快取的 InjectionMetadata 物件;步驟(13)則逐個呼叫 InjectionMetadata 內部集合裡面存放的屬性和方法註解物件的 inject 方法,通過反射設定依賴的屬性值和反射呼叫 set 方法設定屬性值。
如果註解加到了變數上則會呼叫 AutowiredFieldElement 的 inject 方法用來通過反射設定屬性值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | protectedvoidinject(Object bean,@NullableString beanName,@NullablePropertyValues pvs)throwsThrowable { Field field = (Field)this.member; Object value; ... //解析依賴的bean value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter); ... //反射設定依賴屬性值 if(value !=null) { ReflectionUtils.makeAccessible(field); field.set(bean, value); } } |
如果註解加到了 set 方法上則呼叫 AutowiredMethodElement 的 inject 方法通過反射呼叫 set 方法設定依賴的變數值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | protectedvoidinject(Object bean,@NullableString beanName,@NullablePropertyValues pvs)throwsThrowable { ... Method method = (Method)this.member; Object[] arguments; ... //解析依賴的bean value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter); ... //反射設定呼叫set方法設定屬性值 if(arguments !=null) { try{ ReflectionUtils.makeAccessible(method); method.invoke(bean, arguments); } catch(InvocationTargetException ex){ throwex.getTargetException(); } } } |
這裡需要注意的是 @Autowired 註解有一個布林變數的 required 屬性,用來決定在依賴注入時候是否檢測依賴的 Bean 在 BeanFactory 裡面是否存在,預設是 true,就是如果不存在就會丟擲下面異常:org.springframework.beans.factory.NoSuchBeanDefinitionException異常,這個讀者可以把 bean-test.xml 裡面的 serviceA 注入程式碼去掉測試下。
如果 required 設定為 false,則在依賴注入時候不去檢查依賴的 Bean 是否存在,而是在你具體使用依賴的 Bean 時候才會丟擲 NPE 異常:
1 2 3 | 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) |
具體做檢驗的地方就是上程式碼的 resolveDependency 方法裡面。
注:@Autowired 的使用簡化了我們的開發,其原理是使用 AutowiredAnnotationBeanPostProcessor 類來實現,該類實現了 Spring 框架的一些擴充套件介面,通過實現 BeanFactoryAware 介面使其內部持有了 BeanFactory(可輕鬆的獲取需要依賴的的 Bean);通過實現 MergedBeanDefinitionPostProcessor 擴充套件介面,在 BeanFactory 裡面的每個 Bean 例項化前獲取到每個 Bean 裡面的 @Autowired 資訊並快取下來;通過實現 Spring 框架的 postProcessPropertyValues 擴充套件介面在 BeanFactory 裡面的每個 Bean 例項後從快取取出對應的註解資訊,獲取依賴物件,並通過反射設定到 Bean 屬性裡面。
註解 Required 的簡單使用
“註解Autowired的簡單使用”小節執行後結果會輸出 serviceA sayHello null,其中 null 是因為沒給 serviceA 裡面的屬性 serviceName 賦值的原因,在開發時候開發人員也會比較容易犯這個錯誤,而要等執行時使用該屬性的時候才知道沒有賦值。那麼有沒有辦法在 Spring 框架進行 Bean 建立時候就進行檢查某些必要的屬性是否被設定了呢?
其實 @Required 就是做這個的,比如如果你想在 Spring 建立 ServiceA 時候就檢查 serviceName 有沒有被設定,你需要在 serviceName 的 set 方法上加入 @Required 註解:
1 2 3 4 | @Required publicvoidsetServiceName(String serviceName) { this.serviceName = serviceName; } |
並且需要在 XML 裡面新增下面配置,它是 @Required 註解的處理器:
1 | <beanclass="org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor"/> |
加上這些後在執行程式碼會輸出下面結果:
可見 Spring 在設定 ServiceA 的例項的屬性時候會檢查該屬性是否被設定,如果沒有則會丟擲異常。
通過在 bean-test.xml 裡面新增屬性設定如下:
1 2 3 | <bean id="serviceA"class="com.jiaduo.test.ServiceA"> <property name="serviceName"value="Service Name"/> </bean> |
然後在執行,輸出結果如下:
1 | serviceA sayHello Service Name |
RequiredAnnotationBeanPostProcessor 原理剖析
RequiredAnnotationBeanPostProcessor 類似 AutowiredAnnotationBeanPostProcessor 也是間接或者直接實現了 Spring 框架相同的介面。通過實現 BeanFactoryAware 介面內部持有了 BeanFactory(可輕鬆的獲取需要依賴的Bean);通過實現 Spring 框架的 postProcessPropertyValues 擴充套件介面在 BeanFactory 裡面的每個 Bean 例項後設置屬性前,檢查標註 @Required 的 set 訪問器對應的屬性是否被設定。
這個邏輯比較簡單,直接看下 RequiredAnnotationBeanPostProcessor 的 postProcessPropertyValues 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | publicPropertyValues postProcessPropertyValues( PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName)throwsBeansException { if(!this.validatedBeanNames.contains(beanName)) { if(!shouldSkip(this.beanFactory, beanName)) { List<String> invalidProperties =newArrayList<>(); for(PropertyDescriptor pd : pds) { //判斷屬性的set方法是否標註了@Required註解,並且是否該屬性沒有被設定 if(isRequiredProperty(pd) && !pvs.contains(pd.getName())) { invalidProperties.add(pd.getName()); } } //如果發現屬性的set方法標註了@Required註解,但是屬性沒有被設定,則丟擲異常 if(!invalidProperties.isEmpty()) { thrownewBeanInitializationException(buildExceptionMessage(invalidProperties, beanName)); } } this.validatedBeanNames.add(beanName); } returnpvs; } |
其中 isRequiredProperty 作用是判斷當前屬性的 set 方法是否標註了 @Required 註解,程式碼如下:
1 2 3 4 | protectedbooleanisRequiredProperty(PropertyDescriptor propertyDescriptor) { Method setter = propertyDescriptor.getWriteMethod(); return(setter !=null&& AnnotationUtils.getAnnotation(setter, getRequiredAnnotationType()) !=null); } |
注:使用 @Autowired 和 @Required 時候需要注入對應的註解處理器,這很麻煩,所以 Spring 框架添加了一個<context:annotation-config />標籤,當你在 XML 裡面引入這個標籤後,就預設注入了 AutowiredAnnotationBeanPostProcessor 和 RequiredAnnotationBeanPostProcessor 。
註解 @Component 的工作原理
在第二節中,我們通過在 bean-test.xml 裡面配置<bean id="serviceA" class="com.jiaduo.test.ServiceA"></bean>的方式注入 ServiceA 的一個例項;其實可以避免在 XML 裡面使用<bean/>這種方式,可以直接在 ServiceA 類上標註 @Component 註解方式:
1 2 3 4 | @Component("serviceA") publicclassServiceA { ... } |
當一個類上標註 @Component 註解時候,Spring 框架會自動註冊該類的一個例項到 Spring 容器,但是我們需要告訴 Spring 框架需要到去哪裡查詢標註該註解的類,所以需要在 bean-test.xml 裡面配置如下:
1 | <context:component-scan base-package="com.jiaduo.test"/> |
其中 base-package 就是告訴 Spring 框架要去查詢哪些包下的標註 @Component 註解的類。
下面我們來研究下 Spring 框架是如何解析<context:component-scan/>標籤並掃描標註 @Component 註解的 Bean 註冊到 Spring 容器的。
首先看下解析<context:component-scan/>標籤的 ComponentScanBeanDefinitionParser 類的時序圖:
- 如上時序圖步驟(3)建立了一個 ClassPathBeanDefinitionScanner 掃描器,程式碼如下:
1 2 3 4 | protectedClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext,booleanuseDefaultFilters) { returnnewClassPathBeanDefinitionScanner(readerContext.getRegistry(), useDefaultFilters, readerContext.getEnvironment(), readerContext.getResourceLoader()); } |
變數 useDefaultFilters 說明是否使用預設的 filters,所謂 filter 也就是過濾器,這裡 ClassPathBeanDefinitionScanner 會掃描指定包路徑裡面的類,但是那些需要的類,就是通過 filter 進行過濾的,預設 useDefaultFilters 為 true,<context:component-scan base-package="com.jiaduo.test"/>等價於<context:component-scan base-package="com.jiaduo.test" use-default-filters="true"/>,會使用下面程式碼註冊預設 filters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | protectedvoidregisterDefaultFilters() { //這裡決定指定包路徑下標註@Component註解的類是我們想要的 this.includeFilters.add(newAnnotationTypeFilter(Component.class)); ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader(); try{ this.includeFilters.add(newAnnotationTypeFilter( ((Class<?extendsAnnotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)),false)); } catch(ClassNotFoundException ex) { ... } try{ this.includeFilters.add(newAnnotationTypeFilter( ((Class<?extendsAnnotation>) ClassUtils.forName("javax.inject.Named", cl)),false)); } catch(ClassNotFoundException ex) { ... } } |
- 步驟(4)解析使用者自定義的 BeanNameGenerator 的實現類,用來給掃描的類起一個在 BeanFactory 裡面的名字,配置如下:
1 | <context:component-scan base-package="com.jiaduo.test"name-generator="com.my.name.generator.MyBeanNameGenerator"/> |
其中 name-generator 指定 BeanNameGenerator 的實現類的包路徑+類名,內部會建立一個該類的例項。
- 步驟(5)主要用來設定是否對掃描類進行 scope-proxy,我們知道在 XML 裡面配置 Bean 的時候可以指定 scop 屬性來配置該 Bean 的作用域為 singleton、prototype、request、session等,對應後三者來說,Spring 的實現是對標註該作用域的 Bean 進行代理來實現的,而我們知道 Spring 代理為 JDK 代理和 CGLIB 代理(可以參考 :Spring 框架之 AOP 原理剖析),所以步驟(5)作用就是讓使用者通過 scoped-proxy 指定代理方式:<context:component-scan base-package="com.jiaduo.test" scoped-proxy="no"/>,這是預設方式不進行代理;scoped-proxy="interfaces" 標示對介面進行代理,也就是使用 JDK 動態代理;scoped-proxy="targetClass" 標示對目標物件進行代理,也就是使用 CGLIB 進行代理。
- 步驟(6)解析使用者自定義過濾器,前面我們說了,預設下 use-default-filters=true,預設掃描之後只會注入標註 @Component 的元素;這裡則允許使用者自定義攔截器,設定需要註冊掃描到的那些類和排除掃描到的那些類,如下配置:
1 2 3 4 5 6 7 8 | <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> |
需要注意的是,當使用子標籤<context:include-filter/>和<context:exclude-filter/>自定義使用者過濾器時候需要這樣設定:use-default-filters="false"才會生效。
- 程式碼(7)則執行具體掃描,其中 basePackages 是註解裡面 base-package 解析後的包路徑列表,我們在指定base-package時候可以通過,; \t\n中其中一個分隔符指定多個包路徑,比如:<context:component-scan base-package="com.jiaduo.test;com.jiaduo.test1"/>。步驟(8)查詢當前包路徑下滿足過濾器列表的候選bean,預設是查詢所有標註了@Component註解的Bean。步驟(13)則註冊滿足條件的bean到Spring容器。
- 步驟(14)註冊一些組元,比如步驟(15)預設情況下會註冊的前面提到的<context:annotation-config />標籤實現的內容,你可以通過下面方式關閉該功能:
1 | <context:component-scan base-package="com.jiaduo.test"annotation-config="false"/> |
注:當我們在 XML 裡面配置<context:component-scan/>標籤後,Spring 框架會根據標籤內指定的包路徑下查詢指定過濾條件的 Bean,並可以根據標籤內配置的 BeanNameGenerator 生成 Bean 的名稱,根據標籤內配置的 scope-proxy 屬性配置 Bean 被代理的方式,根據子標籤<context:include-filter/>,<context:exclude-filter/>配置自定義過濾條件。
註解 @Configuration、@ComponentScan、@Import、@PropertySource、@Bean工作原理
一個簡單 Demo
《Spring 框架常用擴充套件介面揭祕》講到在 Spring 框架中,每個應用程式上下文(ApplicationContext)管理著一個 BeanFactory,應用程式上下文則是對 BeanFactory 和 Bean 的生命週期中的各個環節進行管理。
而應用程式上下文的子類除了有解析 XML 作為 Bean 來源的 ClassPathXmlApplicationContext,還有基於掃描註解類作為 Bean 來源的 AnnotationConfigApplicationContext,本節就結合 AnnotationConfigApplicationContext 應用程式上下文來講解 @Configuration、@ComponentScan、@Import、@PropertySource、@Bean註解的使用與原理。
其中 ServiceA 和 ServiceB 程式碼修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | publicclassServiceB { publicServiceA getServiceA() { returnserviceA; } publicvoidsetServiceA(ServiceA serviceA) { this.serviceA = serviceA; } @Autowired privateServiceA serviceA; publicvoidsayHello() { serviceA.sayHello(); } } publicclassServiceA { publicString getServiceName() { returnserviceName; } publicvoidsetServiceName(String serviceName) { this.serviceName = serviceName; } //這裡加了一個註解 @Value("${service.name}") privateString serviceName; publicvoidsayHello() { System.out.println("serviceA sayHello "+ serviceName); } } |
其中 ConfigBean 程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 | importorg.springframework.context.annotation.Bean; importorg.springframework.context.annotation.Configuration; importcom.jiaduo.test.annotation.ServiceA; @Configuration publicclassConfigBean { @Bean publicServiceA serviceA() { returnnewServiceA(); } } |
其中 ConfigBean2 程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | importorg.springframework.context.annotation.Bean; importorg.springframework.context.annotation.Configuration; importorg.springframework.context.annotation.Import; importcom.jiaduo.test.annotation.ServiceA; importcom.jiaduo.test.annotation.ServiceB; @Configuration publicclassConfigBean2 { @Bean publicServiceB serviceB() { returnnewServiceB(); } } |
配置檔案 config.properties 內容如下:
1 | service.name=Annotation Learn |
其中測試類 TestAnnotation 程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @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) publicclassTestAnnotaion { publicstaticvoidmain(String[] args) { // (5) AnnotationConfigApplicationContext ctx =newAnnotationConfigApplicationContext(TestAnnotaion.class); ctx.getBean("serviceB", ServiceB.class).sayHello();// (6) } } |
執行 TestAnnotaion 的 main 方程式碼輸出:serviceA sayHello Annotation Learn。
程式碼(5)建立一個 AnnotationConfigApplicationContext 型別的應用程式上下文,建構函式引數為 TestAnnotaion.class。
其內部會解析 TestAnnotaion 類上的 ComponentScan 註解,並掃描 basePackages 指定的包裡面的所有標註 @Configuration 註解的類(這裡會注入 ConfigBean 類到 Spring 容器),然後解析 ConfigBean 內部標註有 @Bean 的方法,把方法內建立的物件注入到 Spring 容器(這裡是把 serviceA 注入到了 Spring 容器)。
然後會解析 TestAnnotaion 類上的 Import 註解,Import 註解作用是把標註 @Configuration 註解裡面建立的 Bean 注入到 Spring 容器,這裡是把 ConfigBean2 裡面建立的 serviceB 注入到了 Spring 容器。
原理剖析
從 Demo 可知一切源於 AnnotationConfigApplicationContext,那麼就從 AnnotationConfigApplicationContext 的建構函式開始,程式碼如下:
1 2 3 4 5 6 7 8 | publicAnnotationConfigApplicationContext(Class<?>... annotatedClasses) { //呼叫無參建構函式 this(); //註冊含有註解的類 register(annotatedClasses); //重新整理應用程式上下文 refresh(); } |
其呼叫時序圖如下:
- 如上時序圖步驟(1)呼叫了 AnnotationConfigApplicationContext 的無參建構函式,其內部建立了一個 AnnotatedBeanDefinitionReader 物件,該物件建構函式內部呼叫 AnnotationConfigUtil.registerAnnotationConfigProcessors 方法註冊了註解處理器(其作用等價於在 XML 裡面配置<context:annotation-config />),其中就註冊了 ConfigurationClassPostProcessor 處理器,該處理器就是專門用來處理 @Configuration 註解的,這個後面再講。
- 步驟(4)則是註冊 AnnotationConfigApplicationContext 建構函式裡面傳遞的含有註解的類到 Spring 容器(這裡是註冊 TestAnnotaion 類到 Spring 容器)。
- 步驟(6)重新整理應用程式上下文,使用註冊的 ConfigurationClassPostProcessor 處理器解析 TestAnnotaion 上的註解,並註冊相應的 Bean 到 Spring 容器。
下面主要來看下 ConfigurationClassPostProcessor 的處理時序圖:
- ConfigurationClassPostProcessor 實現了 Spring 框架的 BeanDefinitionRegistryPostProcessor 介面所以具有 postProcessBeanDefinitionRegistry 方法 ,具體可以參考 :Spring 框架和 Tomcat 容器擴充套件介面揭祕。
- 其中步驟(2)遍歷應用程式上下文中的 Bean 查詢標註 @Configuration 的 Bean 定義,具體是使用 checkConfigurationClassCandidate 方法檢測,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | publicstaticbooleancheckConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) { ... //該類上是否標註了@Configuration註解 if(isFullConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } //該類上是否標註了@Component,@ComponentScan,@Import註解 elseif(isLiteConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); } else{ returnfalse; } ... returntrue; } |
可知只有類上標註 @Configuration、@Component、@ComponentScan、@Import 註解的 Bean 才是候選 Bean。
- 步驟(4)建立了一個 ConfigurationClassParser 物件,這個物件就是專門用來解析標註 @Configuration 註解的 Bean 的。這裡首先呼叫 parse 方法對步驟(2)產生的候選 Bean 進行解析,本文例子是先對 TestAnnotaion 類解析,並會對 TestAnnotaion 上的 @ComponentScan 和 @Import 進行解析,解析出來的 Bean 可能又含有了 @Configuration 註解,那麼把這些新的包含 @Configuration 的 Bean 作為候選 Bean 後然後呼叫 parse 方法,依次類推直到 parse 解析出來的 Bean 不在包含 @Configuration 註解。其中步驟(7)則是註冊解析到的標註 @Import 的 Bean 和 @Bean 的 Bean 到 Spring 容器。
下面著重講解下 ConfigurationClassParser 的 parse 方法:
- 其中最外層迴圈是遞迴解析 configuration 類和它的超類中標註 @Configuration 的類,也就是解析完當前類,會設定 sourceClass=sourceClass.getSuperClass();
- 迴圈內步驟(5)、(6)是解析並處理所有標註 @PropertySources 註解的Bean,具體程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | privatevoidprocessPropertySource(AnnotationAttributes propertySource)throwsIOException { ... //獲取註解上的值 String[] locations = propertySource.getStringArray("value"); ... for(String location : locations) { try{ String resolvedLocation =this.environment.resolveRequiredPlaceholders(location); Resource resource =this.resourceLoader.getResource(resolvedLocation); //設定location裡面的屬性到Spring的環境environment addPropertySource(factory.createPropertySource(name,newEncodedResource(resource, encoding))); } catch(IllegalArgumentException | FileNotFoundException | UnknownHostException ex) { ... } } } privatevoidaddPropertySource(PropertySource<?> propertySource) { //獲取Spring環境environment裡面的屬性集 String name = propertySource.getName(); MutablePropertySources propertySources = ((ConfigurableEnvironment)this.environment).getPropertySources(); ... //添加註解@PropertySource裡面的配置檔案資訊到Spring環境 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); } |
- 迴圈內步驟(7)、(8)是解析標註 @ComponentScan 的類,並註冊掃描到的類到 Spring 容器,並且對掃描到含有 @Configuration 的類在進行解析,具體解析 @ComponentScan 的邏輯如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | publicSet<BeanDefinitionHolder> parse(AnnotationAttributes componentScan,finalString declaringClass) { //建立一個掃描器 ClassPathBeanDefinitionScanner scanner =newClassPathBeanDefinitionScanner(this.registry, componentScan.getBoolean("useDefaultFilters"),this.environment,this.resourceLoader); //配置掃描器中bean的BeanNameGenerator Class<?extendsBeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator"); booleanuseInheritedGenerator = (BeanNameGenerator.class== generatorClass); scanner.setBeanNameGenerator(useInheritedGenerator ?this.beanNameGenerator : BeanUtils.instantiateClass(generatorClass)); //設定scopedProxy ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy"); if(scopedProxyMode != ScopedProxyMode.DEFAULT) { scanner.setScopedProxyMode(scopedProxyMode); } else{ Class<?extendsScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver"); scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass)); } //設定資源匹配模式 scanner.setResourcePattern(componentScan.getString("resourcePattern")); //設定掃描器的過濾條件 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); } } //是否延遲初始化 booleanlazyInit = componentScan.getBoolean("lazyInit"); if(lazyInit) { scanner.getBeanDefinitionDefaults().setLazyInit(true); } //解析掃描包路徑 Set<String> basePackages =newLinkedHashSet<>(); 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(newAbstractTypeHierarchyTraversingFilter(false,false) { @Override protectedbooleanmatchClassName(String className) { returndeclaringClass.equals(className); } }); //執行掃描 returnscanner.doScan(StringUtils.toStringArray(basePackages)); } |
其內部邏輯與<context:component-scan/>相似,這裡不再累述了。
- 步驟(10)解析所有標註 @Import 的 Bean,具體注入到 Spring 容器實際是在 ConfigurationClassPostProcessor 的時序圖的步驟(7),其中掃描 @Import 註解的遞迴程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | privateSet<SourceClass> getImports(SourceClass sourceClass)throwsIOException { Set<SourceClass> imports =newLinkedHashSet<>(); Set<SourceClass> visited =newLinkedHashSet<>(); collectImports(sourceClass, imports, visited); returnimports; } privatevoidcollectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited) throwsIOException { 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)解析所有標註 @Bean 的方法,具體注入操作是在 ConfigurationClassPostProcessor 的時序圖的步驟(7),解析 @Bean 的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | privateSet<MethodMetadata> retrieveBeanMethodMetadata(SourceClass sourceClass) { AnnotationMetadata original = sourceClass.getMetadata(); //獲取所有標註@Bean的方法元資料,這些方法返回順序是任意的 Set<MethodMetadata> beanMethods = original.getAnnotatedMethods(Bean.class.getName()); if(beanMethods.size() >1&& originalinstanceofStandardAnnotationMetadata) { try{ //使用asm讀取位元組碼檔案,並獲取標註@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 =newLinkedHashSet<>(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) { } } returnbeanMethods; } |
注:ConfigurationClassPostProcessor 處理器是 Spring 框架處理本節這些註解的關鍵類,本節內容較為複雜,在解析註解使用運用了大量的迴圈巢狀和遞迴演算法,程式碼研究起來還是有一定難度的,希望讀者結合時序圖慢慢理解。
基於自定義註解實現樹形業務文件生成
自定義註解
當一個系統隨著不斷迭代的需求累加後,業務邏輯就會變得錯綜複雜,新人接手時候就會顯得很吃力;一個辦法是採用模組化思想,每個模組提供一個獨立業務功能,分清業務邊界,也就是使用領域模型;在領域模型中一個系統可以劃分為若干模組,每個模組可以對應多個域,域與域之間通過對外暴露的唯一的 Service 進行通訊,每個域下面有可能對於多個子域。
本節就通過在每個域與子域提供的服務上新增自定義註釋,並收集這些註解來生成一個樹形的文件來顯示整個系統裡面都有哪些域服務。
根據上面介紹,我們設計三類註解。
- 在模組類上面加的註解。
1 2 3 4 5 6 7 | @Target({ ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public@interfaceModuleAnnotation { String moduleName()default""; String moduleDesc()default""; } |
其中 moduleName 是模組的名字要保證應用唯一,moduleDesc 是當前模組的描述。
- 域服務類或者方法上面新增的註解。
1 2 3 4 5 6 7 8 9 10 11 | @Target({ ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public@interfaceDomainAnnotation { String moduleName()default""; String rootDomainName()default""; String rootDomainDesc()default""; String subDomainName()default""; String subDomainDesc()default""; String returnDesc()default"void"; } |
- 在域服務介面的引數上新增的註解,為了獲取引數名字和描述使用。
1 2 3 4 5 6 7 8 | @Target({ElementType.METHOD,}) @Retention(RetentionPolicy.RUNTIME) @Documented public@interfaceParam { String paramName()default""; String paramType()default""; String paramDesc()default""; } |
其中 moduleName 說明當前域服務屬於哪個模組;rootDomainName 和 rootDomainDesc 是根域服務名稱和描述;subDomainName、subDomainDesc 為子域名稱和描述,多箇中間用英文逗號分隔;returnDesc 是返回值說明。
例如模組註解加在類上:
1 2 3 | @ModuleAnnotation(moduleName="trialing",moduleDesc="庭審模組") publicclassmoduleclass{ } |
域服務註解加在方法上,沒有子域時候:
1 2 3 | @DomainAnnotation(moduleName="trialing",rootDomainName="seaDomain",rootDomainDesc="純語音庭審服務") publicvoidm2New() { } |
域服務載入方法上,有子域時候:
1 2 3 4 5 6 7 8 9 10 11 | @DomainAnnotation(moduleName="trialing",rootDomainName="videoDoamin",rootDomainDesc="視訊庭審服務",subDomainName="speechDoamin,speechVideoDoamin") publicString hello(@Param(paramName="type",paramDesc="案件型別")String type,@Param(paramName="num",paramDesc="案件個數")String num){ } @DomainAnnotation(rootDomainName="speechDoamin",rootDomainDesc="語音識別服務") publicString hello2(@Param(paramName="caseId",paramDesc="案號")Long caseId){ } @DomainAnnotation(rootDomainName="speechVideoDoamin",rootDomainDesc="視訊+語音識別服務") publicString hello3(@Param(paramName="name",paramDesc="姓名")String name,@Param(paramName="address",paramDesc="地址")String address){ } |
如果我們能拿到所有類的註解資訊,然後根據模組註解與域名註解的關聯,就可以生成一個文件,類似下圖:
如上圖,樹根為 application 說明當前是什麼應用,它下面有兩個 module 分別為證據模組和庭審模組;在證據模組下有兩個域服務分別為質證服務和舉證服務,並且有具體服務的函式簽名;在庭審模組下有兩個域服務分別為純語音庭審服務和視訊庭審服務;視訊庭審服務下面有兩個字域服務分泌為語音識別服務和視訊+語音識別服務,並且有對應的服務的函式簽名。
自定義註解的收集
本文選擇實現 Spring 框架的 InstantiationAwareBeanPostProcessor 擴充套件介面來做自定義註解資訊收集,該擴充套件的介面如下:
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
}
在 Spring 容器中每個 Bean 例項化前都會呼叫這個擴充套件介面,而該接口裡面有 Bean 的 Class 物件,所以可以方便獲取它方法和類上的註解資訊。
有關 Spring 框架擴充套件介面可以參考 :Spring 框架常用擴充套件介面揭祕。
我們自定義 AnnotationInstantiationAwareBeanPostProcessor 類的 postProcessBeforeInstantiation 的實現為:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | publicObject postProcessBeforeInstantiation(Class<?> beanClass, String beanName)throwsBeansException { if(!isOpenAnnotation) { returnnull; } try{ // 模組類註解收集 ModuleAnnotation moduleAnnotation = beanClass.getAnnotation(ModuleAnnotation.class); if(null!= moduleAnnotation) { moudleAnnotationList.add(moduleAnnotation); } DomainAnnotation domainAnnotation = beanClass.getAnnotation(DomainAnnotation.class); // 域服務類註解收集 if(null!= domainAnnotation) { domainAnnotationList.add(domainAnnotation); } // 域服務方法註解收集 for(Method method : beanClass.getDeclaredMethods()) { domainAnnotation = method.getAnnotation(DomainAnnotation.class); if(null!= domainAnnotation) { domainAnnotationList.add(domainAnnotation); // 獲取引數型別,名稱,函式簽名 getMethodInfoFromParam(method,domainAnnotation); } } }catch(Exception e) { System.out.println("----------------error:"+ e.getLocalizedMessage()); } returnnull; } privatevoidgetMethodInfoFromParam(Method method,DomainAnnotation domainAnnotation) { //引數上的註解的獲取 Annotation[][] parameterAnnotations = method.getParameterAnnotations(); //儲存引數名稱 String[] parameterNames =newString[parameterAnnotations.length]; //引數型別 Class<?>[] parameterTypes = method.getParameterTypes(); intindex =0; //拼接函式簽名 StringBuffer sb =newStringBuffer(); String methodName = method.getName(); String returnType = method.getReturnType().getName(); sb.append(returnType).append(" ").append(methodName).append("("); Map<String, String> map =newHashMap<String, String>(); //獲取方法的引數名字和引數描述儲存到map for(Annotation[] parameterAnnotation : parameterAnnotations) { for(Annotation annotation : parameterAnnotation) { if(annotationinstanceofParam) { 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 =newAnnotationInfo(); 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); } |
註解資訊的列印
上一小節收集到了所有的註解,本節就簡單介紹下如何打印出樹形文件,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | publicActionResult generateDocument(ErrorContext context) { ActionResult result =newActionResult(); //遞迴列印 printTree(); returnresult; } privatevoidprintTree() { //獲取收集的註解資訊 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 =newStringBuffer(); sb.append("-mouduleName:"+ moudleName).append(",moudleDesc:"+ moudleDesc); System.out.println(sb.toString()); //列印模組下的域服務 for(DomainAnnotation da : domainList) { if(da.moduleName().equals(moudleName)) { // 列印當前域服務 printMethodInfo(da,2,'-'); // 列印子域服務 generateSubDoamin(domainList, da.subDomainName(),3); } } System.out.println(); } } |
本節使用簡單的列印輸出樹形文件,其實既然已經獲得了註解資訊,我們可以根據需要比如生成 Markdown 檔案,PDF,或者直接把資料扔給前端,前端按照需要格式渲染都可以。
總結
本文講解了 Spring 框架中常用註解的原理實現,希望讀者能參考本文對著原始碼自己 Debug 跟入一下,以便加深理解;另外目前比較火的微服務框架 SpringBoot 中提倡使用註解,不再建議使用 XML 配置,如果你對本文能夠很好掌握,可以嘗試去研究下 SpringBoot 裡面一些註解原理。