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:#ffcdd2
static {
    // 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();
    }
}

最佳實踐#

  1. 避免 this 呼叫需要 AOP 增強的方法
  2. 確保被代理的方法是 public
  3. 不要直接存取代理類的成員變數,改用 getter
  4. 事務方法中的例外不要輕易 catch 吞掉
  5. 使用 @Order 明確控制切面執行順序
  6. 複雜的 AOP 需求考慮拆分到不同的切面類