擴充套件Spring——使用 Annotation將配置資源注入到Bean中

lvzhou_MadSky發表於2014-05-08

 

來源:http://www.blogjava.net/max/archive/2009/11/20/303112.html

使用XML還是Annotation定義Bean

 

自從Spring 2.5開始引入使用Annotation定義Bean的方式之後,業界時常會有一些關於“到底是應該使用XML還是Annotation定義Bean 呢?”的討論。筆者本人就比較中庸,喜歡兩者結合使用——對於一些框架性的基礎型的Bean使用XML,對於業務性的Bean則使用 Annotation。

 

然而,什麼是“框架性的基礎型的Bean”呢?這些Bean可以理解為由第三方開源元件提供的基礎Java類的、又或者開發者在其基礎上擴充套件而來的 Bean,如資料來源org.apache.commons.dbcp.BasicDataSource、事務管理器 org.springframework.orm.hibernate3.HibernateTransactionManager等。這些Bean一般 在應用程式中數量較少,卻起著框架性和全域性性的作用,對於此類Bean使用XML的好處是必要時可以通過修改一個或幾個XML檔案即可改變應用程式行為滿 足實際的專案需求,如下清單1所示。

 

 1 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
 2       destroy-method="close">
 3    <property name="driverClassName" value="${jdbc.driverClassName}" />
 4    <property name="url" value="${jdbc.url}" />
 5    <property name="username" value="${jdbc.username}" />
 6    <property name="password" value="${jdbc.password}" />
 7 </bean>
 8 <bean id="transactionManager" 
 9       class="org.springframework.orm.hibernate3.HibernateTransactionManager">
10    <property name="sessionFactory" ref="sessionFactory" />
11 </bean>

清單 1. 使用XML定義框架性的Bean

此外,我們再來解釋一下什麼是“業務性的Bean”。這些Bean相對比較容易理解,也就是開發者根據業務需求編寫的XxxDao、XxxManager 或XxxService等。它們的特點是為數眾多,定義起來比較麻煩。Annotation方式的簡潔性可以最大程度地減少這方便的繁鎖,而且可以避免諸 如打錯型別名稱等常見的小錯誤。對比清單2、3和4的程式碼大家應該會有更為深刻的理解。

 

1 <bean id="myService" class="net.blogjava.max.service.MyServiceImpl">
2    <property name="myDao1" ref="myDao1" />
3    <!-- 其它DAO引用  -->
4    <property name="myDaoN" ref="myDaoN" />
5 </bean>

清單 2. 使用XML定義業務性的Bean

 1 public class MyServiceImpl implements MyService {
 2    private MyDao1 myDao1;
 3    // 其它DAO
 4    private MyDaoN myDaoN;
 5 
 6    public void setMyDao1(MyDao1 myDao1) {
 7       this.myDao1 = myDao1;
 8    }
 9 
10    public void setMyDaoN(MyDaoN myDaoN) {
11       this.myDaoN = myDaoN;
12    }
13    // 其它業務程式碼
14 }

清單 3. 使用XML方式時Bean的程式碼

 1 @Service("myService")
 2 public class MyServiceImpl implements MyService {
 3    @Resource
 4    private MyDao1 myDao1;
 5    // 其它DAO
 6    @Resource
 7    private MyDaoN myDaoN;
 8 
 9    // 其它業務程式碼
10 }

清單 4. 使用Annotation方式的Bean程式碼

清單2、3實現的功能與清單4一樣,都是在Spring容器中定義一個MyServiceImpl型別的Bean。孰優孰劣?一目瞭然!

 

在Spring中配置應用程式

 

大家可以從清單1看到有${xxx.xxx}的寫法,有Spring開發經驗的朋友可能已經知道這是使用Spring框架時配置應用程式的方式之一。為了方便一些不甚瞭解的朋友,筆者在此也大概講述一下這種配置方式的步驟。

 

首先,在工程中新建一個資源(Property)檔案(筆者建議放在原始碼目錄下),通過“名稱=取值”的方式定義應用的配置,如下清單5所示。

 

1 jdbc.driverClassName=oracle.jdbc.driver.OracleDriver
2 jdbc.url=jdbc\:oracle\:thin\:@localhost\:1521\:ORCL
3 jdbc.username=max
4 jdbc.password=secret

清單 5. 配置程式碼片段

然後,定義一個 org.springframework.beans.factory.config.PropertyPlaceholderConfigurer型別 的Bean,id可以為propertyConfigurer。通常我們需要通過設定它的locations屬性指明應用程式配置檔案的路徑。例如,以下清單6的程式碼就是指明配置在構建路徑(Build Path)的根目錄下的config.properties檔案裡。

 

1 <bean id="propertyConfigurer" 
2       class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
3    <property name="locations">
4    <list>
5       <value>classpath:config.properties</value>
6    </list>
7    </property>
8 </bean>

清單 6. Spring配置程式碼片段

最後,在XML中定義Bean時,使用${xxx}引用配置資源來初始化物件,如清單1所示。然而這種配置方式僅限於XML,如果我們需要在通過Annotation定義的業務性的Bean中使用配置資源呢?

 

