使用 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 有相當的開銷,要小心過度使用帶來的性能問題。