目录
[Spring AOP 超详细入门教程:从概念到源码](#Spring AOP 超详细入门教程:从概念到源码)
[1. AOP基础概念(先理解思想)](#1. AOP基础概念(先理解思想))
[1.1 什么是AOP?(生活化理解)](#1.1 什么是AOP?(生活化理解))
[1.2 AOP核心术语(必须掌握)](#1.2 AOP核心术语(必须掌握))
[2. Spring AOP快速入门(第一个完整例子)](#2. Spring AOP快速入门(第一个完整例子))
[2.1 添加依赖(项目基础)](#2.1 添加依赖(项目基础))
[2.2 创建业务Controller(被增强的目标)](#2.2 创建业务Controller(被增强的目标))
[2.3 创建切面类(这是重点!)](#2.3 创建切面类(这是重点!))
[2.4 运行测试(看效果)](#2.4 运行测试(看效果))
[3. Spring AOP核心概念详解(代码级深扒)](#3. Spring AOP核心概念详解(代码级深扒))
[3.1 切点(Pointcut) - "规则定义器"](#3.1 切点(Pointcut) - "规则定义器")
[3.2 连接点(Join Point) - "符合条件的具体方法"](#3.2 连接点(Join Point) - "符合条件的具体方法")
[3.3 通知(Advice) - "增强的具体逻辑"](#3.3 通知(Advice) - "增强的具体逻辑")
[3.4 切面(Aspect) - "切点 + 通知的集合"](#3.4 切面(Aspect) - "切点 + 通知的集合")
[4. 通知执行顺序(代码验证结论)](#4. 通知执行顺序(代码验证结论))
[4.1 同一切面内的执行顺序](#4.1 同一切面内的执行顺序)
[4.2 多个切面的执行顺序(@Order注解)](#4.2 多个切面的执行顺序(@Order注解))
[5. 切点表达式详解(精确匹配)](#5. 切点表达式详解(精确匹配))
[5.1 execution表达式(最常用)](#5.1 execution表达式(最常用))
[5.2 @annotation表达式(注解匹配)](#5.2 @annotation表达式(注解匹配))
[6. Spring AOP实现原理(源码级理解)](#6. Spring AOP实现原理(源码级理解))
[6.1 代理模式基础(先理解设计模式)](#6.1 代理模式基础(先理解设计模式))
[6.1.1 静态代理(手动写代理类)](#6.1.1 静态代理(手动写代理类))
[6.1.2 动态代理(自动生成代理类)](#6.1.2 动态代理(自动生成代理类))
[6.2 Spring AOP如何选择代理方式?(源码逻辑)](#6.2 Spring AOP如何选择代理方式?(源码逻辑))
[6.3 代理执行流程(方法调用时发生了什么)](#6.3 代理执行流程(方法调用时发生了什么))
[7. 完整项目实战(整合所有知识点)](#7. 完整项目实战(整合所有知识点))
[7.1 项目结构](#7.1 项目结构)
[7.2 完整代码(带全套注释)](#7.2 完整代码(带全套注释))
[7.3 测试结果分析(结论与代码对应)](#7.3 测试结果分析(结论与代码对应))
[8. 常见问题与解决方案(代码级避坑)](#8. 常见问题与解决方案(代码级避坑))
[8.1 同一个类内方法调用,AOP失效](#8.1 同一个类内方法调用,AOP失效)
[8.2 通知中抛出异常,导致原方法无法执行](#8.2 通知中抛出异常,导致原方法无法执行)
[8.3 切点表达式过于宽泛,影响性能](#8.3 切点表达式过于宽泛,影响性能)
[9. 总结(知识地图)](#9. 总结(知识地图))
[9.1 核心概念关系图](#9.1 核心概念关系图)
[9.2 代理方式选择决策树](#9.2 代理方式选择决策树)
[9.3 最佳实践清单](#9.3 最佳实践清单)
[10. 最后的叮嘱(给新手的建议)](#10. 最后的叮嘱(给新手的建议))
[Spring Boot 3.x AOP 完整实战代码包](#Spring Boot 3.x AOP 完整实战代码包)
[一、Maven 坐标 (pom.xml)](#一、Maven 坐标 (pom.xml))
[三、Java 源码](#三、Java 源码)
[1. 启动类 (DemoApplication.java)](#1. 启动类 (DemoApplication.java))
[2. 自定义注解 (LogExecutionTime.java)](#2. 自定义注解 (LogExecutionTime.java))
[3. 切面类 (Aspects)](#3. 切面类 (Aspects))
[3.1 日志切面 (LogAspect.java)](#3.1 日志切面 (LogAspect.java))
[3.2 耗时统计切面 (TimeAspect.java)](#3.2 耗时统计切面 (TimeAspect.java))
[3.3 安全/权限切面 (SecurityAspect.java)](#3.3 安全/权限切面 (SecurityAspect.java))
[4. 控制层 (Controllers)](#4. 控制层 (Controllers))
[4.1 用户控制器 (UserController.java)](#4.1 用户控制器 (UserController.java))
[4.2 订单控制器 (OrderController.java)](#4.2 订单控制器 (OrderController.java))
[4.3 测试控制器 (HelloController.java)](#4.3 测试控制器 (HelloController.java))
[Spring AOP 实战回顾:你其实已经掌握了什么?](#Spring AOP 实战回顾:你其实已经掌握了什么?)
[一、从日志里能直接验证的 6 个核心结论](#一、从日志里能直接验证的 6 个核心结论)
Spring AOP 超详细入门教程:从概念到源码
写给新手的话
你好!我是你的Java学习伙伴。这份教程将用最白话的语言、最详细的代码注释,带你彻底搞懂Spring AOP。每个概念我们都会先讲"是什么",再看"代码怎么写",最后分析"代码如何体现出这个概念"。
1. AOP基础概念(先理解思想)
1.1 什么是AOP?(生活化理解)
AOP = 面向切面编程,听起来很抽象对吧?我们来举几个生活中的例子:
例子1:餐厅服务员
你点完菜后,服务员会:
前后都要说"您好"/"请慢用"(环绕通知)
上菜前检查餐具是否干净(前置通知)
上菜后主动询问是否需要加调料(后置通知)
如果菜品有问题,立即处理投诉(异常通知)
这些额外服务不干扰厨师做菜,但增强了用餐体验。这就是AOP思想!
例子2:手机壳
手机(核心业务)能打电话、发短信
手机壳(切面)提供防摔、美观功能
你不需要改造手机内部电路,就能增强功能
1.2 AOP核心术语(必须掌握)
|---------------------|---------------|-----------------------------------------------|
| 术语 | 通俗解释 | 代码中的体现 |
| 切点(Pointcut) | "哪些方法要增强"的规则 |@Around("execution(* com.example.*.*(..))")|
| 连接点(Join Point) | 符合规则的具体方法 |BookController.addBook()|
| 通知(Advice) | "增强什么功能"的代码 |recordTime()方法里的计时逻辑 |
| 切面(Aspect) | 切点 + 通知的组合 | 整个TimeAspect类 |
| 织入(Weaving) | 把增强代码"织"到原方法上 | Spring自动完成的代理过程 |2. Spring AOP快速入门(第一个完整例子)
2.1 添加依赖(项目基础)
XML<!-- pom.xml文件 --> <!-- spring-boot-starter-aop:Spring Boot提供的AOP启动器,包含了所有AOP相关依赖 --> <!-- 添加后无需任何配置,Spring Boot会自动开启AOP功能 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>依赖如何体现AOP思想?
这个依赖就像"餐厅服务员培训手册",告诉Spring:"你要准备好提供额外服务的能力"
它内部包含了
spring-aop、aspectjweaver等核心库,相当于服务员的工具包2.2 创建业务Controller(被增强的目标)
java// BookController.java // 这个类就是"厨师",只关心核心业务(做菜/处理图书) import org.springframework.web.bind.annotation.RequestMapping; // Spring MVC的请求映射注解,把HTTP请求映射到方法 import org.springframework.web.bind.annotation.RestController; // 标识这是RESTful控制器,返回JSON数据 @RequestMapping("/book") // 所有请求路径的前缀都是/book @RestController // 告诉Spring:这个类是控制器,所有方法返回的数据直接写给客户端(相当于@Controller + @ResponseBody) public class BookController { // 处理添加图书的请求 @RequestMapping("/addBook") // 映射/book/addBook请求 public String addBook() { // 模拟业务逻辑耗时 try { Thread.sleep(100); // 让当前线程暂停100毫秒,模拟数据库操作耗时 } catch (InterruptedException e) { e.printStackTrace(); // 打印异常堆栈信息 } return "添加成功"; // 返回给客户端的字符串 } // 处理查询图书的请求 @RequestMapping("/queryBookById") // 映射/book/queryBookById请求 public String queryBookById() { // 模拟业务逻辑耗时 try { Thread.sleep(200); // 模拟更复杂的数据库查询,耗时200毫秒 } catch (InterruptedException e) { e.printStackTrace(); } return "查询成功"; } }代码如何体现"连接点"?
addBook()和queryBookById()就是连接点,因为它们是具体的、可以被AOP控制的方法它们符合
execution(* com.example.demo.controller.*.*(..))这个规则,所以会被增强2.3 创建切面类(这是重点!)
java// TimeAspect.java // 这个类就是"服务员",提供计时服务,不修改厨师的做菜逻辑 import lombok.extern.slf4j.Slf4j; // Lombok提供的日志注解,自动生成log对象 import org.aspectj.lang.ProceedingJoinPoint; // 连接点对象,封装了目标方法的信息和执行能力 import org.aspectj.lang.annotation.Around; // 环绕通知注解,表示在目标方法前后都要执行 import org.aspectj.lang.annotation.Aspect; // 标识这是一个切面类,Spring看到这个注解就知道要做AOP了 import org.springframework.stereotype.Component; // Spring组件注解,让Spring管理这个Bean的生命周期 @Slf4j // Lombok注解,自动生成private static final Logger log = LoggerFactory.getLogger(TimeAspect.class); @Aspect // 标识这是切面类(核心注解!没有它就不是切面) @Component // 把切面类交给Spring容器管理,这样Spring才能识别并启用它 public class TimeAspect { /** * 记录方法耗时(环绕通知) * * @param pjp 连接点对象,可以通过它: * - 获取方法名:pjp.getSignature().getName() * - 获取类名:pjp.getTarget().getClass().getName() * - 执行原方法:pjp.proceed() * - 获取方法参数:pjp.getArgs() * * @return 目标方法的执行结果(必须返回,否则调用者拿不到返回值) * @throws Throwable 可能抛出的异常(必须声明,因为pjp.proceed()可能抛出任何异常) */ // @Around:环绕通知,像三明治一样包裹目标方法 // "execution(* com.example.demo.controller.*.*(..))":切点表达式,匹配controller包下所有类的所有方法 // * :任意返回值 // com.example.demo.controller.*:controller包下的所有类 // .*:所有方法 // (..):任意参数 @Around("execution(* com.example.demo.controller.*.*(..))") public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { // ============ 目标方法执行前 ============ long begin = System.currentTimeMillis(); // 记录开始时间(毫秒) // ============ 执行目标方法 ============ // pjp.proceed():调用原始方法(如addBook()) // 这个方法会暂停当前线程,直到目标方法执行完毕 // 返回值就是目标方法的返回值(如"添加成功") Object result = pjp.proceed(); // ============ 目标方法执行后 ============ long end = System.currentTimeMillis(); // 记录结束时间 // 打印日志:方法签名 + 耗时 // pjp.getSignature():获取方法签名(包含方法名、参数类型等) // end - begin:计算耗时 log.info(pjp.getSignature() + "执行耗时:{}ms", end - begin); // 必须返回结果,否则调用方(如浏览器)收不到返回值 return result; } }代码如何体现"切面"概念?
整个
TimeAspect类就是一个切面,因为它:
有
@Aspect注解(标识身份)包含了切点(
@Around后面的字符串)包含了通知(
recordTime方法体)这三者的组合 = 切面!
代码如何体现"无侵入性"?
注意
BookController里完全没有关于计时的代码计时逻辑完全在
TimeAspect里这就是"不修改源代码就能增强功能",完美体现AOP的核心价值
2.4 运行测试(看效果)
bash// 启动Spring Boot应用后,访问: // http://localhost:8080/book/addBook // http://localhost:8080/book/queryBookById // 控制台输出示例: // 2023-10-05 14:30:22.123 INFO 12345 --- [nio-8080-exec-1] c.e.d.a.TimeAspect: String addBook() 执行耗时:105ms // 2023-10-05 14:30:22.328 INFO 12345 --- [nio-8080-exec-2] c.e.d.a.TimeAspect: String queryBookById() 执行耗时:203ms如何从输出验证AOP生效?
c.e.d.a.TimeAspect:说明日志来自我们的切面类
addBook():成功获取到方法名
105ms:记录了真实耗时(100ms sleep + 5ms代码执行)没有修改
BookController任何一行代码,却有了计时功能!3. Spring AOP核心概念详解(代码级深扒)
3.1 切点(Pointcut) - "规则定义器"
概念:定义"哪些方法要被增强"的规则,使用AspectJ表达式描述。
java// 切点表达式详解:execution(* com.example.demo.controller.*.*(..)) // 语法结构:execution(访问修饰符 返回值 包名.类名.方法名(参数) 异常) // 实际示例:execution(public String com.example.demo.controller.BookController.addBook(BookInfo)) // 通配符说明: // * : 匹配任意单个元素(返回值、包名、类名、方法名) // .. : 匹配任意多个连续的任意符号(包层级、参数个数和类型) // 更多示例: @Around("execution(public * com.example.demo.controller.*.*(..))") // 匹配public方法 @Around("execution(* com.example.demo.controller.BookController.*(..))") // 只匹配BookController类 @Around("execution(* com..controller.*.*(..))") // 匹配com包及其子包下的controller包 @Around("execution(* *.*(..))") // 危险!匹配所有方法(性能差)如何从代码中看出"切点"的作用?
在
@Around后面的字符串就是切点表达式Spring启动时,会扫描所有Bean的方法,把符合这个表达式的方法标记为连接点
比如
BookController.addBook()符合规则,就被织入了计时逻辑3.2 连接点(Join Point) - "符合条件的具体方法"
概念:切点表达式匹配到的每一个具体方法就是连接点。
java// 假设我们有以下类: package com.example.demo.controller; @RestController public class UserController { @RequestMapping("/addUser") public String addUser() { /* ... */ } // 这是连接点,因为匹配规则 @RequestMapping("/delUser") private String delUser() { /* ... */ } // 这不是连接点!private方法无法被代理 } @RestController public class OrderController { @RequestMapping("/addOrder") public String addOrder() { /* ... */ } // 这是连接点 } // 切点表达式:execution(* com.example.demo.controller.*.*(..)) // 匹配结果: // ✔ UserController.addUser() - 匹配(public、在controller包下) // ✘ UserController.delUser() - 不匹配(private修饰) // ✔ OrderController.addOrder() - 匹配 // 代码体现:在TimeAspect中,pjp.getSignature()能获取到的方法名,就是当前正在执行的连接点如何从代码中验证"连接点"?
运行后看日志:
String addBook()、String queryBookById()被打印出来这些就是具体的连接点,每一个都是切点表达式筛选出来的"候选人"
3.3 通知(Advice) - "增强的具体逻辑"
概念:真正要执行的增强代码,Spring提供了5种通知类型。
java// 完整展示5种通知的代码 import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; // 方法签名接口 import org.aspectj.lang.annotation.*; // 导入所有AOP注解 import org.springframework.stereotype.Component; @Slf4j @Component @Aspect public class AllAdviceDemo { // ============ 前置通知 ============ // 在目标方法**之前**执行,无法阻止方法执行(除非抛异常) @Before("execution(* com.example.demo.controller.*.*(..))") public void doBefore(JoinPoint jp) { // JoinPoint:连接点信息(比ProceedingJoinPoint少一个proceed()) log.info("【前置通知】方法准备执行: {}", jp.getSignature().getName()); // 可以在这里做:参数校验、权限检查、日志记录 } // ============ 后置通知 ============ // 在目标方法**之后**执行,无论方法是否成功或抛异常,**都会执行**(类似finally) @After("execution(* com.example.demo.controller.*.*(..))") public void doAfter(JoinPoint jp) { log.info("【后置通知】方法执行完毕: {}", jp.getSignature().getName()); // 可以在这里做:资源释放、清理工作 } // ============ 返回后通知 ============ // 在目标方法**成功返回后**执行,**方法抛出异常时不执行** @AfterReturning(value = "execution(* com.example.demo.controller.*.*(..))", returning = "result") // returning指定接收返回值的参数名 public void doAfterReturning(JoinPoint jp, Object result) { log.info("【返回后通知】方法成功返回,返回值: {}", result); // 可以在这里做:结果缓存、成功日志 } // ============ 异常后通知 ============ // 在目标方法**抛出异常后**执行,**方法成功返回时不执行** @AfterThrowing(value = "execution(* com.example.demo.controller.*.*(..))", throwing = "e") // throwing指定接收异常的参数名 public void doAfterThrowing(JoinPoint jp, Exception e) { log.info("【异常后通知】方法抛出异常: {}", e.getMessage()); // 可以在这里做:异常记录、发送告警 } // ============ 环绕通知 ============ // **最强大**的通知,可以完全控制目标方法的执行 // 在目标方法**前后**都执行,可以阻止方法执行、修改参数、修改返回值 @Around("execution(* com.example.demo.controller.*.*(..))") public Object doAround(ProceedingJoinPoint pjp) throws Throwable { log.info("【环绕通知-前】方法准备执行: {}", pjp.getSignature().getName()); // 可以在这里修改参数 Object[] args = pjp.getArgs(); // args[0] = "modified"; // 修改第一个参数 Object result = null; try { result = pjp.proceed(args); // 执行目标方法,可以传入修改后的参数 log.info("【环绕通知-后】方法成功执行,准备返回"); // 可以在这里修改返回值 // result = "modified result"; } catch (Throwable e) { log.info("【环绕通知-异常】方法执行出错"); throw e; // 必须抛出异常,否则异常被吞掉 } finally { log.info("【环绕通知-最终】无论如何都会执行"); } return result; // 必须返回结果 } }如何从代码中理解5种通知的区别?
java// 测试Controller @RestController public class TestController { @RequestMapping("/test1") public String test1() { return "success"; // 正常返回 } @RequestMapping("/test2") public String test2() { int i = 1 / 0; // 抛出ArithmeticException return "success"; } } // 访问/test1的输出顺序: // 【前置通知】方法准备执行: test1 // 【环绕通知-前】方法准备执行: test1 // 【环绕通知-后】方法成功执行,准备返回 // 【环绕通知-最终】无论如何都会执行 // 【返回后通知】方法成功返回,返回值: success // 【后置通知】方法执行完毕: test1 // 访问/test2的输出顺序: // 【前置通知】方法准备执行: test2 // 【环绕通知-前】方法准备执行: test2 // 【环绕通知-异常】方法执行出错 // 【环绕通知-最终】无论如何都会执行 // 【异常后通知】方法抛出异常: / by zero // 【后置通知】方法执行完毕: test2 // 注意:【返回后通知】没有执行!因为方法抛异常了 // 结论体现: // 1. @AfterReturning只在成功时执行 → 代码中test2抛异常,它没有打印 // 2. @AfterThrowing只在异常时执行 → 代码中test2抛异常,它打印了 // 3. @After无论成败都执行 → 两个测试都打印了 // 4. @Around最强大 → 代码中可以控制是否调用pjp.proceed()3.4 切面(Aspect) - "切点 + 通知的集合"
概念:切面 = 切点(在哪里增强)+ 通知(增强什么),是一个完整的增强模块。
java// 一个完整的切面类 import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Slf4j @Component @Aspect // 标识这是一个切面类 public class LogAspect { // ============ 定义切点 ============ // 把公共的切点表达式提取出来,避免重复写 // 方法名pt()就是切点的ID,其他通知可以通过"pt()"引用 @Pointcut("execution(* com.example.demo.service.*.*(..))") private void pt() {} // 方法体为空,因为@Pointcut只需要表达式 // ============ 定义通知 ============ // 引用切点pt() @Before("pt()") public void beforeLog() { log.info("记录日志 - 方法开始"); } @After("pt()") public void afterLog() { log.info("记录日志 - 方法结束"); } // ============ 这个类整体就是一个切面 ============ // 它包含了: // 1. 切点定义:pt() // 2. 通知定义:beforeLog()、afterLog() // 3. 类注解:@Aspect }如何从代码中看出"切面 = 切点 + 通知"?
@Pointcut定义了规则(在哪里)
@Before、@After定义了逻辑(做什么)它们共同存在于一个
@Aspect类中,形成了一个完整的增强模块4. 通知执行顺序(代码验证结论)
4.1 同一切面内的执行顺序
java// 结论代码验证 @Aspect @Component public class OrderDemo { @Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("Around-前"); Object result = pjp.proceed(); System.out.println("Around-后"); return result; } @Before("pt()") public void before() { System.out.println("Before"); } @After("pt()") public void after() { System.out.println("After"); } @AfterReturning("pt()") public void afterReturning() { System.out.println("AfterReturning"); } } // 调用目标方法后输出: // Around-前 // Before // 目标方法执行 // Around-后 // After // AfterReturning // 代码如何体现结论? // 1. @Around先开始 → 代码中around()先打印"Around-前" // 2. @Before在目标方法前 → 代码中before()在pjp.proceed()前打印 // 3. @Around后可以拦截返回值 → 代码中around()可以修改result变量 // 4. @After在@AfterReturning前 → 代码中after()打印在afterReturning()前面4.2 多个切面的执行顺序(@Order注解)
java// 切面A @Slf4j @Aspect @Component @Order(1) // 数字越小,优先级越高(最先执行) public class AspectA { @Before("execution(* com.example.demo.controller.*.*(..))") public void beforeA() { log.info("【AspectA-前置】order=1"); } @After("execution(* com.example.demo.controller.*.*(..))") public void afterA() { log.info("【AspectA-后置】order=1"); } } // 切面B @Slf4j @Aspect @Component @Order(2) // 比AspectA晚执行 public class AspectB { @Before("execution(* com.example.demo.controller.*.*(..))") public void beforeB() { log.info("【AspectB-前置】order=2"); } @After("execution(* com.example.demo.controller.*.*(..))") public void afterB() { log.info("【AspectB-后置】order=2"); } } // 调用目标方法后输出: // 【AspectA-前置】order=1 // 【AspectB-前置】order=2 // 目标方法执行 // 【AspectB-后置】order=2 // 【AspectA-后置】order=1 // 代码如何体现结论? // 1. @Order(1)先执行 → 代码中AspectA的beforeA()先打印 // 2. @Order大的后执行 → 代码中AspectB的beforeB()后打印 // 3. 后置通知顺序相反 → 代码中afterB()在afterA()前打印(先进后出,像栈结构) // 4. 为什么?因为@After是在finally中执行的,执行顺序与调用顺序相反5. 切点表达式详解(精确匹配)
5.1 execution表达式(最常用)
java// 语法:execution(修饰符 返回值 包名.类名.方法名(参数) 异常) // 示例1:精确匹配 @Pointcut("execution(public String com.example.demo.controller.BookController.addBook(BookInfo))") private void exactMatch() {} // 只匹配这一个方法 // 示例2:宽泛匹配 @Pointcut("execution(* com.example.demo.controller.*.*(..))") private void broadMatch() {} // 匹配controller包下所有类的所有方法 // 示例3:匹配特定后缀的方法 @Pointcut("execution(* com.example.demo.service.*Service.*(..))") private void serviceMatch() {} // 匹配所有以Service结尾的类的方法 // 示例4:匹配特定前缀的方法 @Pointcut("execution(* com.example.demo.service.*.save*(..))") private void saveMatch() {} // 匹配所有以save开头的方法 // 示例5:匹配无参方法 @Pointcut("execution(* com.example.demo.controller.*.*())") private void noArgsMatch() {} // 注意:(..)变成了() // 示例6:匹配第一个参数是String的方法 @Pointcut("execution(* com.example.demo.service.*.save(String, ..))") private void firstArgMatch() {} // 第一个参数必须是String,后面可以有任意参数 // 代码如何体现匹配粒度? // 1. 精确匹配 → 代码中只写了addBook一个方法,其他方法不受影响 // 2. 宽泛匹配 → 代码中*通配符,多个类被匹配 // 3. 后缀匹配 → 代码中*Service,所有Service类都被匹配 // 4. 前缀匹配 → 代码中save*,所有save方法都被匹配 // 5. 参数匹配 → 代码中String, ..,精确控制参数类型5.2 @annotation表达式(注解匹配)
java// 场景:有些方法需要增强,有些不需要,用注解标记 // 步骤1:自定义注解 import java.lang.annotation.ElementType; // 注解作用目标类型(类、方法、字段等) import java.lang.annotation.Retention; // 注解保留策略(源码、字节码、运行时) import java.lang.annotation.RetentionPolicy; // 保留策略的枚举 import java.lang.annotation.Target; // 指定注解的作用目标 // @Target:这个注解只能用在方法上 @Target(ElementType.METHOD) // @Retention:这个注解在运行时有效(Spring AOP通过反射读取它) @Retention(RetentionPolicy.RUNTIME) public @interface LogExecutionTime { // 可以添加配置属性,比如:String value() default ""; // 这里简化,不写任何属性 } // 代码如何体现注解的作用? // 1. @Target限制使用位置 → 代码中只能用在方法上,不能用在类或字段上 // 2. @Retention(RUNTIME) → 代码中运行时Spring能读取到这个注解 // 3. @interface → 代码中定义了一个注解类型 // 步骤2:切面类使用@annotation import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class AnnotationAspect { // @annotation:匹配所有标注了@LogExecutionTime的方法 // 括号里是注解的全限定类名 @Around("@annotation(com.example.demo.aspect.LogExecutionTime)") public Object logTime(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); long end = System.currentTimeMillis(); System.out.println(pjp.getSignature().getName() + " 耗时: " + (end - start) + "ms"); return result; } } // 步骤3:在需要增强的方法上添加注解 import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ProductController { @RequestMapping("/addProduct") @LogExecutionTime // 只有这个方法会被增强! public String addProduct() { return "商品添加成功"; } @RequestMapping("/delProduct") // 没有@LogExecutionTime注解,不会被增强 public String delProduct() { return "商品删除成功"; } } // 代码如何体现注解的精确控制? // 1. @LogExecutionTime像开关 → 代码中addProduct()有注解,被增强 // 2. delProduct()无注解 → 代码中没有被打印耗时 // 3. 对比execution:注解方式更精确,不需要写复杂的包路径6. Spring AOP实现原理(源码级理解)
6.1 代理模式基础(先理解设计模式)
6.1.1 静态代理(手动写代理类)
java// 1. 定义接口(Subject) public interface HouseService { void rentHouse(); // 出租房子的方法 } // 2. 真实实现类(RealSubject) public class RealHouseService implements HouseService { @Override public void rentHouse() { System.out.println("房东:签合同、收房租"); // 核心业务 } } // 3. 代理类(Proxy)- 手动编写 public class HouseServiceProxy implements HouseService { // 持有真实对象的引用(组合关系) private HouseService realHouseService; // 通过构造器注入真实对象 public HouseServiceProxy(HouseService realHouseService) { this.realHouseService = realHouseService; } @Override public void rentHouse() { // ============ 代理增强逻辑 ============ System.out.println("中介:带看房、谈价格"); // 前置增强 // 调用真实对象的方法(核心业务) realHouseService.rentHouse(); // ============ 代理增强逻辑 ============ System.out.println("中介:收中介费、帮维修"); // 后置增强 } } // 4. 使用代理 public class StaticProxyDemo { public static void main(String[] args) { // 创建真实对象(房东) HouseService realHouseService = new RealHouseService(); // 创建代理对象(中介),把真实对象传进去 HouseService proxy = new HouseServiceProxy(realHouseService); // 通过代理调用(客户找中介租房) proxy.rentHouse(); // 输出: // 中介:带看房、谈价格 // 房东:签合同、收房租 // 中介:收中介费、帮维修 } } // 代码如何体现静态代理的缺点? // 1. 代码重复 → 每个方法都要写一遍代理逻辑(rentHouse()、sellHouse()都要写) // 2. 不灵活 → 如果增加新方法,代理类必须修改 // 3. 硬编码 → 增强逻辑写死在代理类里6.1.2 动态代理(自动生成代理类)
JDK动态代理(目标必须实现接口)
java// 1. 接口和实现类(同上) public interface HouseService { void rentHouse(); } public class RealHouseService implements HouseService { public void rentHouse() { System.out.println("房东出租房子"); } } // 2. 实现InvocationHandler(代理逻辑处理器) import java.lang.reflect.InvocationHandler; // JDK提供的代理处理器接口 import java.lang.reflect.Method; // 反射的Method类,代表一个方法 public class HouseInvocationHandler implements InvocationHandler { // 目标对象(被代理的对象) private Object target; public HouseInvocationHandler(Object target) { this.target = target; } /** * 代理对象调用任何方法时,都会进入这个方法 * @param proxy 代理对象本身(很少用) * @param method 被调用的方法对象(可以获取方法名、参数等) * @param args 方法调用时传入的参数数组 * @return 方法执行结果 * @throws Throwable 可能抛出的异常 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("中介:开始服务"); // 前置增强 // 通过反射调用目标方法 // method.invoke() = 调用target对象的method方法,参数是args Object result = method.invoke(target, args); System.out.println("中介:服务结束"); // 后置增强 return result; } } // 3. 创建代理对象 import java.lang.reflect.Proxy; // JDK的代理工具类 public class JdkProxyDemo { public static void main(String[] args) { // 创建真实对象 HouseService target = new RealHouseService(); // 创建代理对象 // Proxy.newProxyInstance参数: // 1. classLoader:用目标类的类加载器 // 2. interfaces:代理要实现哪些接口(这里实现HouseService接口) // 3. invocationHandler:代理逻辑处理器 HouseService proxy = (HouseService) Proxy.newProxyInstance( target.getClass().getClassLoader(), // 类加载器:加载代理类到JVM new Class[]{HouseService.class}, // 实现的接口数组 new HouseInvocationHandler(target) // 代理逻辑处理器 ); // 调用代理对象的方法 proxy.rentHouse(); // 会自动进入invoke()方法 // 输出: // 中介:开始服务 // 房东出租房子 // 中介:服务结束 } } // 代码如何体现动态代理的优势? // 1. 无需为每个类写代理类 → 代码中Proxy.newProxyInstance()自动生成代理 // 2. 通用性强 → 代码中target可以是任何实现了接口的对象 // 3. 灵活 → 代码中InvocationHandler可以复用给多个目标类CGLIB动态代理(目标不需要实现接口)
java// 1. 真实类(没有实现接口) public class RealHouseService { public void rentHouse() { System.out.println("房东出租房子(无接口)"); } } // 2. 实现MethodInterceptor(方法拦截器) import org.springframework.cglib.proxy.MethodInterceptor; // CGLIB的方法拦截器接口 import org.springframework.cglib.proxy.MethodProxy; // CGLIB的方法代理(比JDK的Method性能高) public class HouseMethodInterceptor implements MethodInterceptor { private Object target; // 目标对象 public HouseMethodInterceptor(Object target) { this.target = target; } /** * 拦截目标方法调用 * @param obj 代理对象本身 * @param method 被调用的方法对象 * @param args 方法参数 * @param proxy CGLIB的方法代理对象(用于调用父类方法) * @return 方法执行结果 * @throws Throwable */ @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("中介:开始服务(CGLIB)"); // 调用父类(目标类)的方法 // proxy.invokeSuper():调用被代理类的父类方法(即目标方法) // 比JDK的method.invoke()性能高,因为直接操作字节码 Object result = proxy.invokeSuper(target, args); System.out.println("中介:服务结束(CGLIB)"); return result; } } // 3. 创建代理对象 import org.springframework.cglib.proxy.Enhancer; // CGLIB的增强器 public class CglibProxyDemo { public static void main(String[] args) { // 创建真实对象 RealHouseService target = new RealHouseService(); // 创建代理对象 // Enhancer.create参数: // 1. 目标类的Class对象 // 2. 方法拦截器 RealHouseService proxy = (RealHouseService) Enhancer.create( target.getClass(), // 目标类 new HouseMethodInterceptor(target) // 拦截器 ); // 调用代理对象 proxy.rentHouse(); // 输出: // 中介:开始服务(CGLIB) // 房东出租房子(无接口) // 中介:服务结束(CGLIB) } } // 代码如何体现CGLIB的特点? // 1. 无需接口 → 代码中RealHouseService直接是类 // 2. 生成子类 → 代码中Enhancer.create()会生成RealHouseService的子类 // 3. 性能更高 → 代码中MethodProxy.invokeSuper()比反射快6.2 Spring AOP如何选择代理方式?(源码逻辑)
java// Spring AOP代理选择规则(通过配置控制) // 情况1:目标类实现了接口(默认使用JDK代理) @Service public class UserServiceImpl implements UserService { // 实现了UserService接口 // Spring默认用JDK代理 } // 情况2:目标类没实现接口(只能用CGLIB) @Service public class ProductService { // 没有实现接口 // Spring强制用CGLIB代理 } // 情况3:强制使用CGLIB(配置) @SpringBootApplication @EnableAspectJAutoProxy(proxyTargetClass = true) // 关键配置! public class MyApplication { // proxyTargetClass = true:强制使用CGLIB代理 // 即使实现了接口,也用CGLIB } // 代码如何体现选择逻辑? // 1. 没有配置 → 代码中Spring自动判断(有接口就用JDK,无接口用CGLIB) // 2. 配置proxyTargetClass=true → 代码中所有类都用CGLIB代理 // 3. 为什么需要强制? → 代码中CGLIB可以代理类的方法,JDK只能代理接口方法Spring AOP代理选择源码简化版
java// 这是Spring APO选择代理的核心逻辑(伪代码) public class ProxyFactory { public AopProxy createAopProxy(Object target) { // 1. 如果配置了强制使用CGLIB if (proxyTargetClass) { return new CglibProxy(target); } // 2. 如果目标实现了接口 if (target.getClass().getInterfaces().length > 0) { return new JdkProxy(target); // 使用JDK动态代理 } // 3. 否则使用CGLIB return new CglibProxy(target); } } // 代码如何体现选择逻辑? // 1. 优先检查配置 → 代码中if(proxyTargetClass)优先判断 // 2. 次选接口 → 代码中else if检查getInterfaces() // 3. 最后保底 → 代码中return new CglibProxy()6.3 代理执行流程(方法调用时发生了什么)
java// 当你调用被代理的方法时,实际执行流程: // 假设有: @RestController public class BookController { @RequestMapping("/add") public String add() { return "ok"; } } // 调用:bookController.add() 时 // ============ JDK代理执行流程 ============ // 1. 你调用的是**代理对象**的add()方法 // 2. 自动进入JdkDynamicAopProxy.invoke()方法 public Object invoke(Object proxy, Method method, Object[] args) { // 3. 获取所有增强器(通知) List<Advisor> advisors = getAdvisors(); // 4. 创建MethodInvocation(方法调用链) MethodInvocation invocation = new ReflectiveMethodInvocation( proxy, target, method, args, targetClass, advisors ); // 5. 执行拦截器链(依次执行所有通知) return invocation.proceed(); // 关键!所有通知在这里执行 } // ============ CGLIB代理执行流程 ============ // 1. 你调用的是**代理子类**的add()方法 // 2. 自动进入CglibAopProxy.intercept()方法 public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) { // 3. 获取所有增强器 List<Advisor> advisors = getAdvisors(); // 4. 创建MethodInvocation MethodInvocation invocation = new CglibMethodInvocation( proxy, target, method, args, targetClass, advisors, methodProxy ); // 5. 执行拦截器链 return invocation.proceed(); } // 代码如何体现执行流程? // 1. 代理对象 → 代码中proxy不是原始对象,而是Proxy.newProxyInstance()创建的 // 2. 自动拦截 → 代码中invoke()/intercept()由JDK/CGLIB自动调用 // 3. 拦截器链 → 代码中invocation.proceed()会遍历所有通知并执行7. 完整项目实战(整合所有知识点)
7.1 项目结构
javacom.example.demo ├── annotation │ └── LogExecutionTime.java // 自定义注解 ├── aspect │ ├── LogAspect.java // 日志切面 │ ├── TimeAspect.java // 耗时切面 │ └── SecurityAspect.java // 安全切面 ├── controller │ ├── UserController.java // 用户接口 │ └── OrderController.java // 订单接口 └── DemoApplication.java // 启动类7.2 完整代码(带全套注释)
自定义注解
java// annotation/LogExecutionTime.java import java.lang.annotation.ElementType; // 注解作用目标 import java.lang.annotation.Retention; // 注解保留策略 import java.lang.annotation.RetentionPolicy; // 运行时保留 import java.lang.annotation.Target; // 目标为METHOD // 这个注解用于标记需要记录日志的方法 @Target(ElementType.METHOD) // 只能用在方法上 @Retention(RetentionPolicy.RUNTIME) // 运行时通过反射读取 public @interface LogExecutionTime { String value() default ""; // 可以添加描述信息 }日志切面
java// aspect/LogAspect.java import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; // 连接点信息 import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; // 方法签名 import org.aspectj.lang.annotation.*; import org.springframework.core.annotation.Order; // 顺序注解 import org.springframework.stereotype.Component; @Slf4j @Component @Aspect @Order(1) // 优先级最高,最先执行 public class LogAspect { // 定义切点:controller包下所有方法 @Pointcut("execution(* com.example.demo.controller.*.*(..))") public void controllerPointcut() {} // 定义切点:带有@LogExecutionTime注解的方法 @Pointcut("@annotation(com.example.demo.annotation.LogExecutionTime)") public void annotationPointcut() {} // 前置通知:记录方法开始 @Before("controllerPointcut()") public void logBefore(JoinPoint jp) { Signature signature = jp.getSignature(); // 获取方法签名 String className = jp.getTarget().getClass().getSimpleName(); // 获取类名 String methodName = signature.getName(); // 获取方法名 log.info("【日志】{}.{} 开始执行", className, methodName); } // 返回后通知:记录方法成功返回 @AfterReturning(value = "controllerPointcut()", returning = "result") public void logAfterReturning(JoinPoint jp, Object result) { log.info("【日志】{} 返回结果: {}", jp.getSignature().getName(), result); } // 异常后通知:记录方法异常 @AfterThrowing(value = "controllerPointcut()", throwing = "e") public void logAfterThrowing(JoinPoint jp, Exception e) { log.error("【日志】{} 发生异常: {}", jp.getSignature().getName(), e.getMessage()); } }耗时切面
java// aspect/TimeAspect.java import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Slf4j @Component @Aspect @Order(2) // 优先级次之,在LogAspect之后执行 public class TimeAspect { // 环绕通知:计算耗时 @Around("execution(* com.example.demo.controller.*.*(..))") public Object calculateTime(ProceedingJoinPoint pjp) throws Throwable { Signature signature = pjp.getSignature(); String methodName = signature.getName(); long startTime = System.currentTimeMillis(); log.info("【计时】{} 开始", methodName); Object result = pjp.proceed(); // 执行目标方法 long endTime = System.currentTimeMillis(); long costTime = endTime - startTime; log.info("【计时】{} 结束,耗时: {}ms", methodName, costTime); return result; } }安全切面
java// aspect/SecurityAspect.java import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Slf4j @Component @Aspect @Order(3) // 优先级最低,最后执行 public class SecurityAspect { // 前置通知:权限检查 @Before("@annotation(com.example.demo.annotation.LogExecutionTime)") public void checkPermission(JoinPoint jp) { Signature signature = jp.getSignature(); String methodName = signature.getName(); // 模拟权限检查 log.info("【安全】检查 {} 的权限", methodName); // 实际应用:从Session获取用户角色,检查是否有权限 // if (!hasPermission()) { throw new SecurityException("无权限"); } } }Controller
java// controller/UserController.java import com.example.demo.annotation.LogExecutionTime; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { @GetMapping("/add") @LogExecutionTime // 添加注解,会被安全切面检查 public String addUser() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return "用户添加成功"; } @GetMapping("/delete") public String deleteUser() { return "用户删除成功"; } } // controller/OrderController.java import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/order") public class OrderController { @GetMapping("/create") public String createOrder() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } return "订单创建成功"; } }启动类
java// DemoApplication.java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.EnableAspectJAutoProxy; @SpringBootApplication @EnableAspectJAutoProxy(proxyTargetClass = true) // 启用CGLIB代理,强制使用CGLIB public class DemoApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args); // 启动后可以用context获取Bean进行测试 } }7.3 测试结果分析(结论与代码对应)
测试1:访问 /user/add
java控制台输出顺序: 1. 【日志】UserController.addUser 开始执行 ← LogAspect前置通知 2. 【计时】addUser 开始 ← TimeAspect环绕通知-前 3. 【安全】检查 addUser 的权限 ← SecurityAspect前置通知(注解匹配) 4. 【计时】addUser 结束,耗时: 105ms ← TimeAspect环绕通知-后 5. 【日志】addUser 返回结果: 用户添加成功 ← LogAspect返回后通知 结论对应代码: 1. @Order(1)优先执行 → LogAspect的@Before先打印 2. @Order(2)次之 → TimeAspect的@Around在@Before之后执行 3. @annotation精确匹配 → SecurityAspect只对@LogExecutionTime生效 4. 环绕通知包裹 → TimeAspect的计时包含了SecurityAspect的权限检查时间 5. 返回后通知 → LogAspect的@AfterReturning拿到返回值测试2:访问 /user/delete
java控制台输出: 1. 【日志】UserController.deleteUser 开始执行 2. 【计时】deleteUser 开始 3. 【计时】deleteUser 结束,耗时: 1ms 4. 【日志】deleteUser 返回结果: 用户删除成功 结论对应代码: - 没有【安全】日志 → deleteUser()没有@LogExecutionTime注解 - SecurityAspect的切点只匹配注解,所以不生效测试3:访问 /order/create
控制台输出: 1. 【日志】OrderController.createOrder 开始执行 2. 【计时】createOrder 开始 3. 【计时】createOrder 结束,耗时: 202ms 4. 【日志】createOrder 返回结果: 订单创建成功 结论对应代码: - execution(* controller.*.*(..))匹配所有controller - LogAspect、TimeAspect对UserController和OrderController都生效 - SecurityAspect只对添加了@LogExecutionTime的方法生效8. 常见问题与解决方案(代码级避坑)
8.1 同一个类内方法调用,AOP失效
java@Service public class UserService { public void methodA() { System.out.println("执行methodA"); methodB(); // 直接调用同类方法,不会触发AOP! } @LogExecutionTime // 期望被增强 public void methodB() { System.out.println("执行methodB"); } } // 问题代码分析: // methodA()调用methodB()时,调用的是this.methodB(),this是原始对象,不是代理对象 // Spring AOP只对代理对象的方法调用生效 // 解决方案1:注入自己(获取代理对象) @Service public class UserService { @Autowired private UserService self; // 注入Spring管理的代理对象 public void methodA() { System.out.println("执行methodA"); self.methodB(); // 通过代理对象调用,AOP生效! } @LogExecutionTime public void methodB() { System.out.println("执行methodB"); } } // 解决方案2:使用AopContext(需要配置expose-proxy=true) @Service public class UserService { public void methodA() { System.out.println("执行methodA"); ((UserService) AopContext.currentProxy()).methodB(); // 获取当前代理对象 } @LogExecutionTime public void methodB() { System.out.println("执行methodB"); } } // 配置类 @EnableAspectJAutoProxy(exposeProxy = true) // 暴露代理对象到AopContext8.2 通知中抛出异常,导致原方法无法执行
java@Around("execution(* com.example.demo.service.*.*(..))") public Object badAdvice(ProceedingJoinPoint pjp) throws Throwable { // 错误示例1:忘记调用pjp.proceed() System.out.println("前置逻辑"); // 没有pjp.proceed(),原方法不会被执行! return null; // 调用方拿到null,可能引发NPE // 错误示例2:吞掉异常 try { return pjp.proceed(); } catch (Exception e) { System.out.println("出错了"); // 没有throw,异常被吞掉,调用方以为成功了! return null; } // 正确做法 try { return pjp.proceed(); } catch (Throwable e) { System.out.println("出错了: " + e.getMessage()); throw e; // 必须抛出,让调用方感知异常 } } // 代码如何体现问题? // 1. 忘记proceed() → 代码中目标方法根本没执行 // 2. 吞掉异常 → 代码中catch后没有throw,异常链断裂 // 3. 正确做法 → 代码中catch后必须throw,保持异常传播8.3 切点表达式过于宽泛,影响性能
java// 错误示例:匹配所有方法 @Around("execution(* *.*(..))") // 危险!会匹配所有类,包括Spring内置的Bean public Object allMethods(ProceedingJoinPoint pjp) throws Throwable { // 这会拦截Spring自己的方法,如Bean初始化、AOP代理创建等 // 导致:启动慢、性能差、不可预知的错误 return pjp.proceed(); } // 正确示例:精确匹配 @Around("execution(* com.example.demo.controller.*.*(..)) && " + "!execution(* com.example.demo.controller.HealthController.*(..))") // 排除健康检查 public Object preciseMethods(ProceedingJoinPoint pjp) throws Throwable { return pjp.proceed(); } // 代码如何体现性能问题? // 1. 宽泛表达式 → 代码中*.*会匹配到Spring内部Bean的方法 // 2. 精确表达式 → 代码中明确指定包路径,并用!排除不需要的类 // 3. 启动时间 → 宽泛表达式会让Spring启动时处理大量不必要的代理9. 总结(知识地图)
9.1 核心概念关系图
html┌─────────────────────────────────────────────────────────────┐ │ 切面 (Aspect) │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 切点 (Pointcut) │ │ │ │ execution(* com.example.controller.*(..)) │ │ │ │ @annotation(com.example.annotation.Log) │ │ │ └─────────────────────────────────────────────────────────┘ │ │ + │ │ | │ │ | 匹配 │ │ v │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 连接点 (Join Point) │ │ │ │ UserController.addUser() │ │ │ │ OrderController.createOrder() │ │ │ └─────────────────────────────────────────────────────────┘ │ │ + │ │ | │ │ | 应用 │ │ v │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 通知 (Advice) │ │ │ │ @Before: 前置逻辑 │ │ │ │ @Around: 环绕逻辑 │ │ │ │ @After: 后置逻辑 │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘9.2 代理方式选择决策树
java目标类是否实现了接口? ├─ 是 → 是否配置proxyTargetClass=true? │ ├─ 是 → 使用CGLIB代理 │ └─ 否 → 使用JDK动态代理 └─ 否 → 使用CGLIB代理(强制)9.3 最佳实践清单
|----------|-----------------------|----------------------------------------------------|
| 场景 | 推荐做法 | 代码示例 |
| 精确匹配 | 使用@annotation或精确包路径 |@Around("@annotation(Log)")|
| 多个切面 | 用@Order控制顺序 |@Order(1)|
| 代理选择 | 默认即可,有特殊需求再配置CGLIB |@EnableAspectJAutoProxy(proxyTargetClass = true)|
| 内部调用 | 注入自己或使用AopContext |self.methodB()|
| 异常处理 | 环绕通知中必须throw异常 |catch(Throwable e) { throw e; }|10. 最后的叮嘱(给新手的建议)
先跑起来 :不要纠结细节,先把第一个
@Around例子跑通再理解概念:对照代码和日志,理解切点、连接点、通知的关系
后玩复杂:熟练后再尝试多个切面、@annotation、@Order等高级玩法
看日志:日志是理解AOP执行顺序的最好工具
调试:在通知方法里打断点,看调用栈,能清晰看到代理链路
记住:AOP就像给代码穿外套,不穿外套的人(原始类)完全不知道外套的存在,但外套确实提供了额外的功能(日志、计时、安全)。这就是无侵入式编程的魅力!
恭喜你完成了Spring AOP的深度学习! 现在你应该能:
写出带详细注释的切面代码
解释每个注解的作用
通过日志验证执行顺序
选择合适的代理方式
避开了常见的坑
如果还有疑问,随时回来翻代码和注释,它们是最诚实的老师!
Spring Boot 3.x AOP 完整实战代码包
这是一个可以直接运行的完整 Spring Boot 代码示例,展示了如何使用 Spring AOP(面向切面编程)实现日志记录 、方法耗时统计 和权限检查(模拟)。
项目特性:
-
基于 Spring Boot 3.2.5 + JDK 17
-
包含 AOP 切面 (@Aspect) 与 Lombok
-
零数据库依赖(纯控制台日志演示)
-
包含完整的
pom.xml和application.yml
快速开始
-
创建一个新的 Maven 项目。
-
将下方的文件复制到对应的目录中。
-
Maven Reload 加载依赖。
-
运行
DemoApplication启动类。 -
访问测试接口查看控制台日志。
一、Maven 坐标 (pom.xml)
位于项目根目录。
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="[http://maven.apache.org/POM/4.0.0](http://maven.apache.org/POM/4.0.0)"
xmlns:xsi="[http://www.w3.org/2001/XMLSchema-instance](http://www.w3.org/2001/XMLSchema-instance)"
xsi:schemaLocation="[http://maven.apache.org/POM/4.0.0](http://maven.apache.org/POM/4.0.0)
[https://maven.apache.org/xsd/maven-4.0.0.xsd](https://maven.apache.org/xsd/maven-4.0.0.xsd)">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo-aop-complete</artifactId>
<version>1.0.0</version>
<name>demo-aop-complete</name>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Web 模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- AOP 切面模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Lombok 工具库 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
二、配置文件 (application.yml)
位于 src/main/resources 目录。
java
server:
port: 8080
spring:
application:
name: demo-aop-complete
# 只打日志,不配数据库
logging:
level:
com.example.demo: info
pattern:
console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
三、Java 源码
所有代码均位于 com.example.demo 包及其子包下。
1. 启动类 (DemoApplication.java)
路径: src/main/java/com/example/demo/DemoApplication.java
java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true) // 强制使用 CGLIB 代理
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
2. 自定义注解 (LogExecutionTime.java)
路径: src/main/java/com/example/demo/annotation/LogExecutionTime.java
java
package com.example.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记需要记录日志/检查权限的方法
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
String value() default "";
}
3. 切面类 (Aspects)
路径: src/main/java/com/example/demo/aspect/
3.1 日志切面 (LogAspect.java)
java
package com.example.demo.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Slf4j
@Order(1) // 切面执行顺序:数字越小越先执行 Before
public class LogAspect {
// 定义切点:扫描 controller 包下所有方法
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
public void controllerPt() {}
@Before("controllerPt()")
public void logBefore(JoinPoint jp) {
String className = jp.getTarget().getClass().getSimpleName();
String methodName = jp.getSignature().getName();
log.info("【LogAspect-前置】{}.{} 开始", className, methodName);
}
@AfterReturning(value = "controllerPt()", returning = "ret")
public void logAfterReturning(JoinPoint jp, Object ret) {
log.info("【LogAspect-返回】{} 返回值: {}", jp.getSignature().getName(), ret);
}
@AfterThrowing(value = "controllerPt()", throwing = "e")
public void logAfterThrowing(JoinPoint jp, Exception e) {
log.error("【LogAspect-异常】{} 异常信息: {}", jp.getSignature().getName(), e.getMessage());
}
}
3.2 耗时统计切面 (TimeAspect.java)
java
package com.example.demo.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Slf4j
@Order(2)
public class TimeAspect {
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object calculateTime(ProceedingJoinPoint pjp) throws Throwable {
Signature signature = pjp.getSignature();
String methodName = signature.getName();
long start = System.currentTimeMillis();
log.info("【TimeAspect-环绕前】{} 开始", methodName);
Object result = pjp.proceed(); // 执行目标方法
long cost = System.currentTimeMillis() - start;
log.info("【TimeAspect-环绕后】{} 结束,耗时: {} ms", methodName, cost);
return result;
}
}
3.3 安全/权限切面 (SecurityAspect.java)
java
package com.example.demo.aspect;
import com.example.demo.annotation.LogExecutionTime;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Slf4j
@Order(3)
public class SecurityAspect {
// 只拦截带有 @LogExecutionTime 注解的方法
@Before("@annotation(logAnnotation)")
public void checkPermission(JoinPoint jp, LogExecutionTime logAnnotation) {
String methodName = jp.getSignature().getName();
log.info("【SecurityAspect-注解前置】方法: {} , 注解value: {}", methodName, logAnnotation.value());
// 实际开发中可在此处获取 Token 或 Session 进行鉴权
}
}
4. 控制层 (Controllers)
路径: src/main/java/com/example/demo/controller/
4.1 用户控制器 (UserController.java)
java
package com.example.demo.controller;
import com.example.demo.annotation.LogExecutionTime;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/add")
@LogExecutionTime("新增用户") // 触发 SecurityAspect
public String addUser() throws InterruptedException {
Thread.sleep(100); // 模拟业务耗时
return "用户添加成功";
}
@GetMapping("/delete")
public String deleteUser() {
return "用户删除成功";
}
}
4.2 订单控制器 (OrderController.java)
java
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
public class OrderController {
@GetMapping("/create")
public String createOrder() throws InterruptedException {
Thread.sleep(200);
return "订单创建成功";
}
}
4.3 测试控制器 (HelloController.java)
java
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello aop";
}
}
四、验证结果
项目启动后,通过浏览器或 Postman 访问以下链接,观察 IDE 控制台输出。
测试用例
-
- 预期 : 触发
LogAspect和TimeAspect,但不 触发SecurityAspect(因为没有加注解)。
- 预期 : 触发
-
http://localhost:8080/user/add
- 预期: 触发所有三个切面(Log, Time, Security)。
-
http://localhost:8080/order/create
- 预期 : 触发
LogAspect和TimeAspect。
- 预期 : 触发
示例日志输出 (/user/add)
java
14:53:12.121 [http-nio-8080-exec-1] INFO c.e.d.aspect.LogAspect - 【LogAspect-前置】UserController.addUser 开始
14:53:12.122 [http-nio-8080-exec-1] INFO c.e.d.aspect.TimeAspect - 【TimeAspect-环绕前】addUser 开始
14:53:12.123 [http-nio-8080-exec-1] INFO c.e.d.aspect.SecurityAspect - 【SecurityAspect-注解前置】方法: addUser , 注解value: 新增用户
14:53:12.225 [http-nio-8080-exec-1] INFO c.e.d.aspect.TimeAspect - 【TimeAspect-环绕后】addUser 结束,耗时: 103 ms
14:53:12.226 [http-nio-8080-exec-1] INFO c.e.d.aspect.LogAspect - 【LogAspect-返回】addUser 返回值: 用户添加成功
Spring AOP 实战回顾:你其实已经掌握了什么?
你已经把项目跑通了,控制台也打出了整齐的切面日志------这一步看起来"只是跑了个 Demo",但其实你已经一次性把 Spring AOP 的完整生命周期 都体验了一遍。
下面给你一份"战利品清单",告诉你 从这段输出里到底能提炼出什么干货 ,以及 下一步可以怎么玩。
一、从日志里能直接验证的 6 个核心结论
|-----------------------------|-------------------------|-------------------------------------------------|
| 日志片段 | 对应 AOP 概念 | 你得到的验证 |
| 【LogAspect-前置】... 开始执行 | @Before 通知 | 目标方法执行前先 运行,且比 @Around 晚一步(Order 1 vs 2)。 |
| 【TimeAspect-环绕前】... 开始 | @Around 前半 | 环绕通知包裹整个调用链,时间统计包含后续所有切面。 |
| 【SecurityAspect-注解前置】... | @Before + @annotation | 只有标了 @LogExecutionTime 的方法才触发,精确匹配。 |
| 【TimeAspect-环绕后】... 耗时 | @Around 后半 | 拿到原返回值,并可无损追加耗时信息。 |
| 【LogAspect-返回】... 返回值 | @AfterReturning | 目标正常返回时才触发,异常时不打印。 |
| deleteUser 没有 Security 日志 | 切点排除 | 无注解的方法不会 被 @annotation 切面拦截。 |
二、你已经到手的"技能点"
-
零配置开启 AOP
只加 spring-boot-starter-aop + @EnableAspectJAutoProxy 即可,回忆一下你没写任何 XML。
-
会写 5 种通知模板
把 LogAspect、TimeAspect 复制改个类名,就是生产级的日志、监控、权限骨架。
-
知道 Order 数字越小越先执行
下次有"事务 > 日志 > 监控"三级需求时,直接 @Order(1/2/3) 即可,不用再踩顺序坑。
-
会用 @annotation 做"方法级开关"
想给某个接口加限流/缓存/灰度,自定义一个注解 + 一个切面,业务代码零侵入。
-
亲眼见到 CGLIB 代理生效
启动日志里 proxyTargetClass = true 已打开,你调试时可以在 UserController 里断点看到对象是 EnhancerBySpringCGLIB 字样,不再是原始类。
三、下一步可以玩什么(递进路线)
|--------|----------------------------------------------------------------------------|---------------------------|
| 难度 | 玩法 | 目的 |
| ⭐ | 把 TimeAspect 改造成 方法级性能告警 (例如:耗时 > 500 ms 发钉钉消息) | 生产监控 |
| ⭐⭐ | 自定义 @Cache(key="#id") 注解 结合 ConcurrentHashMap 做内存缓存 | 体验"注解即功能" |
| ⭐⭐⭐ | 多切面环境下,用 JoinPoint 和 ProceedingJoinPoint 传递上下文(如 traceId) | 链路追踪 |
| ⭐⭐⭐⭐ | 引入 spring-boot-starter-validation 在 @Before 里统一做参数校验 | 替换 Controller 里的 @Valid |
| ⭐⭐⭐⭐⭐ | 把 SecurityAspect 换成 SpEL 表达式 (如 @PreAuthorize("hasRole('ADMIN')")) | 与 Spring Security 打通 |
四、一句话总结
你现在拥有了一套可复制的"切面模板":
-
日志 → 复制
LogAspect -
计时 → 复制
TimeAspect -
权限 → 复制
SecurityAspect
改包路径、改切点、改 Order 顺序 → 就能直接搬到生产环境。
恭喜你,AOP 已经从"概念"变成你手里的"工具"了!