面向切面程式設計 (AOP) 是極具潛力的新技術,但採用新技術有風險。最佳策略是沿著可管控風險的路徑採用。若將 AOP 用於策略執行與測試,切面不進入生產環境,不會危及程式碼穩定性,卻能協助開發出高品質軟體,同時也是學習 AOP 的絕佳途徑。
QA 目標非找出所有 Bug(不可能),而是提升對程式碼按預期運作的信心。單一測試方法遞減報酬,不同方法發現不同類型錯誤,將 QA 資源分散於測試、程式碼審查、靜態分析,報酬優於集中單一方法。
靜態分析工具 (如 FindBugs) 雖不精確(有誤報、漏報),但能發現真實 Bug,誤報率可接受時極具價值。AOP 驗證設計規則與靜態分析異曲同工:
public aspect GcAspect {
pointcut gcCalls() : call(void java.lang.System.gc());
before() : gcCalls() {
throw new AssertionError("Don't call System.gc!");
}
}
需實際執行到該呼叫點才能偵測,靈活性較高,可在切入點執行任意測試程式碼。
public aspect StaticGcAspect {
pointcut gcCalls() : call(void java.lang.System.gc());
declare error : gcCalls() : "Don't call System.gc!";
}
編譯期即可偵測,不需執行程式碼。
Swing 元件與模型僅應從事件派發執行緒 (EDT) 存取。此規則幾乎無法靜態強制,違規時程式表面可能正常,潛在嚴重執行緒安全問題。
利用 SwingUtilities.isEventDispatchThread(),在每個 Swing 方法呼叫前插入檢查碼,非 EDT 呼叫即拋出 AssertionError,測試階段即可捕捉違規。
public aspect SwingThreadAspect {
pointcut swingMethods() : call(* javax.swing..*.*(..)) || call(javax.swing..*.new(..));
pointcut extendsSwing() : call(* javax.swing.JComponent+.*(..))
|| call(* javax.swing..*Model+.*(..))
|| call(* javax.swing.text.Document+.*(..));
pointcut safeMethods() : call(void JComponent.revalidate())
|| call(void JComponent.repaint(..))
|| call(void SwingUtilities.invokeLater(Runnable))
|| call(void JTextPane.replaceSelection(..))
// ... 其他已知執行緒安全方法
;
pointcut edtMethods() : (swingMethods() || extendsSwing()) && !safeMethods();
before() : edtMethods() {
if (!SwingUtilities.isEventDispatchThread())
throw new AssertionError(thisJoinPointStaticPart.getSignature()
+ " called from " + Thread.currentThread().getName());
}
}
此切面不完美(安全方法列舉不全、extendsSwing 可能遺漏),但僅用於測試環境,無需生產級完美,即能以低成本發現難以手工測試的執行緒違規。
常見需求:類別的除錯版本(含詳細日誌、額外檢查)不適合生產,但手動切換建構子呼叫繁瑣易錯。AOP 提供無修改程式碼的切換機制。
public aspect ReentrantLockAspect {
pointcut newLock() : call(ReentrantLock.new());
pointcut newLockFair(boolean fair) : call(ReentrantLock.new(boolean)) && args(fair);
ReentrantLock around() : newLock() { return new DebuggingLock(); }
ReentrantLock around(boolean fair) : newLockFair(fair) { return new DebuggingLock(fair); }
}
DebuggingLock 每次請求鎖時執行死結檢測(Java SE 6 可透過 ThreadMXBean),效能較差但能更快發現死結。另一版本 OrderHistoryLock 記錄鎖獲取順序歷史,偵測不一致鎖順序(最常見死結成因),空間開銷較大但涵蓋面更廣。
本文示範的屬於「策略執行切面」。無論是應用程式設計策略(如「此方法僅允許從類別 X 呼叫」)或 API 契約(如 Swing 單執行緒規則、EJB 限制),皆可在開發測試階段以切面驗證合規性。無論生產環境是否採用 AOP,它都是測試工具箱中的利器。