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