12 October 2006

今天終於搞定將 txProxyTemplate 移植到 aop:config。原本常見的 transaction 設定是用 txProxyTemplate:

<bean id="txProxyTemplate" abstract="true"
  class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
  <property name="transactionManager">
    <ref local="transactionManager" />
  </property>
  <property name="transactionAttributes">
    <props>
      <prop key="save*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop>
      <prop key="update*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop>
      <prop key="delete*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop>
      <prop key="remove*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop>
      <prop key="tx*">PROPAGATION_REQUIRED, -org.bioinfo.util.BusinessException</prop>
      <prop key="*">PROPAGATION_REQUIRED,readOnly, -org.bioinfo.util.BusinessException</prop>
    </props>
  </property>
  <property name="preInterceptors">
    <list>
      <ref bean="operationLogInterceptor"/>
    </list>
  </property> 
</bean>
<bean id="operationLogInterceptor" scope="singleton"
  class="org.bioinfo.util.OperationLogInterceptor">
</bean>

上面除了一般 transaction 設定外,我還加了一專門做 log 的 interceptor。而一般 service 只要直接套用就好了:

<bean id="orderService" parent="txProxyTemplate">
  <property name="target">
    <bean class="bendon.order.impl.OrderServiceImpl">
      <property name="shareOrderDAO" ref="shareOrderDAO" />
    </bean>
  </property>
</bean>

ok, 開始 migrate,第一個步驟是將 aspectjweaver.jar 放進 classpath 裡 (在 spring-framework/lib/aspectj 裡) 有了這個才能在 spring 啟動時 weave aspect。建一個 applicationContext-aop.xml:

<?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: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-2.0.xsd 
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd 
       default-lazy-init="true">

  <aop:aspectj-autoproxy />
  
  <aop:config>
    <aop:pointcut id="txServiceFacades" 
        expression="execution( public * bendon..*ServiceImpl.*(..) )" />

    <aop:aspect id="operationLogAspect" ref="operationLogAdvice">
      <aop:around method="log" pointcut-ref="txServiceFacades"/>
    </aop:aspect>
  </aop:config>

  <aop:config>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txServiceFacades"/>
  </aop:config>
  
  <tx:advice id="txAdvice">
    <tx:attributes>
      <tx:method name="save*" rollback-for="org.bioinfo.util.BusinessException" />
      <tx:method name="update*" rollback-for="org.bioinfo.util.BusinessException" />
      <tx:method name="delete*" rollback-for="org.bioinfo.util.BusinessException" />
      <tx:method name="remove*" rollback-for="org.bioinfo.util.BusinessException" />
      <tx:method name="tx*" rollback-for="org.bioinfo.util.BusinessException" />
      <tx:method name="*" rollback-for="org.bioinfo.util.BusinessException" read-only="true" />
    </tx:attributes>
  </tx:advice>
    
  <bean id="operationLogAdvice" class="org.bioinfo.util.log.OperationLogAdvice" scope="singleton">
  </bean>
</beans>

最上面的 <aop:aspectj-autoproxy> 會替需要 weave aspect 的 bean 自動加 proxy。接下來定義 point cut: "txServiceFacades" 顧名思意,這個 point cut 包含所有的 需要 transactoin 的 service facade。這裡我的 point cut 語法是:

execution( public * bendon..*ServiceImpl.*(..) )

這表示在 bendon 以下的所有 package,其 class 名稱以 ServiceImpl 為結尾的class 中,所有的 public method (無論參數和回傳為何)。這是我專案中唯一找的出來的 pointcut 了。如果一開始就用 package 隔開就會穩當的多,例如像這樣 execution( public * bendon.service..*.*(..)) 表示 bendon.service 以下的所有 class 的 public method。

接下來看第二個 aop:config ,裡面有一個 advisor -- 設定成當遇到 txServiceFacades pointcut 時,便會執行 "txAdvice" 所定義的指令。txAdvice 應該就很簡單了,跟 txProxyTemplate 裡的 transactionAttributes 長的很像,用意也是一樣的。整個 advisor 我們可以簡短解釋成-- 當程式通過 *ServiceImpl 的 public method 時,會依 method 名稱啟動相對應的 transaction 設定。

