Spring boot AOP 代码-----14
覆盖「痛点原理 → 5 种通知类型 → execution 切点 → 注解式 AOP」全内容,采用行业通用最佳实践写法,和你的
jobportal 项目无缝衔接,复制即可运行。一、前置准备:引入依赖
Spring AOP 是独立起步依赖,只需在
pom.xml 新增一行:xml
<!-- Spring AOP 面向切面编程 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
✅ 对应 147 集:解决的痛点业务代码里到处重复写「日志、耗时统计、权限校验、异常记录」,这些和业务无关的横切关注点,用 AOP 统一抽出来,一次编写,全局生效,业务代码更干净。
核心术语大白话(对应 148 集)
表格
| 术语 | 通俗解释 |
|---|---|
| Aspect 切面 | 装增强逻辑的类,比如「日志切面」「权限切面」 |
| Pointcut 切点 | 规则:哪些方法需要被增强 |
| Advice 通知 | 时机:什么时候执行增强逻辑 |
| JoinPoint 连接点 | 被拦截到的具体方法 |
二、项目结构
plaintext
com.example.jobportal
├── aspect
│ ├── ControllerLogAspect.java # 控制器统一日志+耗时切面(execution切点)
│ └── ServiceLogAspect.java # 演示4种基础通知类型
└── annotation
└── MethodLog.java # 自定义注解:注解式AOP用
三、切面 1:控制器统一日志 + 耗时统计(最常用)
对应课程:149、150、151 集
使用
@Around 环绕通知 + execution() 切点,是项目里最常用的写法,一次配置所有 Controller 接口自动打日志、统计耗时。java
运行
package com.example.jobportal.aspect; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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.springframework.stereotype.Component; /** * 控制器层统一日志切面 * 功能:所有Controller接口自动打印入参、出参、执行耗时 */ @Slf4j @Aspect @Component @RequiredArgsConstructor public class ControllerLogAspect { private final ObjectMapper objectMapper; /** * 切点定义:匹配 controller 包下所有类的所有方法 * 对应150集:execution() 切点表达式精确匹配 * 语法:execution(返回值 包名.类名.方法名(参数)) */ @Pointcut("execution(* com.example.jobportal.controller..*.*(..))") public void controllerPointcut() {} /** * 环绕通知:方法执行前后都能做逻辑 * 对应149、151集:@Around 最强通知,完全掌控方法执行 */ @Around("controllerPointcut()") public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable { // 1. 方法执行前:记录开始时间、方法名、入参 long startTime = System.currentTimeMillis(); String methodName = joinPoint.getSignature().toShortString(); Object[] args = joinPoint.getArgs(); try { log.info("【接口开始】{},入参:{}", methodName, objectMapper.writeValueAsString(args)); } catch (Exception e) { log.info("【接口开始】{},参数解析失败", methodName); } // 2. 执行原方法(必须调用!否则业务方法不会跑) Object result; try { result = joinPoint.proceed(); } catch (Throwable e) { // 异常原样抛出,不能吞掉业务异常 long cost = System.currentTimeMillis() - startTime; log.error("【接口异常】{},耗时:{}ms,异常:{}", methodName, cost, e.getMessage()); throw e; } // 3. 方法执行后:记录返回值、耗时 long costTime = System.currentTimeMillis() - startTime; try { log.info("【接口结束】{},耗时:{}ms,出参:{}", methodName, costTime, objectMapper.writeValueAsString(result)); } catch (Exception e) { log.info("【接口结束】{},耗时:{}ms", methodName, costTime); } return result; } }
execution 表达式常用写法(150 集核心)
plaintext
// 匹配 controller 包下所有类所有方法 execution(* com.example.jobportal.controller..*.*(..)) // 匹配所有 public 方法 execution(public * com.example.jobportal.service.*.*(..)) // 匹配所有以 save 开头的方法 execution(* com.example.jobportal.service.*.save*(..)) // 匹配返回值是 String 的方法 execution(String com.example.jobportal.service.*.*(..))
四、切面 2:4 种基础通知完整演示
对应课程:152、153、154 集
一次性演示
@Before、@AfterReturning、@AfterThrowing、@After 四种通知,清晰看到执行时机。java
运行
package com.example.jobportal.aspect; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; /** * Service层切面:演示4种通知的执行时机 */ @Slf4j @Aspect @Component public class ServiceLogAspect { // 切点:匹配 service 包下所有方法 @Pointcut("execution(* com.example.jobportal.service..*.*(..))") public void servicePointcut() {} /** * 1. 前置通知:方法执行之前运行 * 对应152集:适合做参数校验、权限检查、日志记录 */ @Before("servicePointcut()") public void beforeMethod(JoinPoint joinPoint) { log.info("【Before】方法执行前:{}", joinPoint.getSignature().toShortString()); } /** * 2. 返回后通知:方法正常返回后运行 * 对应153集:可以拿到返回值,适合做返回日志、数据脱敏 */ @AfterReturning(pointcut = "servicePointcut()", returning = "result") public void afterReturningMethod(JoinPoint joinPoint, Object result) { log.info("【AfterReturning】方法正常返回:{},返回值:{}", joinPoint.getSignature().toShortString(), result); } /** * 3. 异常后通知:方法抛出异常后运行 * 对应154集:集中记录异常日志,不用每个方法try-catch打日志 */ @AfterThrowing(pointcut = "servicePointcut()", throwing = "e") public void afterThrowingMethod(JoinPoint joinPoint, Exception e) { log.error("【AfterThrowing】方法抛出异常:{},异常信息:{}", joinPoint.getSignature().toShortString(), e.getMessage()); } /** * 4. 最终通知:无论正常还是异常,方法结束后一定会执行 * 类似 try-finally 里的 finally */ @After("servicePointcut()") public void afterMethod(JoinPoint joinPoint) { log.info("【After】方法结束(无论成功失败):{}", joinPoint.getSignature().toShortString()); } }
5 种通知执行顺序
plaintext
@Around 前置逻辑 → @Before → 执行业务方法 → @AfterReturning/@AfterThrowing → @After → @Around 后置逻辑
五、注解式 AOP(最灵活、最推荐的写法)
对应课程:155 集
自定义一个注解,只有加了这个注解的方法才会被增强,比 execution 更精准、可读性更强,是企业级项目首选方案。
1. 自定义注解 MethodLog.java
java
运行
package com.example.jobportal.annotation; import java.lang.annotation.*; /** * 自定义方法日志注解 * 加在方法上,就会自动被切面拦截 */ @Target(ElementType.METHOD) // 作用在方法上 @Retention(RetentionPolicy.RUNTIME) // 运行时生效 @Documented public @interface MethodLog { // 方法描述,自定义业务名称 String value() default ""; }
2. 注解式切面
java
运行
package com.example.jobportal.aspect; import com.example.jobportal.annotation.MethodLog; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; /** * 注解式AOP切面 * 对应155集:更干净、可读性更强,只增强加了@MethodLog的方法 */ @Slf4j @Aspect @Component public class AnnotationLogAspect { /** * 切点:所有加了 @MethodLog 注解的方法 */ @Around("@annotation(methodLog)") public Object aroundAnnotation(ProceedingJoinPoint joinPoint, MethodLog methodLog) throws Throwable { // 拿到注解上的描述 String bizName = methodLog.value(); String methodName = joinPoint.getSignature().toShortString(); long start = System.currentTimeMillis(); log.info("【注解日志-开始】业务:{},方法:{}", bizName, methodName); Object result; try { result = joinPoint.proceed(); } catch (Throwable e) { long cost = System.currentTimeMillis() - start; log.error("【注解日志-异常】业务:{},耗时:{}ms", bizName, cost); throw e; } long cost = System.currentTimeMillis() - start; log.info("【注解日志-结束】业务:{},耗时:{}ms", bizName, cost); return result; } }
3. 使用方式
在你想增强的方法上加注解即可,比如:
java
运行
@MethodLog("保存用户留言") public Contact saveContact(ContactDTO dto) { // 业务代码 }
六、最佳实践总结
-
优先用注解式 AOP
- 精准控制,不会误切入;代码可读性强,看注解就知道有增强逻辑
execution适合全局统一规则(比如所有 Controller 日志)
-
@Around 是最强大的通知,但不要滥用
- 只做日志、耗时、权限这类无侵入逻辑
- 绝对不要吞掉原方法的异常,必须原样抛出,否则业务异常会丢失
-
切点粒度要精确
- 不要写
execution(* *.*(..))切所有方法,会严重影响性能 - 精确到具体包、具体类、具体前缀
- 不要写
-
切面职责单一
- 日志切面只做日志,权限切面只做权限,不要一个切面塞一堆逻辑
-
复杂逻辑优先用 AOP,简单重复代码直接写工具类
- 跨多个层级、全局生效的逻辑用 AOP
- 局部工具方法不用硬套 AOP
七、课程对应表
表格
| 集数 | 核心内容 | 对应代码 |
|---|---|---|
| 147 | AOP 解决的痛点:重复代码、横切关注点 | 前置说明部分 |
| 148 | 核心概念:Aspect、Advice、Pointcut | 术语大白话表格 |
| 149 | @Around 环绕通知,完全掌控方法 | ControllerLogAspect 的 @Around |
| 150 | execution () 切点表达式精确匹配 | 切点表达式语法说明 |
| 151 | @Around 实战:日志 + 性能统计 | ControllerLogAspect 完整实现 |
| 152 | @Before 前置通知 | ServiceLogAspect 的 @Before |
| 153 | @AfterReturning 返回后通知 | ServiceLogAspect 的 @AfterReturning |
| 154 | @AfterThrowing 集中异常日志 | ServiceLogAspect 的 @AfterThrowing |
| 155 | 注解式 AOP,更干净易读 | @MethodLog 注解 + AnnotationLogAspect |

浙公网安备 33010602011771号