14 October 2006

上一次 的 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 會在 建立好 BeanDefinitioninstantiate 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 就是 我們專案裡粹練出來,用來簡化的小磁鐵。


回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>