回到第一個 aop:config,下方有另一個 aspect: "operationLogAspect",它的目的是記錄 method 的 invoke 和 return。為此,我們設計了一個"operationLogAdvice" 的 bean,內部實做以 log4j 記錄,並將它設為 aspect 的 advice。而 pointcut 我們還是套用 txServiceFacades pointcut,因為有 transaction 的操作是我們所關心的,要多加 log。最後設定當遇到 pointcut 時,執行 operationLogAdvice 的 "log" method。

在這個設定檔裡,operationLogAdvice 在 txAdvice 之前,因此在同一個 pointcut 上 operationLogAdvice 會先執行,然後才執行 txAdvice。OK,經過 aspectj 的加持,我們現在 txProxyTemplate 可以簡化成:

<bean id="orderService" class="bendon.order.impl.OrderServiceImpl">
   <property name="shareOrderDAO" ref="shareOrderDAO" />
</bean>

呃... 少了幾個字,變得比較乾淨一點... 好像... 沒什麼了不起... 至於速度上:

  txProxyTemplate aop:config
總 bean 數 39 44
Proxied 的 bean 數 8 8
loading 費時: ~9 sec ~11 sec

哇咧,loading 出全部的 bean 還變慢... hhmmm... 這到底值不值得?不過總算是又減少了一些 xml 的設定,算是不壞吧。不過我最想要的是 service facade 和 DAO 的 xml 變成:

 
    
    

是的~~ 一片空白,zero xml!這應該是最高境界吧~~ 要達成這個邪惡的目標就得自己寫 annotation 和客製 BeanFactory 了。有空再來試試吧~

最後附上 operationLogAdvice 給大家參考:

/**
 * @author ingram
 * 
 */
public class OperationLogAdvice {

    private final static Logger logger = Logger
            .getLogger(OperationLogAdvice.class);

    // do not declare this as static (out of memory error due to classloader)
    private final ThreadLocal<RuntimeException> loggedRuntimeException = new ThreadLocal<RuntimeException>();

    private int maxArgumentLength = Integer.MAX_VALUE;

    public Object log(ProceedingJoinPoint pjp) throws Throwable {

        if (pjp == null) {
            if (logger.isDebugEnabled()) {
                logger.debug("[invoke] null");
            }
            return null;
        }

        Logger targetLogger = Logger.getLogger(pjp.getTarget().getClass());

        if (targetLogger.isDebugEnabled()) {
            targetLogger.debug("[invoke] " + pjp.getSignature().getName() + " "
                    + buildArgumentsString(pjp.getArgs()));
        }

        try {
            Object o = pjp.proceed();
            if (targetLogger.isDebugEnabled()) {
                targetLogger.debug("[ done ] " + pjp.getSignature().getName()
                        + " return: " + buildReturnString(o));
            }
            return o;
        } catch (RuntimeException e) {
            if (loggedRuntimeException.get() == null) {
                targetLogger.error(e.getMessage(), e);
                loggedRuntimeException.set(e);
            }
            throw e;
        }

    }

    private String buildReturnString(Object o) {
        if (o == null) {
            return null;
        }
        String all = o.toString();
        if (all.length() > maxArgumentLength) {
            return all.substring(0, maxArgumentLength) + "...trimed";
        }
        return all;
    }

    private String buildArgumentsString(Object[] args) {
        String all = Arrays.toString(args);
        if (all.length() > maxArgumentLength) {
            return all.substring(0, maxArgumentLength) + "...trimed]";
        }
        return all;
    }

    public void setMaxArgumentLength(int length) {
        this.maxArgumentLength = length;
    }
}

使用方法是在 log4j.xml 裡加上

<category name="foo.bar"><priority value="DEBUG" /></category>

一行即可。foo.bar 則是要 log 的 package 名稱

[Update] <aop:aspectj-autoproxy /> 這個 tag 只有在 使用 @AspectJ 時才會用到,如果沒用可以拿掉