系列文章第2篇 | 共3篇
难度:⭐⭐⭐ | 适合人群:想深入理解Spring AOP的开发者
📝 上期回顾
上一篇我们学习了:
- ✅ 代理模式的概念
- ✅ JDK动态代理(基于接口)
- ✅ CGLIB动态代理(基于继承)
- ✅ 两种代理方式的对比
- ✅ Spring的选择策略
上期思考题解答:
Q1: 为什么final方法无法被代理?
A: CGLIB通过继承实现代理,子类无法重写父类的final方法,所以无法代理。JDK代理也无法代理接口中声明为default final的方法。
Q2: 既有接口又有非接口方法会怎样?
A: 如果用JDK代理,只能代理接口中的方法,其他方法无法代理。如果用CGLIB,所有public非final方法都能代理。
Q3: this调用为什么AOP不生效?
A: 今天详细解答!
💥 开场:一次权限校验的"惨案"
时间: 周二下午
地点: 办公室
事件: 线上安全漏洞
安全部门: "你们的删除订单接口没做权限校验,普通用户可以删除任何订单!"
我: "不可能啊,我明明加了权限注解..." 😰
代码是这样的:
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
/**
* 删除订单(需要管理员权限)
*/
@RequireRole("ADMIN") // 自定义权限注解
public void deleteOrder(Long orderId) {
orderDao.delete(orderId);
System.out.println("订单删除成功");
}
/**
* 批量删除
*/
public void batchDelete(List<Long> orderIds) {
for (Long id : orderIds) {
this.deleteOrder(id); // 调用deleteOrder
}
}
}
哈吉米: "你通过batchDelete删除,权限校验不会生效。"
我: "为什么?我调用了deleteOrder啊,它有@RequireRole注解。" 🤔
南北绿豆: "因为你用this.deleteOrder()调用的,this是原始对象,不是代理对象!"
阿西噶阿西画了个图:
客户端调用
↓
orderService(这是代理对象)
↓
代理逻辑执行(权限校验) ✅
↓
batchDelete()方法
↓
this.deleteOrder() ← this是原始对象!
↓
直接调用原始方法,没经过代理 ❌
↓
权限校验不生效!
我: "原来如此!那怎么解决?" 💡
哈吉米: "要么注入自己,要么用AopContext.currentProxy(),但这些都是治标不治本。要真正理解AOP,得从@Aspect注解开始学..."
🎯 第一问:Spring AOP核心概念
AOP是什么?
AOP = Aspect Oriented Programming(面向切面编程)
南北绿豆: "我用一个场景给你解释。"
场景: 你要给所有Service方法加日志
传统方式(OOP):
public class UserService {
public void createUser() {
System.out.println(">>> 日志开始"); // 重复代码
// 业务逻辑
System.out.println("<<< 日志结束"); // 重复代码
}
public void deleteUser() {
System.out.println(">>> 日志开始"); // 重复代码
// 业务逻辑
System.out.println("<<< 日志结束"); // 重复代码
}
}
public class OrderService {
public void createOrder() {
System.out.println(">>> 日志开始"); // 重复代码
// 业务逻辑
System.out.println("<<< 日志结束"); // 重复代码
}
}
// 100个Service,每个方法都要写日志代码...
问题: 代码重复、难维护!
AOP方式:
// 业务代码:只关注业务逻辑
public class UserService {
public void createUser() {
// 只写业务逻辑
}
}
// 日志代码:统一在切面中处理
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
System.out.println(">>> 日志开始");
Object result = pjp.proceed();
System.out.println("<<< 日志结束");
return result;
}
}
优势:
- ✅ 业务代码干净
- ✅ 日志逻辑集中
- ✅ 易于维护
AOP核心概念
阿西噶阿西: "AOP有6个核心概念。"
1. 切面(Aspect)
@Aspect // ← 这就是一个切面
@Component
public class LogAspect {
// 包含切点和通知
}
定义: 切面 = 切点 + 通知
类比: 切面就是一个"功能模块"(如日志模块、权限模块)
2. 连接点(Join Point)
定义: 程序执行过程中可以插入切面的点
可以是:
- 方法调用
- 方法执行
- 字段访问
- 异常抛出
Spring AOP只支持方法执行级别的连接点!
public void createUser() { // ← 这个方法的执行就是一个连接点
// ...
}
3. 切点(Pointcut)
定义: 匹配连接点的表达式
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
作用: 定义"哪些方法"需要被增强
类比: 切点就是"过滤器",选出需要增强的方法
4. 通知(Advice)
定义: 在切点处执行的代码
5种类型:
- @Before - 前置通知
- @After - 后置通知
- @AfterReturning - 返回通知
- @AfterThrowing - 异常通知
- @Around - 环绕通知
5. 目标对象(Target)
@Service
public class UserService { // ← 这就是目标对象
public void createUser() {
// 被代理的对象
}
}
6. 织入(Weaving)
定义: 将切面应用到目标对象创建代理的过程
时机:
- 编译期(AspectJ)
- 类加载期(AspectJ)
- 运行期(Spring AOP)← Spring用这个
概念关系图
切面(Aspect)
├─ 切点(Pointcut)
│ └─ 匹配哪些方法
└─ 通知(Advice)
└─ 做什么增强
↓
应用到目标对象
↓
通过织入创建代理
↓
在连接点执行增强
🎨 第二问:5种通知类型详解
准备工作
创建测试Service:
@Service
public class UserService {
public String createUser(String name) {
System.out.println(" [业务方法] 创建用户:" + name);
return "User-" + name;
}
public void deleteUser(Long id) {
System.out.println(" [业务方法] 删除用户:" + id);
if (id == 0) {
throw new IllegalArgumentException("ID不能为0");
}
}
}
类型1:@Before(前置通知)
定义: 在目标方法执行之前执行
@Aspect
@Component
public class BeforeAspect {
@Before("execution(* com.example.service.UserService.*(..))")
public void before(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println(">>> 【Before】方法:" + methodName);
System.out.println(" 参数:" + Arrays.toString(args));
}
}
测试:
userService.createUser("张三");
输出:
>>> 【Before】方法:createUser
参数:[张三]
[业务方法] 创建用户:张三
特点:
- 在方法执行前执行
- 无法阻止方法执行
- 无法修改返回值
类型2:@After(后置通知)
定义: 在目标方法执行之后执行(无论成功还是异常)
@Aspect
@Component
public class AfterAspect {
@After("execution(* com.example.service.UserService.*(..))")
public void after(JoinPoint joinPoint) {
System.out.println("<<< 【After】方法:" + joinPoint.getSignature().getName());
System.out.println(" 无论成功还是异常都会执行");
}
}
测试:
// 正常执行
userService.createUser("张三");
// 抛异常
try {
userService.deleteUser(0L);
} catch (Exception e) {
System.out.println("捕获异常:" + e.getMessage());
}
输出:
[业务方法] 创建用户:张三
<<< 【After】方法:createUser
无论成功还是异常都会执行
[业务方法] 删除用户:0
<<< 【After】方法:deleteUser
无论成功还是异常都会执行
捕获异常:ID不能为0
特点:
- 类似于finally块
- 无论成功失败都执行
- 无法获取返回值
类型3:@AfterReturning(返回通知)
定义: 在目标方法正常返回后执行
@Aspect
@Component
public class AfterReturningAspect {
@AfterReturning(
pointcut = "execution(* com.example.service.UserService.*(..))",
returning = "result" // 绑定返回值
)
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("<<< 【AfterReturning】方法:" +
joinPoint.getSignature().getName());
System.out.println(" 返回值:" + result);
}
}
测试:
// 正常返回
String user = userService.createUser("张三");
// 抛异常
try {
userService.deleteUser(0L);
} catch (Exception e) {
System.out.println("捕获异常");
}
输出:
[业务方法] 创建用户:张三
<<< 【AfterReturning】方法:createUser
返回值:User-张三
[业务方法] 删除用户:0
捕获异常
(AfterReturning不执行)
特点:
- 只在正常返回时执行
- 可以获取返回值
- 无法修改返回值(只读)
类型4:@AfterThrowing(异常通知)
定义: 在目标方法抛异常后执行
@Aspect
@Component
public class AfterThrowingAspect {
@AfterThrowing(
pointcut = "execution(* com.example.service.UserService.*(..))",
throwing = "ex" // 绑定异常
)
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
System.out.println("<<< 【AfterThrowing】方法:" +
joinPoint.getSignature().getName());
System.out.println(" 异常:" + ex.getClass().getSimpleName());
System.out.println(" 消息:" + ex.getMessage());
}
}
测试:
// 正常执行
userService.createUser("张三");
// 抛异常
try {
userService.deleteUser(0L);
} catch (Exception e) {
System.out.println("已捕获异常");
}
输出:
[业务方法] 创建用户:张三
(AfterThrowing不执行)
[业务方法] 删除用户:0
<<< 【AfterThrowing】方法:deleteUser
异常:IllegalArgumentException
消息:ID不能为0
已捕获异常
特点:
- 只在抛异常时执行
- 可以获取异常对象
- 可以记录异常日志
类型5:@Around(环绕通知)
定义: 包围目标方法,最强大的通知类型
@Aspect
@Component
public class AroundAspect {
@Around("execution(* com.example.service.UserService.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
System.out.println(">>> 【Around-Before】方法:" + methodName);
System.out.println(" 参数:" + Arrays.toString(args));
Object result = null;
try {
// 执行目标方法
result = pjp.proceed();
System.out.println("<<< 【Around-AfterReturning】返回值:" + result);
} catch (Exception e) {
System.out.println("<<< 【Around-AfterThrowing】异常:" + e.getMessage());
throw e;
} finally {
System.out.println("<<< 【Around-After】方法结束\n");
}
return result;
}
}
测试:
userService.createUser("张三");
输出:
>>> 【Around-Before】方法:createUser
参数:[张三]
[业务方法] 创建用户:张三
<<< 【Around-AfterReturning】返回值:User-张三
<<< 【Around-After】方法结束
特点:
- 最强大,可以完全控制方法执行
- 可以决定是否执行目标方法
- 可以修改参数
- 可以修改返回值
- 可以捕获异常
5种通知对比
| 通知类型 | 执行时机 | 获取返回值 | 修改返回值 | 捕获异常 | 阻止执行 |
|---|---|---|---|---|---|
| @Before | 方法前 | ❌ | ❌ | ❌ | ❌ |
| @After | 方法后(finally) | ❌ | ❌ | ❌ | ❌ |
| @AfterReturning | 正常返回后 | ✅ | ❌ | ❌ | ❌ |
| @AfterThrowing | 异常后 | ❌ | ❌ | ✅ | ❌ |
| @Around | 环绕方法 | ✅ | ✅ | ✅ | ✅ |
通知执行顺序
哈吉米: "如果多个通知同时存在,执行顺序是怎样的?"
正常情况(无异常):
@Around - Before
↓
@Before
↓
【目标方法】
↓
@Around - After
↓
@After
↓
@AfterReturning
异常情况:
@Around - Before
↓
@Before
↓
【目标方法抛异常】
↓
@Around - AfterThrowing
↓
@After
↓
@AfterThrowing
完整验证:
@Aspect
@Component
public class OrderAspect {
@Pointcut("execution(* com.example.service.UserService.createUser(..))")
public void pointcut() {}
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("1. Around - Before");
try {
Object result = pjp.proceed();
System.out.println("5. Around - AfterReturning");
return result;
} catch (Exception e) {
System.out.println("5. Around - AfterThrowing");
throw e;
} finally {
System.out.println("4. Around - After");
}
}
@Before("pointcut()")
public void before() {
System.out.println("2. Before");
}
@After("pointcut()")
public void after() {
System.out.println("6. After");
}
@AfterReturning("pointcut()")
public void afterReturning() {
System.out.println("7. AfterReturning");
}
}
测试:
userService.createUser("张三");
输出:
1. Around - Before
2. Before
[业务方法] 创建用户:张三
5. Around - AfterReturning
4. Around - After
6. After
7. AfterReturning
顺序完全验证! ✅
📐 第三问:切点表达式详解
execution表达式(最常用)
完整语法:
execution(modifiers? return-type declaring-type?method-name(param-types) throws?)
modifiers:修饰符(public、private等)
return-type:返回类型
declaring-type:类名
method-name:方法名
param-types:参数类型
throws:异常类型
南北绿豆: "我们看几个实际例子。"
示例1:匹配所有public方法
@Pointcut("execution(public * *(..))")
示例2:匹配指定包下的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
// ↓ ↓ ↓ ↓ ↓
// 返回类型 包名 类 方法 参数
// 解释:
// * - 任意返回类型
// com.example.service.* - service包下的所有类
// .* - 所有方法
// (..) - 任意参数
示例3:匹配指定类的所有方法
@Pointcut("execution(* com.example.service.UserService.*(..))")
// ↑
// 具体类名
示例4:匹配指定方法名
@Pointcut("execution(* com.example.service.*.create*(..))")
// ↑
// 方法名以create开头
示例5:匹配指定参数
// 只有一个String参数
@Pointcut("execution(* com.example.service.*.*(String))")
// 第一个参数是String
@Pointcut("execution(* com.example.service.*.*(String, ..))")
// 无参方法
@Pointcut("execution(* com.example.service.*.*())")
示例6:匹配指定返回类型
// 返回String
@Pointcut("execution(String com.example.service.*.*(..))")
// 返回void
@Pointcut("execution(void com.example.service.*.*(..))")
@annotation表达式(推荐)
场景: 匹配带有特定注解的方法
自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
切点:
@Pointcut("@annotation(com.example.annotation.Log)")
public void logMethods() {}
使用:
@Service
public class UserService {
@Log("创建用户") // ← 加了注解,会被拦截
public void createUser(String name) {
System.out.println("创建用户:" + name);
}
public void deleteUser(Long id) { // 没加注解,不会被拦截
System.out.println("删除用户:" + id);
}
}
优点:
- ✅ 精确控制哪些方法需要增强
- ✅ 不依赖包名、方法名
- ✅ 更灵活
within表达式
匹配指定类型内的方法:
// 匹配UserService类中的所有方法
@Pointcut("within(com.example.service.UserService)")
// 匹配service包及子包下的所有方法
@Pointcut("within(com.example.service..*)")
args表达式
匹配参数类型:
// 第一个参数是String
@Pointcut("args(String, ..)")
// 只有一个Long参数
@Pointcut("args(Long)")
组合表达式
// && 与
@Pointcut("execution(* com.example.service.*.*(..)) && args(String)")
// || 或
@Pointcut("execution(* create*(..)) || execution(* save*(..))")
// ! 非
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* delete*(..))")
💻 第四问:实战案例
案例1:自定义日志切面
@Aspect
@Component
@Slf4j
public class LogAspect {
/**
* 切点:所有Controller方法
*/
@Pointcut("execution(* com.example.controller.*.*(..))")
public void controllerMethods() {}
/**
* 环绕通知:记录请求日志
*/
@Around("controllerMethods()")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
// 获取方法信息
String className = pjp.getTarget().getClass().getSimpleName();
String methodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
// 记录请求
log.info(">>> 请求开始:{}.{}", className, methodName);
log.info(" 参数:{}", Arrays.toString(args));
long start = System.currentTimeMillis();
Object result = null;
try {
// 执行方法
result = pjp.proceed();
long end = System.currentTimeMillis();
log.info("<<< 请求成功:{}.{}", className, methodName);
log.info(" 返回值:{}", result);
log.info(" 耗时:{}ms", (end - start));
} catch (Exception e) {
long end = System.currentTimeMillis();
log.error("<<< 请求失败:{}.{}", className, methodName);
log.error(" 异常:{}", e.getMessage());
log.error(" 耗时:{}ms", (end - start));
throw e;
}
return result;
}
}
案例2:自定义权限校验切面
自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String value(); // 需要的角色
}
切面实现:
@Aspect
@Component
public class RoleAspect {
@Around("@annotation(requireRole)")
public Object checkRole(ProceedingJoinPoint pjp, RequireRole requireRole)
throws Throwable {
String requiredRole = requireRole.value();
// 获取当前用户角色(简化处理)
String currentRole = getCurrentUserRole();
System.out.println(">>> 【权限校验】需要角色:" + requiredRole);
System.out.println(" 当前角色:" + currentRole);
// 校验权限
if (!requiredRole.equals(currentRole)) {
System.out.println("<<< 【权限不足】拒绝访问!\n");
throw new SecurityException("权限不足,需要" + requiredRole + "角色");
}
System.out.println("<<< 【权限通过】允许访问");
// 执行方法
return pjp.proceed();
}
private String getCurrentUserRole() {
// 实际项目从SecurityContext或Session获取
return "USER"; // 模拟普通用户
}
}
使用:
@Service
public class OrderService {
@RequireRole("ADMIN") // 需要管理员角色
public void deleteOrder(Long orderId) {
System.out.println(" [业务方法] 删除订单:" + orderId);
}
@RequireRole("USER") // 需要普通用户角色
public void viewOrder(Long orderId) {
System.out.println(" [业务方法] 查看订单:" + orderId);
}
}
测试:
// 测试1:普通用户查看订单(有权限)
orderService.viewOrder(123L);
// 测试2:普通用户删除订单(无权限)
try {
orderService.deleteOrder(123L);
} catch (SecurityException e) {
System.out.println("捕获异常:" + e.getMessage());
}
输出:
>>> 【权限校验】需要角色:USER
当前角色:USER
<<< 【权限通过】允许访问
[业务方法] 查看订单:123
>>> 【权限校验】需要角色:ADMIN
当前角色:USER
<<< 【权限不足】拒绝访问!
捕获异常:权限不足,需要ADMIN角色
完美! ✨
案例3:性能监控切面
@Aspect
@Component
public class PerformanceAspect {
/**
* 监控service层方法性能
*/
@Around("execution(* com.example.service.*.*(..))")
public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().toShortString();
long start = System.currentTimeMillis();
Object result = pjp.proceed();
long end = System.currentTimeMillis();
long cost = end - start;
// 如果耗时超过100ms,打印警告
if (cost > 100) {
System.out.println("⚠️ 【性能警告】方法:" + methodName);
System.out.println(" 耗时:" + cost + "ms(超过阈值100ms)");
}
return result;
}
}
🎓 第五问:JoinPoint详解
JoinPoint vs ProceedingJoinPoint
哈吉米: "注意区别!"
// JoinPoint:用于Before、After、AfterReturning、AfterThrowing
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
// 可以获取方法信息,但不能控制方法执行
}
// ProceedingJoinPoint:只用于Around
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 可以控制方法执行
return pjp.proceed(); // 执行目标方法
}
JoinPoint常用方法
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
// 1. 获取方法签名
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
String className = signature.getDeclaringTypeName();
// 2. 获取方法参数
Object[] args = joinPoint.getArgs();
// 3. 获取目标对象
Object target = joinPoint.getTarget();
// 4. 获取代理对象
Object proxy = joinPoint.getThis();
// 5. 获取连接点类型
String kind = joinPoint.getKind(); // method-execution
System.out.println("类名:" + className);
System.out.println("方法名:" + methodName);
System.out.println("参数:" + Arrays.toString(args));
System.out.println("目标对象:" + target.getClass().getSimpleName());
System.out.println("代理对象:" + proxy.getClass().getSimpleName());
}
ProceedingJoinPoint特有方法
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 1. 执行目标方法(原始参数)
Object result = pjp.proceed();
// 2. 执行目标方法(修改参数)
Object[] newArgs = new Object[]{"修改后的参数"};
result = pjp.proceed(newArgs);
return result;
}
💡 知识点总结
本篇你学到了什么?
✅ AOP核心概念
- 切面(Aspect)= 切点 + 通知
- 连接点(Join Point)= 可以插入切面的点
- 切点(Pointcut)= 匹配连接点的表达式
- 通知(Advice)= 增强逻辑
- 目标对象(Target)= 被代理的对象
- 织入(Weaving)= 创建代理的过程
✅ 5种通知类型
- @Before - 前置通知
- @After - 后置通知(finally)
- @AfterReturning - 返回通知
- @AfterThrowing - 异常通知
- @Around - 环绕通知(最强大)
✅ 通知执行顺序
- 正常:Around-Before → Before → 方法 → Around-After → After → AfterReturning
- 异常:Around-Before → Before → 方法 → Around-AfterThrowing → After → AfterThrowing
✅ 切点表达式
- execution - 方法匹配(最常用)
- @annotation - 注解匹配(推荐)
- within - 类型匹配
- args - 参数匹配
✅ 实战案例
- 日志切面
- 权限校验切面
- 性能监控切面
记忆口诀
Before方法前,After像finally。
AfterReturning成功返,AfterThrowing异常抛。
Around最强大,控制全流程。
execution匹配方法,annotation更灵活。
JoinPoint获信息,Proceeding能执行。
🤔 思考题
问题1: 如果一个方法上有多个切面,执行顺序如何控制?
提示: @Order注解
问题2: @Around能完全替代其他4种通知吗?为什么还要有其他类型?
问题3: Spring AOP的代理对象是什么时候创建的?在Bean生命周期的哪个阶段?
提示: 下一篇会详细解答,并深入源码!
📢 下期预告(系列完结篇)
《Spring AOP原理(三):源码剖析代理创建时机,AOP失效场景全解!》
下一篇我们将:
- 深入AbstractAutoProxyCreator源码
- 分析代理对象的创建时机
- 详解AOP失效的8种场景
- 每种场景的解决方案
- this调用问题的本质
- 系列知识点总结
系列完结篇,彻底搞懂Spring AOP! 🚀
💬 互动时间
你最常用哪种通知类型?
有没有遇到过AOP不生效的情况?
对切点表达式还有疑问吗?
欢迎评论区讨论!💭
觉得有帮助?三连支持: 👍 点赞 | ⭐ 收藏 | 🔄 转发
看完这篇,@Aspect注解完全掌握! ✨
下一篇见(完结篇)! 👋