AOP(面向切面編程)這個概念一開始聽起來很唬爛,但其實就是把某些跨越多個業務邏輯的共同功能(比如 logging、權限驗證、效能監控)提取出來,用一個統一的地方來管理。
我之前有個專案就是沒用 AOP,logging 的程式碼散落在各個 service 裡面,後來改用 AOP 重構了一遍,爽度直接升天。
前置準備
首先要加上依賴。如果用 Spring Boot,直接加:
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
|
這個依賴裡面已經包含了 AspectJ 和 Spring AOP 相關的東西。
基本結構
用 Spring AOP 的步驟很簡單:
- 寫一個類別標注 @Aspect
- 標注 @Component 讓 Spring 掃描到
- 在方法上標注 @Before、@After 等
- 定義切入點(pointcut)
來個最簡單的例子:
1 2 3 4 5 6 7 8 9 10
| @Aspect @Component public class LoggingAspect { @Before("execution(* com.example.service.*.*(..))") public void logBefore(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); System.out.println("方法開始: " + methodName); } }
|
要注意,這邊有個坑:你得要開啟 AOP 自動代理。在 Spring Boot application 的 main class 上面加:
1 2 3 4 5 6 7
| @SpringBootApplication @EnableAspectJAutoProxy public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
|
不過在 Spring Boot 2.0 以後,如果你的 classpath 有 AspectJ 的東西,它會自動開啟,不用手動加也可以。但為了清楚起見,我還是習慣明確寫上去。
五種 Advice 類型
1. @Before
在方法執行前執行:
1 2 3 4
| @Before("execution(* com.example.service.UserService.getUser(..))") public void beforeGetUser(JoinPoint joinPoint) { System.out.println("Getting user..."); }
|
2. @After
無論方法是否拋例外,都會在方法執行後執行:
1 2 3 4
| @After("execution(* com.example.service.UserService.getUser(..))") public void afterGetUser(JoinPoint joinPoint) { System.out.println("Getting user finished (finally)"); }
|
3. @AfterReturning
方法正常返回時執行(沒有拋例外):
1 2 3 4 5 6 7
| @AfterReturning( pointcut = "execution(* com.example.service.UserService.getUser(..))", returning = "result" ) public void afterReturning(JoinPoint joinPoint, Object result) { System.out.println("方法返回結果: " + result); }
|
4. @AfterThrowing
方法拋出例外時執行:
1 2 3 4 5 6 7
| @AfterThrowing( pointcut = "execution(* com.example.service.UserService.*(..))", throwing = "ex" ) public void afterThrowing(JoinPoint joinPoint, Exception ex) { System.out.println("方法拋出例外: " + ex.getMessage()); }
|
5. @Around
最強大的一個,可以在方法執行前後都做事,而且可以決定是否執行原方法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Around("execution(* com.example.service.UserService.*(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); System.out.println("方法開始: " + joinPoint.getSignature().getName()); try { Object result = joinPoint.proceed(); return result; } finally { long duration = System.currentTimeMillis() - startTime; System.out.println("方法耗時: " + duration + "ms"); } }
|
定義 Pointcut
Pointcut 就是用來定義”要攔哪些方法”的規則。最常用的有三種:
execution
根據方法簽名匹配:
1 2 3 4 5 6 7 8 9 10 11
| @Pointcut("execution(* com.example.service.UserService.*(..))") public void userServiceMethods() {}
@Pointcut("execution(* com.example.service.UserService.get*(..))") public void getUserMethods() {}
@Pointcut("execution(* com.example.service.UserService.*(String))") public void getStringArgMethods() {}
|
within
根據類別層級匹配:
1 2 3
| @Pointcut("within(com.example.service.*)") public void allService() {}
|
@annotation
根據自訂註解匹配:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogMe { }
@Before("@annotation(LogMe)") public void logAnnotated(JoinPoint joinPoint) { System.out.println("Logging: " + joinPoint.getSignature().getName()); }
@Service public class UserService { @LogMe public void deleteUser(String userId) { } }
|
實務例子:請求計時 + 例外處理
把這些概念組合起來,做一個實務的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Aspect @Component public class PerformanceAspect { private static final Logger logger = LoggerFactory.getLogger(PerformanceAspect.class); @Pointcut("execution(* com.example.service..*(..))") public void allServiceMethods() {} @Around("allServiceMethods()") public Object trackPerformance(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); String methodName = joinPoint.getSignature().getName(); try { Object result = joinPoint.proceed(); long duration = System.currentTimeMillis() - startTime; logger.info("{}() executed in {}ms", methodName, duration); return result; } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; logger.error("{}() failed after {}ms: {}", methodName, duration, e.getMessage()); throw e; } } }
|
權限驗證的例子
常見的另一種用途是權限驗證。先定義註解:
1 2 3 4
| @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireAdmin { }
|
然後寫 aspect:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Aspect @Component public class AuthenticationAspect { @Before("@annotation(RequireAdmin)") public void checkAdmin(JoinPoint joinPoint) { User currentUser = getCurrentUser(); if (!currentUser.isAdmin()) { throw new UnauthorizedException("This operation requires admin privileges"); } } }
|
在方法上標注:
1 2 3 4 5 6 7 8
| @Service public class UserService { @RequireAdmin public void deleteUser(String userId) { } }
|
幾個常見的坑
1. AOP 對同一個 class 內部呼叫不會生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Service public class UserService { @Before("execution(* com.example.service.UserService.getUser(..))") public void checkAuth() { System.out.println("Checking auth"); } public void deleteUser() { getUser(); } public void getUser() { } }
|
解法是注入自己,用代理物件呼叫:
1 2 3 4 5 6 7 8 9 10
| @Service public class UserService { @Autowired private UserService self; public void deleteUser() { self.getUser(); } }
|
2. Private 和 final 方法無法被攔截
Spring AOP 用的是 CGLIB 動態代理(或 JDK proxy),無法攔截 private 和 final 的方法。如果你需要攔截這些方法,得用 AspectJ 的編譯時織入(compile-time weaving),但那就比較複雜了。
3. 多個 Aspect 的執行順序
如果有多個 aspect 要攔截同一個方法,可以用 @Order 來控制順序:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Aspect @Component @Order(1) public class LoggingAspect { }
@Aspect @Component @Order(2) public class PerformanceAspect { }
|
Order 數字越小,執行優先級越高。
重點整理
- 需要加
spring-boot-starter-aop 依賴
- 用 @Aspect + @Component 定義 aspect
- 五種 advice:@Before、@After、@AfterReturning、@AfterThrowing、@Around
- Pointcut 有三種主要方式:execution、within、@annotation
- @Around 最強大,可以控制方法執行與否
- 注意 private/final 方法無法被攔截
- 同一個 class 內部呼叫不經過 proxy
寫好 AOP 之後,真的可以大大簡化業務邏輯程式碼,把那些橫切關注點(cross-cutting concerns)統一管理。
你的鼓勵將被轉換為我明天繼續加班的動力(真的)。 ❤️