在 JBoss Seam 使用 AOP 作 logging / auditing

使用 JBoss Seam 2.0 ,我們可以利用 Annotation 和 Interceptor 去作 AOP 去減低組件間的耦合。最適合用來處理一些非核心、但容易分佈各處的功能。

目標
我們的軟件需要記錄登入用戶後的工作以作審計用途。系統有很多的組件,每個組件有若干的功能。當用戶使用某功能的時候,我們希望可以記錄該用戶的資料、使用時間、哪一個功能、影響了甚麼資料等等。

第一個答案
最簡單的作法就是寫一個獨立的 Audit 組件,再在每個功能之後使用它:

FundHome.java
@Name("fundHome")
public class FundHome extends EntityHome<Fund> {
    public String update() {
        String result = null;
        try {
            // update data
            // ....
        } finally {
            audit.log("update user account: #0, #1, #2", currentUser, oldFund, newFund);
        }
        return result;
    }
    // ...
}
雖然這可以解決問題,然而這樣有關 Audit 的邏輯就混在業務邏輯之中。如果有六十個這種要記錄的功能,就要重覆這種沒個性的代碼 60 次,真痛苦,更別說當中可能有人為出錯和其後果了。本著 DRY ( Dont Repeat Yourself ) 的精神,我們可以用 Seam 的 Interceptor 去簡化這工作。


Seam Interceptor
EJB 3.0 有 interceptor 的標準。任何 class 只要在其 method 上加上 @AroundInvoke 的標記,Container 就會知道這是個 interceptor。想在執行某動作的前後執行這 interceptor,只需在目標方法前加入 @Interceptors 標記以及 interceptor 的 class。

AuditInterceptor.java
@Name("auditInterceptor")
public class AuditInterceptor {
    protected static Log log = Logging.getLog(AuditInterceptor.class);
    @AroundInvoke
    public Object audit(InvocationContext context) throws Exception {
        Object result = null;
        try {
            result = context.proceed();
        } finally {
            User user = (User) Contexts.getSessionContext().get("user");
            log.info("[#0][#1][#2]", 
                context.getClass().getName(), 
                context.getMethod().getName(), 
                user==null? 
                    "not logged in" : 
                    user.getLogin());
        }
        return result;
    }
}


FundHome.java
@Name("fundHome")
public class FundHome extends EntityHome<Fund> {
    @Interceptors(AuditInterceptor.class)
    public String update() {
        // original update data
        // ....
    }
}
注意 Contexts.getSessionContext().get(”user”),這是 Seam 去存取 Session Context 的方法。。Interceptor 可以直接存取所有目標的 context,這已足夠處理大部份的應用了。就這樣只需加一個 annotation 就可以做到原本的效果,是否比較漂亮?

EJB3.0 還支援用在 ejb-jar.xml 中定義 Interceptors,JBoss Seam 本身就是用 Interceptor 去運作的:

ejb-jar.xml
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns="http://java.sun.com/xml/ns/javaee" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"
         version="3.0">
         
   <interceptors>
      <interceptor>
         <interceptor-class>org.jboss.seam.ejb.SeamInterceptor</interceptor-class>
      </interceptor>
   </interceptors>
   
   <assembly-descriptor>
      <interceptor-binding>
         <ejb-name>*</ejb-name>
         <interceptor-class>org.jboss.seam.ejb.SeamInterceptor</interceptor-class>
      </interceptor-binding>
   </assembly-descriptor>   
</ejb-jar>

Meta-Annotation as Interceptor
Seam Interceptors 基於 EJB3 之上再加了一些改良。首先你不單可以在 EJB3 中用 Seam Interceptor,Seam 讓普通的 JavaBean 也可以有同樣功能。其次,進一步 DRY ,Seam 支援用 meta-annotation 去定義 class level interceptor:

Auditor.java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Interceptors(AuditInterceptor.class)
public @interface Auditor {}
FundHome.java
@Name("fundHome")
@Auditor
public class FundHome extends EntityHome<Fund> {
    @Interceptors(AuditInterceptor.class)
    public String update() {
        // original update data
        // ....
    }
}

這就等於在 FundHome 中加入 class level interceptor: @Interceptors(AuditInterceptor.class),除了省了打幾個字外,這還讓源碼更加易讀。(注:這個似乎只在 EJB3 才行,在 JavaBean 中要稍為改動,詳情請參考這篇。)

加入更多自定的資料
由於 Audit 需要監察很多不同功能,怎樣用一個 Interceptor 處理和記錄不同的資料呢?比如新增用戶時我想知道新增的帳戶名稱、轉賬時想知道過數的銀碼等等。按 Seam 的思路,我會用另一個 Annotation 去定義這些資料:

AuditableAction.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditableAction {    
    String description() default "";
    String module() default "Unknown";
    String action() default "Unknown";    
}

更新後的 AuditInterceptor.audit() 方法
@AroundInvoke
public Object audit(InvocationContext context) throws Exception {
    Object result = null;
    try {
        result = context.proceed();
    } finally {
        if (context.getMethod().isAnnotationPresent(AuditableAction.class)) {
            ActionLogHome actionLogHome = (ActionLogHome) Component.getInstance("actionLogHome");
            AuditableAction auditableAction = context.getMethod().getAnnotation(AuditableAction.class);                
            Interpolator interpolator = (Interpolator) Component.getInstance("org.jboss.seam.core.interpolator");
            ActionLog actionLog = actionLogHome.getInstance();

            User user = (User) Contexts.getSessionContext().get("user");
            String module = StringUtils.left(auditableAction.module(), 64);
            String action = StringUtils.left(auditableAction.action(), 64);                    
            if (StringUtils.isNotEmpty(auditableAction.details())){
                String details = interpolator.interpolate(auditableAction.details());
                actionLog.setDetails(details);
            }
            actionLog.setModule(module);
            actionLog.setAction(action);
            actionLog.setUser(user);
            actionLogHome.persist();
       }
    }
    return result;
}

ActionLog 是個 Entity Bean。
ActionLogHome 是標源的 EntityHome ,是生產 ActionLog 的地方。
Interpolator 是 Seam 的標準組件,可以 evaluate Seam 的 EL Expression。
FundHome.java
@Name("fundHome")
@Auditor
public class FundHome extends EntityHome<Fund> {
    @AuditableAction(module="Fund", 
            action="Update", 
            details="fund.id=#{fundHome.instance.id}")    
    public String update() {
        // ... update logic ...
    }
}

利用 annotation 我們可以定義任何的資料,包括 EL Expression。以上的例子就讓我們可以讓 log 記錄獨立的訊息,在 log 前會把 EL Expression 解讀回我們想要的資料。

結語
剛才我們利用 Seam Interceptor 去處理分佈各處的非核心功能。比起用 AspectJ 用另一個檔案和另一種語法去改動程式, Seam Interceptor 較易使用和維護。然而要注意的是 Interceptor 有相當的開銷,要小心過度使用帶來的性能問題。

你可能感兴趣的:(java,AOP,jboss,ejb,seam)