上一次 的 aop 移植裡,我提到想要將 spring xml 減化的構想,像下面的設定檔:
<bean id="projectDAO" class="foo.project.ProjectDAO" >
<property name="hibernateTemplate" ref="hibernateTemplate" />
</bean>
<bean id="documentDAO" class="foo.doc.DocumentDAO" >
<property name="hibernateTemplate" ref="hibernateTemplate" />
</bean>
<!-- documentService 已經用 aop 自動加上 transaction -->
<bean id="documentService" class="foo.doc.DocumentServiceImpl">
<property name="documentDAO" ref="documentDAO" />
<property name="projectDAO" ref="projectDAO" />
</bean>
這樣 千篇一律的 DAO/ServiceImpl 的設定檔大家應該不漠生吧?現在我們來完全移除它,改以 annotation 的方式取代。 在進行之前,先聲明這樣的做法是 邪惡的。因為它違反了 Spring 的基本理念 -- 將 configuration 完全抽離 POJO。未來 Spring 可能也不會提類似的 solution,所以這些自創的 code 可能會增加維護上的負擔。
廢話講完了,開始吧。我們得想個法子能夠以程式的方式註冊 (register) 自己的 bean,subclass XmlApplicationContext 可能是其中之一的做法。不過我選擇比較簡單的 BeanFactoryPostProcessor。implement 這個 interface 的 bean, Spring 會在 建立好 BeanDefinition 和 instantiate bean 之間時呼叫 postProcessBeanFactory() 。換句話說時間點在讀完設定檔後,實際 new bean 之前。
package foo.util.spring;
public class AnnotatedBeanRegister implements BeanFactoryPostProcessor, Ordered {
//於讀完 xml 和 new bean 的時間點呼叫
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory)
//正常來講由 xml 來的 beanFactory 都有 BeanDefinitionRegistry interface
final BeanDefinitionRegistry registry = (BeanDefinitionRegistry) factory;
//手動 register Bean Definition.....
for (String className : findAnnotatedClassNames()) {
Class clazz = ClassUtils.forName(className);
registerTxService(clazz, registry);
registerDAO(clazz, registry);
}
//ps. 省略 exception 處理的 code
}
//設定這個 Processor 為最優先執行
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
AnnotatedBeanRegister 除了 implements BeanFactoryPostProcessor 之外,還多加 Ordered,有了這個就 能指定 Processor 執行優先順序。手動註冊 Bean 是讀完 xml 後馬上要做的第一件事,所以我設 HIGHEST_PRECEDENCE。
至於 postProcessBeanFactory() 裡,我們做三件事:
- findAnnotatedClassNames() 查出所有在 classpath 底下的 class 名稱
- 如果 class 具備 @TxService 這個 annotation,便利用 BeanDefinitionRegistry 註冊一個 bean 進去
- 如果 class 具備 @DAO 這個 annotation,也註冊一個 bean
findAnnotatedClassNames() 我用的是笨方法... 等一下在說。先看看 registerDAO():
private void registerDAO(Class<?> clazz, BeanDefinitionRegistry registry) {
//取得 @DAO annotation
DAO daoAnnot = clazz.getAnnotation(DAO.class);
if (daoAnnot == null) return;
//如果 @DAO 上沒有定義 id,則用 class 名稱轉換
String id = daoAnnot.id();
if (id == null || id.trim().length() == 0) {
id = ClassUtils.getShortNameAsProperty(clazz);
}
//id 若重覆丟 Exception
checkIdDuplication(registry, id);
//建立一個 BeanDefinition
final RootBeanDefinition beanDef = new RootBeanDefinition();
//設為 Autowire.BY_NAME
beanDef.setAutowireMode(Autowire.BY_NAME.value());
//設定 class
beanDef.setBeanClass(clazz);
//萬事俱備,註冊到 BeanDefinitionRegistry (beanFactory)
registry.registerBeanDefinition(id, beanDef);
}
// @DAO 的定義:
@Target( { ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface DAO {
//有一個預設的 id 可以寫
public String id() default "";
}
ok,上面的 step by step 的說明應該很清楚了。凡是 class 上有註記 @DAO 者,預設以 class 的名稱為 bean name 、並依據名稱來做 Autowire,然後註冊該 class 到 BeanFactory 裡。同樣的做法可以套用在 @TxService,唯一 的差別是預設的 id 是用 class 的 interface 名稱來轉,這裡就不再贅述。
接下來是討厭的 findAnnotatedClassNames(),它要找出在 class path 裡的所有 class 名稱,這可難倒我了... 目前我找到的方法是利用掃出實際的 .class 檔案來反推 class 名稱:
private List<String> findAnnotatedClassNames() {
List<String> classNames = new ArrayList<String>();
//利用 ClassPathResource 找出 root class path 的 絕對路徑
String rootClassPath = new ClassPathResource("/").getURL().getPath();
//將 class file 的 path 轉換為 class name
for (Resource scanClassFile : scanClassFiles) {
//class file 的絕對路徑
final String classFileFullPath = scanClassFile.getFile()
.getPath();
//class file 路徑減去 root class path 即得 class 的相對路徑
final String relativePath = classFileFullPath.substring(
rootClassPath.length(), classFileFullPath.length()
- ".class".length());
//相對路徑轉換為 full qualified class name
classNames.add(relativePath.replace('/', '.'));
}
return classNames;
//ps. 省略 exception 處理的 code
}
//利用 Spring 的內建的 Editor 來查 class file
private Resource[] scanClassFiles;
public void setScanClassFiles(Resource[] scanClassFiles) {
this.scanClassFiles = scanClassFiles;
}
我們利用了 spring 內建的 Resource 來取得 class file,所以享有 wildcard 的好處:
<bean class="foo.util.spring.AnnotatedBeanRegister" lazy-init="false" >
<property name="scanClassFiles">
<value>classpath:/foo/**/*.class</value>
</property>
</bean>
按上面的設定,可以找到所有 package 為 foo 的 .class 檔,findAnnotatedClassNames() 再依據這些檔案路徑 轉換為 class name。這個實做我只在 tomcat 上試過而已,如果在比較複雜的 class loader 環境,或是想找 jar file 裡的 class 可能就比較難了... 若有人知道更好的做法,請救救我吧~~
最後,替我們的 DAO 和 ServiceFacade 加上 annotation
@DAO
public class ProjectDAO {//略...}
@DAO
public class DocumentDAO {//略...}
@TxService
public class DocumentServiceImpl implements DocumentService {//略...}
搞定!短短幾個字就 ok 啦!不需要再替這些 bean 做繁鎖的 xml 定義了。而我們的 poincut 也可進化成:
<aop:pointcut id="txServiceFacades" expression="@within(foo.util.spring.TxService) || execution( public * foo..*ServiceImpl.*(..) )
除了原本的 exeuction(ServiceImpl) 之外,還可以加上所有具備 @TxService 的 class。這樣就不用限定 package 或是 class 名稱的命名了。
總結:
- 優: 不需要寫額外重覆的 xml,符合 DRY 原理
- 優: 沒有 xml 等於可以任意的 refactoring class/package,改完不用再心裡發毛啦
- 優: 預設使用 AutoWire.BY_NAME,by name 較 by type 來有的彈性多,而且不易錯。
- 優: 可以寫出更穩當的 pointcut
- 優: xml 設定檔大幅減少,加快啟動速度。
- 優: 設定很固定,簡單易懂。
- 劣: 太自動化了,很難搞懂來龍去脈
- 劣: 客製 annontation 做法非 Spring 標準,長期維護吃力
- 劣: 設定上缺乏變化,沒彈性。
顯然優點多於缺點 :-) 那我們可不可以繼續強化 AnnotatedBeanRegister ,增加更多的設定可能性:
@TxService(id="documentService", autoWire="bytype" )
@Authorized(role="admin", projectOwner="true")
@Lonable(category="old, cheap")
public class DocumentServiceImpl implements DocumentService {//略...}
我個人想法是 No! 如果需要特別的設定,Spring xml 仍是最好的地方,不該在 class 上註記一大堆的有的沒的 ,看的人真的會眼花。這就像是你在營幕旁貼了滿滿的小黃標,即混亂又礙眼,也沒有頭緒可言,它們真的有用嗎? 遇到這種情況,你需要的是一本萬用記事本 (即 xml 檔) 好好整理,而不是 Post It。上面我們實做的 @TxService 和 @DAO 則都完全沒額外的設定,盡量簡化。這份簡化是來自於長期的經驗,就好比你常常在白板上寫一些公事 ,寫久了自然就會有一些雷同的例行公事反覆不斷出現,你可以買個黃、綠、紅的小磁鐵,每個顏色各自代表那 些常做的事,這樣既簡明又快速。個人認為客製的 annotation 也該以這樣方向來設計,@TxService 和 @DAO 就是 我們專案裡粹練出來,用來簡化的小磁鐵。