AOP(Aspect Oriented Programming,面向切面程式設計)是 Spring 除了依賴注入外最核心的功能。它將與業務無關的程式碼單獨抽離,降低系統耦合性。
AOP 本質上就是代理模式
Spring 在執行期幫我們把切面中的程式碼邏輯動態「織入」到容器物件方法內。
AOP 概念與術語#
| 術語 | 說明 |
|---|---|
| Aspect(切面) | 橫切關注點的模組化,如日誌、事務 |
| Join Point(連線點) | 程式執行的特定點,如方法呼叫 |
| Pointcut(切點) | 匹配連線點的表達式 |
| Advice(通知) | 在切點執行的動作 |
| Target(目標物件) | 被代理的原始物件 |
| Proxy(代理物件) | AOP 創建的代理物件 |
Advice 類型#
| 類型 | 說明 | 執行時機 |
|---|---|---|
@Before | 前置通知 | 方法執行前 |
@After | 後置通知 | 方法執行後(無論成功或例外) |
@AfterReturning | 回傳通知 | 方法成功回傳後 |
@AfterThrowing | 例外通知 | 方法拋出例外後 |
@Around | 環繞通知 | 包圍方法執行 |
Spring AOP 實現原理#
JDK 動態代理 vs CGLIB#
| 特性 | JDK 動態代理 | CGLIB |
|---|---|---|
| 前提條件 | 目標類必須實現介面 | 無限制(不能是 final 類) |
| 實現方式 | 實現相同介面 | 生成目標類的子類 |
| 效能 | 呼叫時較快 | 創建時較慢,呼叫時較快 |
graph LR
subgraph JDK["JDK 動態代理"]
I1[Interface] <-.- P1[Proxy]
P1 --> T1[Target]
T1 -.-> I1
end
subgraph CGLIB["CGLIB"]
T2[Target] <|-- C2["Target$$EnhancerByCGLIB<br/>(子類)"]
end
style JDK fill:#e3f2fd
style CGLIB fill:#fff3e0代理物件的創建過程#
// 關鍵類:AnnotationAwareAspectJAutoProxyCreator
// 實現 BeanPostProcessor,在 Bean 初始化後創建代理
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
// 獲取適用的 Advisors
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
// 創建代理物件
Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors,
new SingletonTargetSource(bean));
return proxy;
}
return bean;
}切點表達式#
常用表達式#
// execution: 匹配方法執行
@Pointcut("execution(* com.example.service.*.*(..))")
// within: 匹配特定類型
@Pointcut("within(com.example.service.*)")
// @annotation: 匹配標記特定註解的方法
@Pointcut("@annotation(com.example.Loggable)")
// bean: 匹配特定 Bean
@Pointcut("bean(*Service)")execution 表達式詳解#
execution(修飾符? 回傳類型 類型名?方法名(參數) 例外?)
execution(public * com.example.service.*.*(..))
│ │ │ │ │
│ │ │ │ └── 任意參數
│ │ │ └── 任意方法名
│ │ └── service 套件下任意類
│ └── 任意回傳類型
└── public 方法(可省略)AOP 失效的常見場景#
場景一:this 呼叫當前類方法#
使用
this呼叫的方法無法被 AOP 攔截
案例:this 呼叫導致 AOP 失效
問題程式碼:
@Service
public class ElectricService {
public void charge() throws Exception {
System.out.println("Electric charging ...");
this.pay(); // this 是原始物件,不是代理物件!
}
public void pay() throws Exception {
System.out.println("Pay with alipay ...");
Thread.sleep(1000);
}
}
@Aspect
@Service
public class AopConfig {
@Around("execution(* ElectricService.pay())")
public void recordPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println("Pay method time cost: " + (end - start));
}
}呼叫 charge() 時,pay() 的計時切面不會生效!
原因分析:
- Controller 中注入的是代理物件
- 但
this.pay()中的this是原始物件 - 只有通過代理物件呼叫的方法才會被 AOP 攔截
解決方案 1:自己注入自己
@Service
public class ElectricService {
@Autowired
private ElectricService electricService; // 注入代理物件
public void charge() throws Exception {
electricService.pay(); // 通過代理呼叫
}
}解決方案 2:使用 AopContext
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true) // 必須開啟
public class Application { }
@Service
public class ElectricService {
public void charge() throws Exception {
ElectricService proxy = (ElectricService) AopContext.currentProxy();
proxy.pay();
}
}場景二:直接存取代理類的屬性#
代理類的成員變數不會被初始化(使用 CGLIB + Objenesis 時)
案例:存取代理類屬性回傳 null
問題程式碼:
@Service
public class AdminUserService {
public final User adminUser = new User("202101166");
public void login() {
System.out.println("admin user login...");
}
}
@Service
public class ElectricService {
@Autowired
private AdminUserService adminUserService;
public void pay() throws Exception {
adminUserService.login();
String payNum = adminUserService.adminUser.getPayNum(); // NPE!
}
}原因分析:
- Spring 使用
ReflectionFactory.newConstructorForSerialization()創建代理實體 - 這種方式不會初始化類成員變數
- 代理類的
adminUser欄位為null
解決方案:通過方法存取
@Service
public class AdminUserService {
private final User adminUser = new User("202101166");
public User getAdminUser() {
return adminUser;
}
}
// 使用 getter 方法
String payNum = adminUserService.getAdminUser().getPayNum();方法呼叫會被代理攔截,最終從原始物件獲取屬性值。
場景三:方法不是 public#
private方法無法被代理final方法無法被 CGLIB 覆寫
場景四:類內部直接 new 的物件#
// 這樣創建的物件不受 Spring 管理,AOP 不生效
MyService service = new MyService();
service.doSomething();多個增強的執行順序#
同一切面中不同類型的增強#
flowchart LR
A["@Around"] --> B["@Before"] --> C["@After"] --> D["@AfterReturning"] --> E["@AfterThrowing"]
style A fill:#ffccbc
style B fill:#c8e6c9
style C fill:#fff9c4
style D fill:#bbdefb
style E fill:#ffcdd2static {
// Spring 內部定義的順序
new InstanceComparator<>(
Around.class,
Before.class,
After.class,
AfterReturning.class,
AfterThrowing.class
)
}同一切面中相同類型的增強#
按方法名稱的字母順序排序(ASCII 碼比較)
@Aspect
@Service
public class AopConfig {
@Before("execution(* ElectricService.charge())")
public void logBeforeMethod(JoinPoint pjp) { } // 排序較後
@Before("execution(* ElectricService.charge())")
public void checkAuthority(JoinPoint pjp) { } // 'c' < 'l',排序較前
}不同切面之間的順序#
使用 @Order 註解控制:
@Aspect
@Service
@Order(1) // 數值越小,優先級越高
public class SecurityAspect { }
@Aspect
@Service
@Order(2)
public class LoggingAspect { }事務管理原理#
@Transactional 的工作機制#
@Transactional 本質上是通過 AOP 實現的:
// 簡化的事務切面邏輯
@Around("@annotation(Transactional)")
public Object transactionAdvice(ProceedingJoinPoint pjp) throws Throwable {
// 1. 開啟事務
TransactionStatus status = transactionManager.getTransaction(definition);
try {
// 2. 執行業務邏輯
Object result = pjp.proceed();
// 3. 提交事務
transactionManager.commit(status);
return result;
} catch (Exception e) {
// 4. 回滾事務
transactionManager.rollback(status);
throw e;
}
}@Transactional 失效場景#
以下情況
@Transactional會失效:
| 場景 | 原因 |
|---|---|
| 方法不是 public | 代理無法覆寫非 public 方法 |
| this 內部呼叫 | 繞過了代理物件 |
| 例外被 catch 吞掉 | 沒有拋出例外,事務不知道要回滾 |
| 拋出非 RuntimeException | 預設只回滾 RuntimeException |
| 資料庫不支援事務 | 如 MyISAM 引擎 |
事務傳播行為#
| 傳播行為 | 說明 |
|---|---|
REQUIRED | 預設,有則加入,無則創建 |
REQUIRES_NEW | 總是創建新事務 |
NESTED | 巢狀事務,外層回滾會影響內層 |
SUPPORTS | 有則加入,無則非事務執行 |
NOT_SUPPORTED | 非事務執行,有則掛起 |
MANDATORY | 必須在事務中,否則拋例外 |
NEVER | 不能在事務中,否則拋例外 |
案例:事務不回滾
問題程式碼:
@Transactional
public void createOrder() {
try {
orderDao.insert(order);
inventoryService.deduct(order); // 可能拋例外
} catch (Exception e) {
log.error("建立訂單失敗", e);
// 例外被吞掉,事務不會回滾!
}
}解決方案:
@Transactional
public void createOrder() {
try {
orderDao.insert(order);
inventoryService.deduct(order);
} catch (Exception e) {
log.error("建立訂單失敗", e);
throw e; // 重新拋出
// 或使用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}最佳實踐#
- 避免 this 呼叫需要 AOP 增強的方法
- 確保被代理的方法是 public
- 不要直接存取代理類的成員變數,改用 getter
- 事務方法中的例外不要輕易 catch 吞掉
- 使用 @Order 明確控制切面執行順序
- 複雜的 AOP 需求考慮拆分到不同的切面類