Spring AOP 從淺入深完整指南
面向切面程式設計 (AOP) 將橫切關注點 (事務、日誌、安全、快取) 模組化,降低業務代碼耦合。本文系統講解 AOP 核心術語、五大實現策略、Spring 代理機制 (JDK/CGLib)、編程式/聲明式增強、切點表達式、AspectJ 整合、自動代理、XML 與註解雙軌配置,附完整代碼範例。
一、核心術語 (AOP 詞彙表)
| 術語 | 定義 | 對應 Spring |
| Join Point (連接點) | 程式執行中可插入切面的點 (方法呼叫、異常拋出、欄位修改) | Spring 僅支援方法執行連接點 |
| Pointcut (切點) | 匹配一組連接點的謂語 (正則/表達式) | execution(* com..service.*.*(..)) |
| Advice (通知/增強) | 在切點執行的動作 (前/後/環繞/拋出/引入) | @Before, @AfterReturning, @Around, @AfterThrowing, @DeclareParents |
| Aspect (切面) | 切點 + 通知 的模組化封裝 | @Aspect 類 |
| Introduction (引入增強) | 為現有類動態添加方法/欄位/介面實現 | @DeclareParents, DelegatingIntroductionInterceptor |
| Weaving (織入) | 將切面應用到目標對象的過程 | 運行期動態代理 (Spring)、編譯期 (AspectJ)、類載入期 (JBoss AOP) |
| Target (目標對象) | 被代理的業務對象 | Spring Bean |
| Proxy (代理) | 織入增強後的對象 | JDK 動態代理 / CGLib 子類代理 |
二、AOP 五大實現策略對比
| 策略 | 原理 | 代表框架 | 優點 | 缺點 |
| JDK 動態代理 | 實現介面生成匿名類,反射調用 | Spring AOP (預設) | 標準 API、無第三方依賴 | 僅能代理介面、反射開銷 |
| CGLib 字節碼生成 | 繼承目標類生成子類,覆寫方法 | Spring AOP (proxyTargetClass=true) | 可代理類、效能優於反射 | 無法代理 final 類/方法、構造子雙調用 |
| 自定義類載入器 | 類載入時修改字節碼 | JBoss AOP | 可攔截構造子、欄位、靜態方法 | 複雜、需特殊啟動參數 |
| 編譯期織入 | 編譯階段修改 .class | AspectJ (ajc 編譯器) | 效能最好、支援所有連接點 | 需專用編譯器、構建流程複雜 |
| 語言擴展 | 擴展 Java 語法 (關鍵字 aspect) | AspectJ (.aj 檔案) | 語法豐富、可攔截構造子/欄位 | 非標準 Java、IDE 支援有限 |
三、Spring AOP 代理機制核心規則
- 目標類實現介面 → **預設 JDK 動態代理** (可強制 CGLib:
proxy-target-class="true")。
- 目標類未實現介面 → **強制 CGLib** (生成子類)。
@Transactional 等註解驅動的 AOP 同樣遵循此規則。
- 代理物件 只能轉型為介面 (JDK) 或類 (CGLib),不可混用。
四、編程式 vs 聲明式增強實戰
1. 編程式 (ProxyFactoryBean / ProxyFactory)
// 目標介面/類
public interface Greeting { void sayHello(String name); }
public class GreetingImpl implements Greeting {
public void sayHello(String name) { System.out.println("Hello, " + name); }
}
// 通知 (MethodInterceptor = 環繞通知)
public class LoggingInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("[Before] " + invocation.getMethod().getName());
Object ret = invocation.proceed(); // 執行目標方法
System.out.println("[After] " + invocation.getMethod().getName());
return ret;
}
}
// XML 配置
<bean id="greetingImpl" class="GreetingImpl"/>
<bean id="loggingInterceptor" class="LoggingInterceptor"/>
<bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="greetingImpl"/>
<property name="interceptorNames">
<list><value>loggingInterceptor</value></list>
</property>
</bean>
// 測試
Greeting proxy = (Greeting) ctx.getBean("greetingProxy");
proxy.sayHello("Spring"); // 輸出 Before/After 包裹
2. 聲明式 (Schema-based / @AspectJ)
// Schema-based XML (aop 命名空間)
<aop:config>
<aop:aspect ref="loggingAspect">
<aop:pointcut id="serviceMethods" expression="execution(* com..service.*.*(..))"/>
<aop:before pointcut-ref="serviceMethods" method="logBefore"/>
<aop:after-returning pointcut-ref="serviceMethods" method="logAfter"/>
<aop:around pointcut-ref="serviceMethods" method="logAround"/>
</aop:aspect>
</aop:config>
@Component("loggingAspect")
public class LoggingAspect {
public void logBefore(JoinPoint jp) { System.out.println("[Before] " + jp.getSignature()); }
public void logAfter(JoinPoint jp) { System.out.println("[After] " + jp.getSignature()); }
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("[Around Before]");
Object ret = pjp.proceed();
System.out.println("[Around After]");
return ret;
}
}
五、AspectJ 切點表達式語法速查
| 表達式 | 匹配目標 |
execution(public * *(..)) | 所有 public 方法 |
execution(* com.app.service.*.*(..)) | service 包下所有類的所有方法 |
execution(* com.app.service.UserService.*(..)) | UserService 所有方法 |
execution(* com.app.service.UserService.save*(..)) | save 開頭方法 |
execution(* *(..)) && @annotation(Transactional) | 帶 @Transactional 註解的方法 |
within(com.app.service.*) | service 包下所有類 (含非 public) |
this(com.app.service.UserService) | 代理物件實現 UserService 介面 |
target(com.app.service.UserServiceImpl) | 目標物件是 UserServiceImpl |
args(String, ..) | 第一參數為 String 的方法 |
@within(Secured) | 類帶 @Secured 註解 |
@annotation(Secured) | 方法帶 @Secured 註解 |
六、引入增強 (Introduction) 實戰
Spring AOP 方式 (DelegatingIntroductionInterceptor)
// 新介面
public interface Lockable { void lock(); void unlock(); boolean locked(); }
// 實現
public class LockableImpl implements Lockable {
private boolean locked;
public void lock() { locked = true; }
public void unlock() { locked = false; }
public boolean locked() { return locked; }
}
// XML 配置 (需 proxy-target-class="true" 使用 CGLib 代理類)
<bean id="lockableAdvisor" class="org.springframework.aop.support.DelegatingIntroductionInterceptor">
<constructor-arg><bean class="LockableImpl"/></constructor-arg>
</bean>
<bean id="target" class="com.app.UserServiceImpl"/>
<bean class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="target"/>
<property name="interceptorNames"><list><value>lockableAdvisor</value></list></property>
<property name="proxyTargetClass" value="true"/> <!-- 關鍵:代理類而非介面 -->
</bean>
// 使用
UserServiceImpl proxy = (UserServiceImpl) ctx.getBean("target");
Lockable lockable = (Lockable) proxy; // 強轉新介面
lockable.lock(); // 動態實現
AspectJ 方式 (@DeclareParents) - 更強大,支援介面/類切點
@Aspect
public class LockableAspect {
@DeclareParents(value = "com.app.service.*+", defaultImpl = LockableImpl.class)
public static Lockable lockable; // 靜態欄位持有介面引用
}
// 配置
<aop:aspectj-autoproxy proxy-target-class="true"/>
<bean class="LockableAspect"/>
// 使用 (可面向介面編程)
UserService proxy = ctx.getBean("userService", UserService.class);
Lockable lockable = (Lockable) proxy; // 直接轉型
lockable.lock();
七、自動代理機制
| 類別 | 配置 | 特點 |
| BeanNameAutoProxyCreator | beanNames="*Service" | 按 Bean 名稱後綴匹配 |
| DefaultAdvisorAutoProxyCreator | <aop:config> 隱式註冊 | 掃描所有 Advisor (Pointcut+Advice) 自動代理匹配 Bean |
| AnnotationAwareAspectJAutoProxyCreator | <aop:aspectj-autoproxy/> | 掃描 @Aspect Bean,最常用、功能最全 |
八、XML 純配置風格 (無註解侵入)
<aop:config proxy-target-class="true">
<aop:aspect id="txAspect" ref="txAdvice">
<aop:pointcut id="serviceMethods" expression="execution(* com.app.service.*.*(..))"/>
<aop:around pointcut-ref="serviceMethods" method="invoke"/>
</aop:aspect>
</aop:config>
<bean id="txAdvice" class="com.app.TxAdvice"/> <!-- 實現 MethodInterceptor -->
九、Spring AOP vs AspectJ 選型建議
- Spring AOP:運行期代理、僅方法攔截、零侵入、啟動快、適合 95% 企業場景 (事務、日誌、權限、快取)。
- AspectJ:編譯期/類載入期織入、支援構造子/欄位/靜態方法攔截、效能極致、需 ajc 編譯器或 LTW (Load-Time Weaving) 配置、適合基礎設施層、極高效能要求、領域模型驅動設計 (DDD)。
- 混合使用:Spring 核心業務用 Spring AOP,底層框架/領域模型用 AspectJ (可透過
@EnableAspectJAutoProxy 整合)。
十、常見坑點避雷
- 內部方法調用不生效:
this.method() 繞過代理 → 注入自己 (@Autowired SelfService self) 或 AopContext.currentProxy()。
- final 方法/類無法代理 (CGLib):移除 final 或改用介面 + JDK 代理。
- 事務不回滾:檢查異常類型 (需
rollbackFor=Exception.class)、是否吞掉異常、傳播行為、DataSource AutoCommit。
- 切點表達式寫錯:
execution(* com..service.*.*(..)) 雙點號匹配子包,單點號僅當前包。
- 多介面實現類代理轉型失敗:JDK 代理只能轉介面,需
proxy-target-class="true" 轉具體類。