實現通過Annotation向Bean注入配置資源

 

解決上述問題的思路很簡單。首先,參考Spring注入Bean的Annotation(如@Resource等)編寫一個類似的Annotation類,如下清單7所示。

 

 1 package net.blogjava.max.spring;
 2 
 3 import java.lang.annotation.ElementType;
 4 import java.lang.annotation.Retention;
 5 import java.lang.annotation.RetentionPolicy;
 6 import java.lang.annotation.Target;
 7 
 8 @Retention(RetentionPolicy.RUNTIME)
 9 @Target(ElementType.FIELD)
10 public @interface Config {
11    String value() default "";
12 }

清單 7. Config.java

上述Config類有隻一個屬性,所以用預設的“value”作為名稱,而且此屬性是可選的,換而言之,開發者可以通過@Config("配置名稱")或 簡單地直接使用@Config來注入配置資源。當程式發現@Config的value為空時,會使用變數域(Field)的名稱作為配置名稱獲取其值。

 

然後,通過上節配置的propertyConfigurer物件獲取配置資源。不過通過閱讀Spring的API文件或 org.springframework.beans.factory.config.PropertyPlaceholderConfigurer的原始碼,筆者發現此物件並沒有一個公共方法可以滿足以上需求,但是它有一個受保護的方法,protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException,作用是將從配置中讀入的配置資源應用到Bean的生產工廠物件中。因此,我們可以繼承此類,然後改寫該方法,將引數 props的引用放到類的全域性變數裡,接著通過它提供一個公共方法返回對應名稱的配置資源,如下清單8所示。

 

 1 package net.blogjava.max.spring;
 2 
 3 import java.util.Properties;
 4 
 5 import org.springframework.beans.BeansException;
 6 import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
 7 import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
 8 
 9 public class ExtendedPropertyPlaceholderConfigurer extends
10       PropertyPlaceholderConfigurer {
11    private Properties props;
12 
13    @Override
14    protected void processProperties(
15          ConfigurableListableBeanFactory beanFactory, Properties props)
16          throws BeansException {
17       super.processProperties(beanFactory, props);
18       this.props = props;
19    }
20 
21    public Object getProperty(String key) {
22       return props.get(key);
23    }
24 }

清單 8. ExtendedPropertyPlaceholderConfigurer.java

最後,我們需要通過實現Spring的某此生命週期回撥方法,在Bean例項化之後將配置資源注入到標記有@Config的變數域(Field)中。通過 閱讀Spring的API文件,筆者發現 org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor 介面的方法boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException非常符合我們的需求,而且Spring的@Autowire就是通過實現此方法工作的。當然,在此大家已經可以著手編寫該接 口的實現類了。不過,由於該介面還不少其它方法,而這些方法跟我們的目標是毫無瓜葛的,直接實現它就不得不被迫編寫一堆空的實現程式碼,所以筆者選擇繼承 org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter 虛基類,改寫其postProcessAfterInstantiation方法。該虛基類是提供了一些介面(當然其中包括 InstantiationAwareBeanPostProcessor)的空實現,因此開發者只需改寫自己需要的方法即可,如下清單9所示。

 

 1 package net.blogjava.max.spring;
 2 
 3 import java.lang.reflect.Field;
 4 import java.lang.reflect.Modifier;
 5 
 6 import org.springframework.beans.BeansException;
 7 import org.springframework.beans.SimpleTypeConverter;
 8 import org.springframework.beans.factory.annotation.Autowired;
 9 import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
