前言:橫切關注點的模組化方案
物件導向擅長**垂直領域建模**(業務實體),但**日誌、事務、安全、效能監控、快取**等橫切關注點會散佈在各業務類別中,造成**代碼糾纏、重複、難維護**。AspectJ 作為 Java 生態最成熟的 AOP 實現,**以編譯期/類載入期織入**實現零侵入橫切邏輯分離。本文系統介紹核心概念、Eclipse AJDT 環境搭建與完整示例。
一、AspectJ 核心概念速查
| 術語 | 定義 | 對應程式碼元素 |
| Join Point (連接點) | 程式執行中可定位的具體點 | 方法呼叫、欄位存取、異常處理、建構子執行... |
| Pointcut (切點) | 匹配一組連接點的謂語 | execution(* com.service.*.*(..)) |
| Advice (通知/增強) | 在切點處執行的動作 | before(), after(), around() |
| Aspect (切面) | 切點 + 通知的模組化封裝 | @Aspect 類別 |
| Inter-type Declaration (類型間宣告) | 靜態修改類別結構 | 為現有類別新增欄位/方法/介面實作 |
| Weaving (織入) | 將切面應用到目標類別的過程 | 編譯期、類載入期、運行期 |
Advice 類型與執行時機
@Aspect
public class LoggingAspect {
// 前置通知:目標方法執行前
@Before("pointcutExpr")
public void before(JoinPoint jp) { ... }
// 後置通知:目標方法正常返回後
@AfterReturning(pointcut = "pointcutExpr", returning = "result")
public void afterReturning(Object result) { ... }
// 例外通知:目標方法拋出異常後
@AfterThrowing(pointcut = "pointcutExpr", throwing = "ex")
public void afterThrowing(Exception ex) { ... }
// 最終通知:無論正常/異常都執行(類似 finally)
@After("pointcutExpr")
public void after() { ... }
// 環繞通知:完全控制目標方法執行(可攔截、修改參數/返回值、決定是否執行)
@Around("pointcutExpr")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 前置邏輯
Object result = pjp.proceed(); // 執行目標方法
// 後置邏輯
return result;
}
}
二、Eclipse AJDT 開發環境搭建
- **Eclipse Marketplace** 搜尋 AJDT (AspectJ Development Tools) 安裝。
- 或 **Help → Install New Software** → 添加更新站點:
https://download.eclipse.org/tools/ajdt/4.10/update (對應 Eclipse 版本)。
- 重啟 Eclipse 後,**New → Other → AspectJ → AspectJ Project**。
- 專案自動包含
aspectjrt.jar,編譯器為 ajc(而非 javac)。
三、完整示例:幾何圖形繪製的效能監控切面
1. 領域介面與實作
package aop.test;
public interface FigureElement {
void setXY(int x, int y);
void draw();
}
package aop.test;
public class Point implements FigureElement {
public int x;
private int y;
public int getX() { return x; }
public String setX(int x) {
System.out.println("設定 x 值:x=" + x);
this.x = x;
return "返回值是 x=" + x;
}
public int getY() { return y; }
public void setY(int y) { this.y = y; }
public Point() {}
public Point(int x, int y) { this.x = x; this.y = y; }
@Override public void setXY(int x, int y) {
this.x = x; this.y = y;
System.out.println("Point setXY: x=" + x + ", y=" + y);
}
@Override public void draw() { System.out.println("Point draw"); }
@Override public String toString() { return "Point: x=" + x + ", y=" + y; }
}
2. 切面定義:監控所有 FigureElement 方法執行時間
package aop.test;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class PerformanceMonitorAspect {
// 切點:所有 FigureElement 實作類的公共方法
@Pointcut("execution(public * aop.test.FigureElement+.*(..))")
public void figureElementMethods() {}
// 環繞通知:測量執行時間
@Around("figureElementMethods()")
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed(); // 執行目標方法
} finally {
long elapsed = System.nanoTime() - start;
String method = pjp.getSignature().toShortString();
System.out.printf("[AOP] %s 執行耗時: %,d ns (%.3f ms)%n", method, elapsed, elapsed / 1e6);
}
}
// 前置通知:記錄方法進入
@Before("figureElementMethods()")
public void logEntry(JoinPoint jp) {
System.out.println("[AOP] 進入方法: " + jp.getSignature().getName());
}
// 後置通知:記錄方法退出
@After("figureElementMethods()")
public void logExit(JoinPoint jp) {
System.out.println("[AOP] 退出方法: " + jp.getSignature().getName());
}
}
3. 測試主程式
package aop.test;
public class Main {
public static void main(String[] args) {
FigureElement point = new Point(10, 20);
point.draw();
point.setXY(30, 40);
System.out.println("--- 呼叫有返回值方法 ---");
String ret = point.setX(100);
System.out.println("返回: " + ret);
}
}
4. 執行輸出(經 ajc 編譯織入後)
[AOP] 進入方法: draw
Point draw
[AOP] 退出方法: draw
[AOP] draw 執行耗時: 12,345 ns (0.012 ms)
[AOP] 進入方法: setXY
Point setXY: x=30, y=40
[AOP] 退出方法: setXY
[AOP] setXY 執行耗時: 8,901 ns (0.009 ms)
--- 呼叫有返回值方法 ---
[AOP] 進入方法: setX
設定 x 值:x=100
[AOP] 退出方法: setX
[AOP] setX 執行耗時: 15,678 ns (0.016 ms)
返回: 返回值是 x=100
四、Pointcut 表達式語法速查
| 模式 | 含義 | 範例 |
execution(modifiers? ret-type declaring-type?name(param-pattern)) | 方法執行連接點 | execution(public * *(..)) |
within(type-pattern) | 指定類型內的所有連接點 | within(com.service.*) |
this(type-pattern) | 代理物件類型匹配 | this(com.service.UserService) |
target(type-pattern) | 目標物件類型匹配 | target(com.service.UserService) |
args(param-pattern) | 參數類型匹配 | args(String, ..) |
@annotation(AnnotationType) | 方法帶有指定註解 | @annotation(Transactional) |
@within(AnnotationType) | 類別帶有指定註解 | @within(Component) |
&&, ||, ! | 邏輯組合 | execution(* *(..)) && within(com..) |
五、AspectJ vs Spring AOP:選型指南
| 維度 | AspectJ | Spring AOP |
| 織入時機 | 編譯期 (ajc) / 類載入期 (LTW) / 運行期 | 運行期 (動態代理) |
| 支援連接點 | **全部**:方法、建構子、欄位、靜態初始化、異常處理... | **僅方法執行** (Spring Bean 方法) |
| 目標物件 | 任意類別(含非 Spring Bean、final 類、靜態方法) | Spring 容器管理的 Bean |
| 效能 | 編譯期織入零運行開銷,LTW 單次類載入開銷 | 動態代理攔截開銷(反射/CGLIB) |
| 複雜度 | 需 ajc 編譯器或 Java Agent 配置 | 零配置,Spring Boot 自動裝配 |
| 適用場景 | 基礎設施級:日誌、追蹤、效能、安全、領域模型增強 | 業務級:事務、快取、權限、業務日誌 |
六、總結
- AspectJ 以 **Join Point / Pointcut / Advice / Aspect** 四大核心概念模組化橫切關注點。
ajc 編譯期織入提供**最佳效能與最完整連接點支援**。
- Eclipse AJDT 提供**視覺化切點匹配、織入報告、偵錯支援**,是 AspectJ 開發利器。
- **AspectJ 處理基礎設施橫切**,**Spring AOP 處理業務橫切**,兩者互補共存。
- 掌握 Pointcut 表達式是寫好切面的關鍵,**多用
within/@annotation 限定範圍,避免過度匹配**。
避坑指南:AspectJ 功能強大但「威力過大易濫用」。**切面應保持薄、專、穩**:薄(邏輯極簡)、專(單一職責)、穩(不頻繁變更)。業務邏輯留在業務類,切面只做「無感增強」。