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 的步驟很簡單:

  1. 寫一個類別標注 @Aspect
  2. 標注 @Component 讓 Spring 掃描到
  3. 在方法上標注 @Before、@After 等
  4. 定義切入點(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
// 匹配 UserService 的所有方法
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}

// 只匹配 get 開頭的方法
@Pointcut("execution(* com.example.service.UserService.get*(..))")
public void getUserMethods() {}

// 只匹配傳入 String 的方法
@Pointcut("execution(* com.example.service.UserService.*(String))")
public void getStringArgMethods() {}

within

根據類別層級匹配:

1
2
3
// 匹配整個 service package
@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 {
}

// 然後 aspect 就可以這樣用
@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) {
// 假設從 context 拿到當前使用者
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) {
// 只有 admin 能執行
}
}

幾個常見的坑

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(); // AOP 不會生效!因為是內部呼叫
}

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(); // 這樣 AOP 會生效
}
}

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)統一管理。