10 import org.springframework.stereotype.Component;
11 import org.springframework.util.ReflectionUtils;
12 
13  @Component //定義一個匿名Spring元件
14 public class ConfigAnnotationBeanPostProcessor extends
15       InstantiationAwareBeanPostProcessorAdapter {
16    @Autowired //自動注入  ExtendedPropertyPlaceholderConfigurer物件,用於獲取配置資源
17    private ExtendedPropertyPlaceholderConfigurer propertyConfigurer;
18 
19    //建立簡單型別轉換器
20    private SimpleTypeConverter typeConverter = new SimpleTypeConverter();
21 
22    @Override
23    public boolean postProcessAfterInstantiation(final Object bean, String beanName) 
24          throws BeansException {
25       ReflectionUtils.doWithFields(bean.getClass(), new ReflectionUtils.FieldCallback() {
26          public void doWith(Field field) throws IllegalArgumentException, 
27                IllegalAccessException {
28             Config cfg = field.getAnnotation(Config.class);
29             if (cfg != null) {
30                if (Modifier.isStatic(field.getModifiers())) {
31                   throw new IllegalStateException("@Config annotation is not supported 
32                            on static fields");
33                }
34 
35             //如果開發者沒有設定@Config的 value,則使用變數域的名稱作為鍵查詢配置資源
36             String key = cfg.value().length() <= 0 ? field.getName() : cfg.value();
37             Object value = propertyConfigurer.getProperty(key);
38 
39             if (value != null) {
40                //轉換配置值成其它非String型別
41                Object _value = typeConverter.convertIfNecessary(value, field.getType());
42                //使變數域可用,並且轉換後的配置值注入其中
43                ReflectionUtils.makeAccessible(field);
44                field.set(bean, _value);
45             }
46          }
47       }
48    });
49 
50    //通常情況下返回true即可
51    return true;
52    }
53 }

清單 9. ConfigAnnotationBeanPostProcessor.java

@Config使用示例

 

完成了上述步驟之後,下面我們用一個完整的例子來演示一下@Config的使用。首先,建立配置檔案,如下清單10所示。

 

1 demo.config1=Demo Config \#1
2 config2=314159

清單 10. src/config.properties

接著,編寫Demo類,它將演示通過XML和Annotation的方式獲取配置檔案的資源。如下清單11所示。

 

 1 package net.blogjava.max.spring;
 2 
 3 import org.springframework.context.ApplicationContext;
 4 import org.springframework.context.support.ClassPathXmlApplicationContext;
 5 import org.springframework.stereotype.Service;
 6 
 7 @Service("demoAnn")//通過Annotation的方式定義Bean
 8 public class Demo {
 9    @Config("demo.config1"//演示最常見的用法
10    private String config1;
11 
12    @Config //演示通過域變數名字獲取配置資源和資料型別轉換
13    private Integer config2;
14 
15    //演示通過XML方式注入配置資源
16    private String config3;
17    private Integer config4;
18 
19    public void setConfig3(String config3) {
20       this.config3 = config3;
21    }
22 
23    public void setConfig4(Integer config4) {
24       this.config4 = config4;
25    }
26 
27    public void printConfigAnn() {
28       System.out.println("{ config1 = " + config1 + ", config2 = " + config2
29       + "}");
30    }
31 
32    public void printConfigXML() {
33       System.out.println("{ config3 = " + config3 + ", config4 = " + config4
34       + "}");
35    }
36 
37    public static void main(String[] args) {
38       ApplicationContext appCtx = new ClassPathXmlApplicationContext(
39             "applicationContext.xml");
40 
41       Demo demoAnn = (Demo) appCtx.getBean("demoAnn");
42       demoAnn.printConfigAnn();
43 
44       Demo demoXML = (Demo) appCtx.getBean("demoXML");
45       demoXML.printConfigXML();
46    }
47 }

清單 11. Demo.java

由於本示例的目的是演示@Config的使用,所以採取了最簡單編碼風格,而並非大家使用Spring時常用的基於介面的編碼風格。另外,本示例同時通過 XML和Annotation的方式在Spring中定義Demo型別的Bean,前者通過類中的XML和兩個Setter注入配置資源,後者則是通過 Annotation和兩個私有域變數。

 

最後,編寫Spring的XML配置檔案,如清單12所示。

 

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 
 3 <beans xmlns=http://www.springframework.org/schema/beans
 4    xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
 5    xmlns:context=http://www.springframework.org/schema/context
 6    xsi:schemaLocation="http://www.springframework.org/schema/beans 
 7       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
 8       http://www.springframework.org/schema/context 
 9       http://www.springframework.org/schema/context/spring-context-2.5.xsd">
10 
11    <!-- 指明需要進行Annotation掃描的包 -->
12    <context:component-scan base-package="net.blogjava.max" />
13 
14    <!-- 讀入配置檔案  -->
15    <bean id="propertyConfigurer"
16          class="net.blogjava.max.spring.ExtendedPropertyPlaceholderConfigurer">
17       <property name="locations">
18          <list>
19             <value>classpath:config.properties</value>
20          </list>
21       </property>
22    </bean>
23 
24    <!-- 通過XML配置Demo的Bean,並注入配置資源 -->
25    <bean id="demoXML" class="net.blogjava.max.spring.Demo">
26       <property name="config3" value="${demo.config1}" />
27       <property name="config4" value="${config2}" />
28    </bean>
29 
30 </beans>

清單 12. src/applicationContext.xml

完成了配置之後,大家可以執行Demo類的main方法,控制檯會有如清單13的輸出。這就證明了通過XML或Annotation可以注入相同配置資源,而且對比兩者的程式碼,Annotation比XML更為簡便和快捷。

 

1 { config1 = Demo Config #1, config2 = 314159}
2 { config3 = Demo Config #1, config4 = 314159}

清單 13. 示例控制檯輸出

結束語

 

本文再三強調定義Bean時Annotation對比XML的優越性,尤其是針對業務性的物件;而配置又是每個應用程式必不可少的一部分,通過擴充套件 Spring框架,開發者可以輕鬆地使用Annotation的方式實現應用程式配置。同時,筆者也希望Spring社群能夠意識到這方面的需求,將其整 合在以後發行的Spring版本之中。在此之前,大家可以通過文章後面的下載連結獲得本文Eclipse工程的壓縮包檔案,執行示例或者將程式碼應用到您的 工程之中。該程式碼不受任何版權保護,可以隨便修改或釋出。

 

相關文章