AOP 切面編程
什么是 AOP ?
AOP:
Aspect Oriented Programming(面向切面編程、面向方面編程),其實(shí)就是面向特定方法編程。
實(shí)現(xiàn):
- 動(dòng)態(tài)代理是面向切面編程最主流的實(shí)現(xiàn)。而
SpringAOP是Spring框架的高級(jí)技術(shù),旨在管理bean對(duì)象的過程中,主要通過底層的動(dòng)態(tài)代理機(jī)制,對(duì)特定的方法進(jìn)行編程。
AOP 核心概念
- 連接點(diǎn):
JoinPoint, 可以被AOP控制的方法(暗含方法執(zhí)行時(shí)的相關(guān)信息) - 通知:
Advice, 指哪些重復(fù)的邏輯,也就是共性功能(最終體現(xiàn)為一個(gè)方法) - 切入點(diǎn):
PointCut, 匹配連接點(diǎn)的條件,通知僅會(huì)在切入點(diǎn)方法執(zhí)行時(shí)被應(yīng)用 - 切面:
Aspect, 描述通知與切入點(diǎn)的對(duì)應(yīng)關(guān)系(通知+切入點(diǎn)) - 目標(biāo)對(duì)象:
Target, 通知所應(yīng)用的對(duì)象
場(chǎng)景說明
例如現(xiàn)有一個(gè)場(chǎng)景:定位執(zhí)行耗時(shí)較長(zhǎng)的業(yè)務(wù)方法,統(tǒng)計(jì)各個(gè)業(yè)務(wù)層方法的執(zhí)行耗時(shí)
@Component
@Aspect // 切面類
@Slf4j
public class TimeAspect {
@Around ("execution (* com.itheima.service.impl.DeptServiceImpl.list ())") // 切面表達(dá)式
public Object recordTime (ProceedingJoinPoint joinPoint) throws Throwable {
long begin = System.currentTimeMillis ();
// 調(diào)用原始操作
Object result = joinPoint.proceed ();
long end = System.currentTimeMillis ();
log.info("執(zhí)行耗時(shí) : {} ms", (end-begin));
return result;
}
}
// 目標(biāo)對(duì)象
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
// region-begin 連接點(diǎn)
@Override
public List<Dept> list() {
List<Dept> deptList = deptMapper.list(); // 切入點(diǎn)
return deptList;
}
@Override
public void delete(Integer id) {
deptMapper.delete(id);
}
@Override
public void save(Dept dept) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.save(dept);
}
// region-end 連接點(diǎn)
}
對(duì)于aop的五大核心概念,我們可以使用更加通俗易懂的類比來說明:
可以用 "學(xué)校檢查衛(wèi)生" 來類比:
- 連接點(diǎn):學(xué)校里所有可能被檢查的班級(jí)(每個(gè)班級(jí)都是一個(gè)潛在的檢查點(diǎn))
- 通知:檢查衛(wèi)生的具體操作流程(比如看地面是否干凈、桌椅是否整齊,這是一套固定重復(fù)的動(dòng)作)
- 切入點(diǎn):篩選要檢查的班級(jí)的條件(比如 "只查一年級(jí)的班級(jí)" 或 "只查偶數(shù)號(hào)的班級(jí)")
- 切面:檢查計(jì)劃(把 "檢查流程" 和 "篩選條件" 結(jié)合起來,比如 "用標(biāo)準(zhǔn)流程檢查所有一年級(jí)班級(jí)")
- 目標(biāo)對(duì)象:最終被檢查的那些班級(jí)(符合篩選條件,實(shí)際接受檢查的對(duì)象)
簡(jiǎn)單來說就是:學(xué)校(AOP)要檢查衛(wèi)生(通知),所有的班級(jí)都可能被抽查到(連接點(diǎn)),但是只會(huì)查到一年級(jí)的(切入點(diǎn)),"用標(biāo)準(zhǔn)流程查一年級(jí)班級(jí)" 這個(gè)整體安排就是切面,而被查到的那些一年級(jí)班級(jí)就是目標(biāo)對(duì)象。
特別注意:連接點(diǎn)
-
在Spring中用
JoinPoint抽象了連接點(diǎn),用它可以獲得方法執(zhí)行時(shí)的相關(guān)信息,如目標(biāo)類名、方法名、方法參數(shù)等。 -
- 對(duì)于
@Around通知,獲取連接點(diǎn)信息只能使用ProceedingJoinPoint - 對(duì)于其他四種通知,獲取連接點(diǎn)信息只能使用
JoinPoint,它是ProceedingJoinPoint的父類型
- 對(duì)于
通知類型
通知類型
**@Around**:環(huán)繞通知,此注解標(biāo)注的通知方法在目標(biāo)方法前、后都被執(zhí)行(常用)@Before:前置通知,此注解標(biāo)注的通知方法在目標(biāo)方法前被執(zhí)行@After:后置通知,此注解標(biāo)注的通知方法在目標(biāo)方法后被執(zhí)行,無(wú)論是否有異常都會(huì)執(zhí)行@AfterReturning:返回后通知,此注解標(biāo)注的通知方法在目標(biāo)方法后被執(zhí)行,有異常不會(huì)執(zhí)行@AfterThrowing:異常后通知,此注解標(biāo)注的通知方法發(fā)生異常后執(zhí)行
注意事項(xiàng)
@Around環(huán)繞通知需要自己調(diào)用ProceedingJoinPoint.proceed()來讓原始方法執(zhí)行,其他通知不需要考慮目標(biāo)方法執(zhí)行@Around環(huán)繞通知方法的返回值,必須指定為Object,來接收原始方法的返回值。
通知順序
當(dāng)有多個(gè)切面的切入點(diǎn)都匹配到了目標(biāo)方法,目標(biāo)方法運(yùn)行時(shí),多個(gè)通知方法都會(huì)被執(zhí)行。
執(zhí)行順序
- 不同切面類中,默認(rèn)按照切面類的類名字母排序:
-
- 目標(biāo)方法前的通知方法:字母排名靠前的先執(zhí)行
- 目標(biāo)方法后的通知方法:字母排名靠前的后執(zhí)行
- 用
@Order(數(shù)字)加在切面類上來控制順序
-
- 目標(biāo)方法前的通知方法:數(shù)字小的先執(zhí)行
- 目標(biāo)方法后的通知方法:數(shù)字小的后執(zhí)行
@PointCut
該注解的作用是將公共的切點(diǎn)表達(dá)式抽取出來,需要用到時(shí)引用該切點(diǎn)表達(dá)式即可。
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void pt(){}
@Around("pt()")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
// 方法體內(nèi)容
}
切入點(diǎn)表達(dá)式
execution
execution 主要根據(jù)方法的返回值、包名、類名、方法名、方法參數(shù)等信息來匹配,語(yǔ)法為:
execution(訪問修飾符? 返回值 包名.類名.?方法名(方法參數(shù)) throws 異常?)
-
可以使用通配符描述切入點(diǎn)
-
*:?jiǎn)蝹€(gè)獨(dú)立的任意符號(hào),可以通配任意返回值、包名、類名、方法名、任意類型的一個(gè)參數(shù),也可以通配包、類、方法名的一部分
execution(* com.**.service.**.update*(*))..:多個(gè)連續(xù)的任意符號(hào),可以通配任意層級(jí)的包,或任意類型、任意個(gè)數(shù)的參數(shù)
execution(* com.itheima..DeptService.*(..))
切入點(diǎn)表達(dá)式-@annotation
@annotation切入點(diǎn)表達(dá)式,用于匹配標(biāo)識(shí)有特定注解的方法。
@annotation(com.itheima.anno.Log)
@Before("@annotation(com.itheima.anno.Log)")
public void before(){
log.info("before ....");
}
Spring 實(shí)戰(zhàn)代碼演示
依賴導(dǎo)入
引入aop依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
日志響應(yīng)攔截
主要作用:對(duì)controller上的
@Around("execution(* com.lantzuc.lanucbackend.controller.*.*(..))")
public Object doInterceptor(ProceedingJoinPoint joinPoint) throws Throwable {
// 開始計(jì)時(shí)
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 獲取請(qǐng)求路徑
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
// 生成唯一請(qǐng)求id
String requestId = UUID.randomUUID().toString();
String url = servletRequest.getRequestURI();
// 獲取請(qǐng)求參數(shù)
Object[] args = joinPoint.getArgs();
String reqParams = "[" + StringUtils.join(args, ", ") + "]";
// 輸出請(qǐng)求日志
log.info("request start,id: {}, path: {}, ip: {}, params: {}", requestId, url,
servletRequest.getRemoteHost(), reqParams);
// 執(zhí)行原方法
Object result = joinPoint.proceed();
// 輸出原日志
stopWatch.stop();
long totalTimeMillis = stopWatch.getTotalTimeMillis();
log.info("request end, id: {}, cost: {}ms", requestId, totalTimeMillis);
return result;
}
結(jié)果顯示
2025-10-15 17:13:48.649 INFO 23396 --- [nio-8080-exec-5] c.l.lanucbackend.aop.LogInterceptor : request start,id: ad4061ee-d5ed-47f3-bf2e-e0567867fabc, path: /api/user/login, ip: 0:0:0:0:0:0:0:1, params: [UseLoginRequest(userAccount=Lantz, userPassword=12345678), org.apache.catalina.connector.RequestFacade@6ce7aed7]
2025-10-15 17:13:49.265 INFO 23396 --- [nio-8080-exec-5] c.l.lanucbackend.aop.LogInterceptor : request end, id: ad4061ee-d5ed-47f3-bf2e-e0567867fabc, cost: 623ms
相比原來沒有添加日志攔截的,我們可以更加清晰地看到對(duì)某一路徑發(fā)送請(qǐng)求的狀態(tài),比如請(qǐng)求路徑,請(qǐng)求參數(shù),IP 地址等等信息,而且我們還可以獲悉到某一請(qǐng)求的執(zhí)行時(shí)間是多少,可以在后續(xù)有針對(duì)的目的優(yōu)化
權(quán)限響應(yīng)攔截
首先要?jiǎng)?chuàng)建一個(gè)注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AuthCheck {
/**
* 必須有某一個(gè)角色(默認(rèn)無(wú))
* @return
*/
String mustRole() default "";
}
然后再編寫權(quán)限校驗(yàn)攔截代碼:
@Around("@annotation(authCheck)")
public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
String mustRole = authCheck.mustRole();
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
// 當(dāng)前登錄用戶
User loginUer = userService.getLoginUer(httpServletRequest);
UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
// 不需要權(quán)限,放行
if (mustRoleEnum == null) {
return joinPoint.proceed();
}
// 必須有該權(quán)限才放行
UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUer.getUserRole());
if (userRoleEnum == null) {
throw new BusinessException(ErrorCode.NO_AUTH);
}
// 如果被封號(hào),直接拒絕
if (UserRoleEnum.BAN.equals(userRoleEnum)) {
throw new BusinessException(ErrorCode.NO_AUTH);
}
// 必需有管理員權(quán)限
if (UserRoleEnum.ADMIN.equals(mustRoleEnum)) {
// 用戶沒有管理員權(quán)限,拒絕
if (!UserRoleEnum.ADMIN.equals(userRoleEnum)) {
throw new BusinessException(ErrorCode.NO_AUTH);
}
}
// 通過管理權(quán)限,放行
return joinPoint.proceed();
}
在controller中使用:
@GetMapping("/search")
@AuthCheck(mustRole = ADMIN_ROLE) // 需要管理員權(quán)限
public List<User> searchUser(String userName, HttpServletRequest request){
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(userName)) {
queryWrapper.like("userName", userName);
}
List<User> userList = userService.list(queryWrapper);
return userList.stream().map(user -> userService.getSafetyUser(user)).collect(Collectors.toList());
}
測(cè)試結(jié)果:
如果登錄用戶為管理員則正常通過,如果不是,則會(huì)報(bào)錯(cuò):

Postman Body 顯示:
{
"code": 40101,
"data": null,
"message": "無(wú)權(quán)限",
"description": ""
}
記得如果使用了jwt鑒權(quán),在Postman中測(cè)試的時(shí)候記得選擇Bearer Token然后粘貼進(jìn)去登錄時(shí)候產(chǎn)生的Token

浙公網(wǎng)安備 33010602011771號(hào)