說說AOP 中的 Aspect、Advice、Pointcut、JointPoint 和 Advice 參數分別是什么?
面試回答
Aspect(切面): 切面是橫切關注點的模塊化,它包含Advice
和Pointcut
,是AOP的基本單位。切面可以理解為我們要增強的邏輯模塊,例如日志、事務等功能。
Advice(通知): 通知定義了切面在特定連接點要執行的動作。Spring支持5種通知類型:@Before
(前置)、@After
(后置)、@AfterReturning
(返回后)、@AfterThrowing
(異常后)和@Around
(環繞)。
Pointcut(切點): 切點是匹配連接點的表達式,決定Advice
在哪些方法上執行。Spring使用AspectJ
的表達式語言定義切點。
JoinPoint(連接點): 連接點是程序執行過程中可以插入切面的點,如方法調用、異常拋出等。在Spring AOP中,連接點總是方法的執行點。
Advice參數: 通過JoinPoint
對象可以獲取目標方法的簽名、參數等信息,還可以自定義參數綁定,實現更靈活的邏輯處理。
AOP的核心概念圍繞著"在什么地方(Pointcut)"執行"什么操作(Advice)",并將這些封裝在切面(Aspect)中。通過連接點(JoinPoint)和參數綁定,我們可以獲取方法執行的上下文信息,實現更靈活的橫切關注點模塊化,提高代碼的可維護性和復用性。
詳細解析
1. Aspect(切面)
切面是AOP的核心模塊化單元,它封裝了跨多個類的橫切關注點。
在Spring中,切面通過@Aspect
注解定義,簡單說來就是需要增強的代碼邏輯
package com.qy.aop;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
// 這里包含Pointcut和Advice定義
}
切面的職責是將Pointcut
(定義在哪里切入)和Advice
(定義做什么)整合在一起。
2. Advice(通知)
通知定義了切面在特定連接點執行的動作,Spring支持5種類型:
package com.qy.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
publicclass LoggingAspect {
// 前置通知:方法執行前
@Before("execution(* com.qy.service.*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("前置通知:準備執行方法 " + joinPoint.getSignature().getName());
}
// 后置通知:方法執行后(無論是否異常)
@After("execution(* com.qy.service.*.*(..))")
public void afterAdvice(JoinPoint joinPoint) {
System.out.println("后置通知:方法 " + joinPoint.getSignature().getName() + " 已執行完畢");
}
// 返回通知:方法正常返回后
@AfterReturning(pointcut = "execution(* com.qy.service.*.*(..))", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
System.out.println("返回通知:方法 " + joinPoint.getSignature().getName() + " 返回值: " + result);
}
// 異常通知:方法拋出異常后
@AfterThrowing(pointcut = "execution(* com.qy.service.*.*(..))", throwing = "ex")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
System.out.println("異常通知:方法 " + joinPoint.getSignature().getName() + " 拋出異常: " + ex.getMessage());
}
// 環繞通知:完全控制方法執行
@Around("execution(* com.qy.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("環繞通知開始:準備調用方法 " + joinPoint.getSignature().getName());
long startTime = System.currentTimeMillis();
Object result = null;
try {
// 調用原方法
result = joinPoint.proceed();
} catch (Exception e) {
System.out.println("環繞通知捕獲異常: " + e.getMessage());
throw e;
} finally {
long endTime = System.currentTimeMillis();
System.out.println("環繞通知結束:方法執行耗時 " + (endTime - startTime) + "ms");
}
return result;
}
}
各種通知類型的特點和使用場景:
- @Before: 適合做參數校驗、權限檢查等前置工作
- @After: 適合做資源釋放等必須執行的操作
- @AfterReturning: 適合對返回結果進行處理或記錄
- @AfterThrowing: 適合做異常處理、日志記錄等
- @Around: 功能最強大,可以完全控制方法執行,適合做性能監控、事務控制等
3. Pointcut(切點)
切點是匹配連接點的表達式,定義了Advice
在哪些方法上執行。Spring采用AspectJ表達式語言:
package com.qy.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
publicclass SystemArchitecture {
// 所有service包中的方法
@Pointcut("execution(* com.qy.service.*.*(..))")
public void serviceLayer() {}
// 所有dao包中的方法
@Pointcut("execution(* com.qy.dao.*.*(..))")
public void dataAccessLayer() {}
// 組合切點:所有service或dao包中的方法
@Pointcut("serviceLayer() || dataAccessLayer()")
public void businessLogicLayer() {}
// 帶有@Transactional注解的方法
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalMethods() {}
}
常用的切點表達式:
execution(* com.qy.service.*.*(..))
: 匹配service包中所有類的所有方法@annotation(com.qy.annotation.LogExecutionTime)
: 匹配帶有特定注解的方法within(com.qy.service.*)
: 匹配service包中所有類的方法this(com.qy.service.UserService)
: 匹配實現了UserService接口的代理對象的方法target(com.qy.service.UserService)
: 匹配實現了UserService接口的目標對象的方法args(java.lang.String,..)
: 匹配第一個參數為String類型的方法
4. JoinPoint(連接點)
連接點是程序執行過程中可以插入切面的點。在Spring AOP中,連接點總是方法執行點。JoinPoint對象提供了訪問連接點信息的方法:
package com.qy.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Aspect
@Component
publicclass JoinPointDemoAspect {
@Before("execution(* com.qy.service.*.*(..))")
public void demonstrateJoinPoint(JoinPoint joinPoint) {
// 獲取目標方法簽名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 獲取目標對象
Object target = joinPoint.getTarget();
// 獲取方法參數
Object[] args = joinPoint.getArgs();
System.out.println("目標類: " + target.getClass().getName());
System.out.println("方法名: " + method.getName());
System.out.println("參數列表: " + Arrays.toString(args));
System.out.println("方法修飾符: " + method.getModifiers());
System.out.println("方法返回類型: " + method.getReturnType().getName());
}
}
JoinPoint對象提供的關鍵信息:
getSignature()
: 獲取方法簽名getTarget()
: 獲取目標對象getArgs()
: 獲取方法參數getThis()
: 獲取代理對象getKind()
: 獲取連接點類型
環繞通知中使用的是ProceedingJoinPoint,它擴展了JoinPoint接口,添加了proceed()方法用于執行目標方法。
5. Advice參數
Advice
方法可以接收參數,增強其靈活性:
package com.qy.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
publicclass ParameterDemoAspect {
// 通過JoinPoint獲取參數
@Before("execution(* com.qy.service.UserService.findById(Long))")
public void beforeFindById(JoinPoint joinPoint) {
Long userId = (Long) joinPoint.getArgs()[0];
System.out.println("查詢用戶ID: " + userId);
}
// 直接綁定參數
@Before("execution(* com.qy.service.UserService.findById(Long)) && args(userId)")
public void beforeFindByIdWithParam(Long userId) {
System.out.println("準備查詢用戶ID: " + userId);
}
// 綁定注解參數
@Before("@annotation(audit)")
public void auditMethod(JoinPoint joinPoint, com.qy.annotation.Audit audit) {
System.out.println("審計記錄: " + audit.operation());
System.out.println("操作方法: " + joinPoint.getSignature().getName());
}
}
參數綁定的主要方式:
- 通過JoinPoint對象獲取
- 通過切點表達式中的args()綁定
- 通過注解屬性綁定
AOP的工作流程
- 定義切面類(Aspect),包含切點(Pointcut)和通知(Advice)
- Spring容器啟動時識別
@Aspect
注解的Bean - 根據切點表達式匹配目標Bean的方法,創建代理對象
- 當調用目標方法時,代理對象攔截調用并按順序執行相應的通知
實際應用場景
AOP在實際開發中有廣泛應用:
- 日志記錄:記錄方法調用、參數、執行時間等
- 事務管理:聲明式事務控制
- 安全控制:權限檢查、認證
- 性能監控:統計方法執行時間
- 緩存處理:方法結果緩存
- 異常處理:統一異常處理和日志
- 重試機制:失敗自動重試
使用AOP實現日志
下面是一個完整的使用Spring AOP實現日志功能的例子
package com.qy.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect
@Component
publicclass LoggingAspect {
// 定義切點:攔截service包下所有類的所有方法
@Pointcut("execution(* com.qy.service.*.*(..))")
public void serviceLog() {}
// 環繞通知:記錄方法執行前后的日志
@Around("serviceLog()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 獲取方法簽名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 獲取目標類
Class<?> targetClass = joinPoint.getTarget().getClass();
// 創建日志對象
Logger logger = LoggerFactory.getLogger(targetClass);
// 記錄方法開始執行的日志
String methodName = signature.getName();
String className = targetClass.getSimpleName();
Object[] args = joinPoint.getArgs();
logger.info("開始執行: {}.{},參數: {}", className, methodName, Arrays.toString(args));
long startTime = System.currentTimeMillis();
Object result = null;
try {
// 執行目標方法
result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
// 記錄方法正常結束的日志
logger.info("方法執行成功: {}.{},耗時: {}ms,返回值: {}",
className, methodName, (endTime - startTime), result);
return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
// 記錄方法異常的日志
logger.error("方法執行異常: {}.{},耗時: {}ms,異常信息: {}",
className, methodName, (endTime - startTime), e.getMessage());
// 拋出原始異常,不影響業務邏輯
throw e;
}
}
}
業務服務類
package com.qy.service;
import com.qy.model.User;
import org.springframework.stereotype.Service;
@Service
publicclass UserService {
public User getUserById(Long id) {
// 模擬業務邏輯
if (id <= 0) {
thrownew IllegalArgumentException("用戶ID必須大于0");
}
// 模擬從數據庫查詢用戶
User user = new User();
user.setId(id);
user.setUsername("用戶" + id);
user.setEmail("user" + id + "@qq.com");
return user;
}
public boolean updateUser(User user) {
// 模擬更新用戶信息
System.out.println("更新用戶信息: " + user);
returntrue;
}
}
用戶實體類
package com.qy.model;
publicclass User {
private Long id;
private String username;
private String email;
// 省略getter和setter方法
@Override
public String toString() {
return"User{id=" + id + ", username='" + username + "', email='" + email + "'}";
}
}
主應用類
package com.qy;
import com.qy.model.User;
import com.qy.service.UserService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
publicclass LoggingAopApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(LoggingAopApplication.class, args);
// 獲取UserService
UserService userService = context.getBean(UserService.class);
try {
// 測試正常情況
User user = userService.getUserById(1L);
System.out.println("獲取到用戶: " + user);
// 修改用戶并更新
user.setUsername("修改后的用戶名");
userService.updateUser(user);
// 測試異常情況
userService.getUserById(-1L);
} catch (Exception e) {
System.out.println("捕獲到異常: " + e.getMessage());
}
}
}
Spring配置類
package com.qy.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.qy")
public class AppConfig {
}
當應用運行時,AOP會自動攔截UserService中的方法調用并輸出日志,日志輸出示例
com.qy.service.UserService - 開始執行: UserService.getUserById,參數: [1]
com.qy.service.UserService - 方法執行成功: UserService.getUserById,耗時: 3ms,返回值: User{id=1, username='用戶1', email='user1@qq.com'}
獲取到用戶: User{id=1, username='用戶1', email='user1@qq.com'}
com.qy.service.UserService - 開始執行: UserService.updateUser,參數: [User{id=1, username='修改后的用戶名', email='user1@qq.com'}]
更新用戶信息: User{id=1, username='修改后的用戶名', email='user1@qq.com'}
com.qy.service.UserService - 方法執行成功: UserService.updateUser,耗時: 1ms,返回值: true
com.qy.service.UserService - 開始執行: UserService.getUserById,參數: [-1]
com.qy.service.UserService - 方法執行異常: UserService.getUserById,耗時: 1ms,異常信息: 用戶ID必須大于0
捕獲到異常: 用戶ID必須大于0