SpringAOP 切面学习笔记
一、AOP 基础概念
1. 核心名词
| 名词 | 释义 | 本项目对应 |
|---|---|---|
| 切面 Aspect | 封装统一横切逻辑的类,@Aspect+@Component | ExportLimitAspect、SqlBase64Aspect |
| 切点 Pointcut | 匹配哪些类 / 方法需要增强,execution 表达式 | execution(* ...exportFile(..)) |
| 通知 Advice | 切点匹配后执行的增强逻辑 | @Around 环绕通知(最常用)、@Before、@After |
| 连接点 JoinPoint | 被拦截的原始方法 | exportFile()、findDataCount() |
| 目标对象 | 被代理的类(Jar 包内 Controller) | Query_datasetController |
横切:非业务主干、通用逻辑:参数预处理、权限、导出条数校验、加解密、日志、限流。
二、切面开发标准步骤(实操流程)
创建切面类,注册 SpringBean
@Component:交给 SpringIOC 容器管理(切面本质也是 Bean,必须被扫描实例化)@Aspect:标识当前类为 AOP 切面类
java
@Aspect
@Component
public class ExportLimitAspect{}
定义切点表达式 execution
格式:execution(返回值 全类名.方法名(参数))
java
// 匹配Jar中Controller指定方法
@Around("execution(* com.mingyoutech.mybi.boot.query.dataset.controller.Query_datasetController.exportFile(..))")
*:任意返回值(..):任意参数个数、任意参数类型- 支持多切点
||拼接:方法1() || 方法2() - 里面的方法就是切点要处理的方法
- @Around环绕通知->方法前后都会处理
各种通知类型,编写增强逻辑
| 注解 | 执行时机 | 适用场景 | 本项目使用 |
|---|---|---|---|
| @Before | 目标方法执行前 | 参数校验、预处理 | SqlBase64 解码参数 |
| @AfterReturning | 方法正常返回后 | 结果处理 | 极少使用 |
| @AfterThrowing | 方法异常抛出后 | 异常统一记录 | 异常日志 |
| @After | 方法结束 (无论异常) | 资源释放 | 关闭流 |
| @Around | 环绕:方法前后都可拦截,可放行 / 终止 | 参数校验、拦截终止请求(导出条数限制) | ✅本项目导出限制、Sql 解码全部使用 |
@Around 固定模板
java
public Object around(ProceedingJoinPoint joinPoint)throws Throwable{
// 1.前置增强逻辑
Object[] args = joinPoint.getArgs();//获取原始入参
// 自定义拦截:满足条件抛异常,不走原方法
if(超限){throw new ServiceException("QUERY0002");}
// 放行原目标方法
Object result = joinPoint.proceed(args);
// 后置增强逻辑
return result;
}
joinPoint.proceed(args):执行原方法;不调用 = 直接截断目标方法(核心拦截原理)。
其他通知类型例子
切点统一:拦截
com.mingyoutech.service..*.*(..)下所有 service 方法
java
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AllAspectDemo {
private static final Logger log = LoggerFactory.getLogger(AllAspectDemo.class);
// 公共切点表达式,复用
@Pointcut("execution(* com.mingyoutech.service..*.*(..))")
public void pointCut(){}
/**
* 1. @Before 前置通知:目标方法运行【之前】执行
* 拿不到返回值;目标方法抛异常,这个也已经执行过了
*/
@Before("pointCut()")
public void before(JoinPoint joinPoint){
// 获取方法名、入参
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("@Before 前置通知,方法名:{},入参:{}", methodName, args);
}
/**
* 2. @AfterReturning 返回通知:方法【正常执行完毕返回后】执行
* ⚠️ 如果目标方法抛出异常,此方法不会跑!
* returning = "retObj" 用来接收目标方法的返回值
*/
@AfterReturning(value = "pointCut()", returning = "retObj")
public void afterReturning(JoinPoint joinPoint, Object retObj){
log.info("@AfterReturning 返回通知,拿到返回值:{}", retObj);
// 这里可以读取返回值,但**不能修改返回值**!!!
// 注意:@AfterReturning只能读取,无法改变上层的返回结果;想修改返回只能用@Around
}
/**
* 3. @AfterThrowing 异常通知:目标方法抛出异常的时候才执行
* throwing = "ex" 接收抛出的异常对象
*/
@AfterThrowing(value = "pointCut()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Throwable ex){
log.error("@AfterThrowing 异常通知,捕获异常:", ex);
}
/**
* 4. @After 最终通知:相当于finally
* ✅不管方法正常返回 OR 抛出异常,一定会执行
* 拿不到返回值、拿不到异常;做资源释放、记录日志
*/
@After("pointCut()")
public void after(JoinPoint joinPoint){
log.info("@After 最终通知,一定会执行");
}
/**
* 5. @Around 环绕通知:功能最全,包裹目标方法
* ProceedingJoinPoint 只能用在@Around
* 可以控制:要不要执行原方法、修改入参、修改返回值、捕获异常
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("@Around >>> 环绕:目标方法执行【之前】");
Object[] args = joinPoint.getArgs();
Object result;
try {
// 执行原始目标方法
result = joinPoint.proceed(args);
log.info("@Around >>> 环绕:方法正常执行完成,原始返回值={}", result);
// ✅环绕独有的能力:修改返回值,替换上层返回
// result = R.success("切面修改后的返回");
} catch (Throwable e) {
log.error("@Around >>> 环绕:捕获业务异常", e);
// 可以选择:继续抛出异常 或者 返回自定义对象
throw e;
} finally {
log.info("@Around >>> 环绕:finally块,无论成功失败都执行");
}
log.info("@Around >>> 环绕:返回处理后结果");
return result;
}
}
执行顺序(目标方法抛出异常)
1. @Around 前置部分
2. @Before
3. 【执行业务目标方法】
4. @Around 拿到proceed返回之后的代码
5. @AfterReturning
6. @After
7. @Around return
执行顺序(目标方法抛出异常)
1. @Around 前置部分
2. @Before
3. 【执行业务目标方法 → 抛出异常】
4. @Around catch块执行
5. @AfterThrowing
6. @After
⚠️ @AfterReturning 不会执行!
三、切面使用时机(什么时候选用 AOP)
适合使用 AOP 场景(横向公共逻辑)
- 参数统一预处理:SqlBase64 解码(项目现有切面)、参数脱敏、参数格式转换
- 接口拦截校验:导出条数上限校验、接口访问白名单、权限拦截、防重复提交
- 全局通用处理:接口耗时统计、统一日志记录、全局异常捕获
- 多接口复用规则:同一个校验规则作用在多个接口(findDataCount+exportFile 双接口导出校验)
四、开发注意事项(高频踩坑点)
1、切点相关
- 拦截 Jar 包内类 :全限定类名必须写完整包路径,不能简写;必须开启
proxyTargetClass=true - 参数下标固定:
args[下标]严格对应接口参数顺序,接口修改参数顺序切面同步出错
java
// exportFile(HttpServletResponse response,String jdbcid,String format,Map param)
args[0]=response、args[1]=jdbcid、args[3]=param
- 不要用
this内部调用:同类中 A 方法调用 B 方法,B 方法切面无法拦截(AOP 代理失效)
2、入参空指针问题
获取 param 前先判空:
java
if(param!=null&¶m.containsKey("queryid"))
3、SpringBean 注入
- 切面必须
@Component,否则 Spring 不实例化、切面不生效; - final 修饰成员变量:强制构造注入,避免空指针;
- Jar 中的 Service 可以正常注入,SpringIoC 可扫描第三方 Jar 的 @Service。
4、@Around 关键注意
proceed()只能调用一次,多次调用会重复执行原接口;- 抛出异常直接终止接口,和业务全局异常处理器联动;
- 若需要修改入参:修改
args数组元素,proceed(新args)传入修改后的参数。
5、事务失效
切面和事务注解 @Transactional 顺序:AOP 外层、事务内层;切面抛异常会触发事务回滚。
五、速记总结
- 切面三板斧:
@Component+@Aspect+ 切点表达式 + @Around 通知; - 拦截 Jar:
proxyTargetClass=true; - 多切面排序:@Order;
- @Around 核心:proceed 放行、抛异常拦截;
- 公共通用逻辑用 AOP,私有业务写在方法内。