springAOP
springAOP
John Doe通知执行顺序
Created: 9/2/2025 14:34
Updated: 9/2/2025 23:15
Exported: 9/2/2025 23:15
Link: https://chatgpt.com/c/68b68fe0-9498-8324-90b3-982caf2cb49a
Prompt:spring aop是什么
Response:
Spring AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 框架提供的一种编程思想和实现机制,它主要用来处理程序中的横切关注点(cross-cutting concerns),比如日志记录、安全控制、事务管理、性能监控等。
在传统的面向对象编程(OOP)里,这些横切逻辑通常会分散在多个类的多处方法中,导致代码冗余、耦合度高、不易维护。AOP 的目标就是 把这些横切逻辑抽离出来,以切面的形式统一管理。
核心概念
Spring AOP 里有几个重要的概念:
- 切面(Aspect)
横切逻辑的模块化单元,通常是一个类(比如日志切面、事务切面)。 - 连接点(Join Point)
程序运行中的某个点,比如方法调用、异常抛出等。在 Spring AOP 中,连接点一般就是方法的执行。 - 切入点(Pointcut)
用来选择一组连接点的表达式(比如指定某个包下的所有方法)。 - 通知(Advice)
在连接点上要执行的动作。Spring AOP 支持多种通知类型:- Before:方法执行前
- After Returning:方法成功执行后
- After Throwing:方法抛异常后
- **After (Finally)**:方法执行后(无论是否异常)
- Around:环绕通知(在方法前后都可以执行逻辑)
- 目标对象(Target Object)
被代理的业务类对象。 - 代理对象(Proxy)
AOP 运行时生成的对象,封装了目标对象和切面逻辑。- JDK 动态代理(基于接口)
- CGLIB 动态代理(基于子类)
工作原理
Spring AOP 是基于 代理模式 实现的:
- 当调用目标对象的方法时,Spring 实际上调用的是代理对象;
- 代理对象会在合适的连接点插入切面逻辑(Advice),然后再执行目标方法;
- 从而达到在不修改业务代码的情况下实现横切功能。
示例
假设要给所有 service 包下的方法加日志:
1 |
|
当你调用 UserService.saveUser() 时,Spring 会先执行切面里的日志逻辑,再执行实际的 saveUser 方法。
👉 总结一句话:
Spring AOP 是 Spring 提供的面向切面编程实现,用于把横切关注点抽离出来,通过代理机制在运行时织入业务代码,从而实现解耦和代码复用。
Prompt:同一类型的通知的执行顺序
Response:
好问题 👍
在 Spring AOP 里,如果你在同一个连接点上定义了 多个相同类型的通知(比如多个 @Before),它们的执行顺序是可以控制的,否则会存在默认顺序。
1. 默认规则
- 同一个切面类中,相同类型的通知执行顺序由 方法名的字典序(ASCII 排序) 决定。
例如:输出顺序是:1
2
3
4
public void aBefore() { System.out.println("aBefore"); }
public void bBefore() { System.out.println("bBefore"); }aBefore → bBefore。 - 不同切面类中,相同类型的通知执行顺序是 不确定的,除非你显式指定顺序。
2. 指定执行顺序
Spring 提供了两种方式来控制多个切面或通知的执行顺序:
方式一:@Order
你可以在 切面类 上加 @Order 注解(值越小优先级越高)。
1 |
|
输出顺序:FirstAspect Before → SecondAspect Before。
方式二:实现 Ordered 接口
1 |
|
3. 特殊情况:@Around
@Around通知的优先级最高,因为它可以包裹整个方法调用。- 在一个切面类中,
@Around会先于@Before和@After执行。 - 当多个切面都定义了
@Around,它们的嵌套顺序也受@Order/Ordered控制。
✅ 总结
- 同类通知(同一个切面类):按方法名字典序执行
- 不同切面类:用
@Order或Ordered控制,值越小优先级越高 @Around通知优先级最高,可以控制方法整体执行流程
Prompt:切入点表达式的写法
Response:
切入点表达式(Pointcut Expression)是 Spring AOP 用来匹配目标方法的规则,写法灵活但有套路。常用的就是基于 AspectJ 表达式。
1. 常见的切入点表达式类型
(1)execution —— 最常用的方式
匹配方法执行。语法:
1 | execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?) |
modifiers-pattern:方法修饰符(可选),如public。return-type-pattern:返回值类型,*代表任意类型。declaring-type-pattern:方法所属类(可选)。method-name-pattern:方法名,*代表任意方法。param-pattern:方法参数。throws-pattern:抛出的异常类型(可选)。
示例:
1 | execution(* com.example.service.UserService.*(..)) |
(2)within —— 限定包或类
1 | within(com.example.service.*) |
(3)this / target
this:匹配 代理对象 实现的接口类型。target:匹配 目标对象(真实对象)的类型。
1 | this(com.example.service.UserService) |
(4)args —— 按参数类型匹配
1 | args(String) |
(5)@annotation —— 按方法注解匹配
1 |
|
(6)@within / @target / @args
@within:类上有指定注解。@target:目标对象的类有指定注解。@args:方法参数的类有指定注解。
1 |
|
2. 组合表达式
切入点可以用逻辑运算符组合:
&&:与||:或!:非
1 | execution(* com.example.service..*.*(..)) |
3. 实用示例
1 | // 匹配 service 包下所有类的所有方法 |
✅ 总结口诀
- 常用:
execution(方法) - 按类/包:
within - 按对象类型:
this/target - 按参数:
args - 按注解:
@annotation/@within
Prompt:什么是threadlocal
Response:
很好,这个问题经常出现在面试里 👍
1. 什么是 ThreadLocal
ThreadLocal 是 Java 提供的一种线程本地存储工具类,可以让 每个线程都拥有自己独立的变量副本,从而实现 线程隔离。
换句话说:
- 普通变量是 多个线程共享 的;
ThreadLocal变量是 每个线程独享 的(线程自己存取自己的那份数据)。
2. 工作原理
- 每个线程(
Thread)内部都有一个ThreadLocalMap,用来存储本线程的变量副本; - 当调用
threadLocal.set(value)时,实际上是把(ThreadLocal实例 -> value)这一对数据放到 当前线程的ThreadLocalMap里; - 当调用
threadLocal.get()时,就会从当前线程的ThreadLocalMap里取出对应的值; - 因此,不同线程互不干扰。
3. 简单示例
1 | public class ThreadLocalDemo { |
输出示例:
1 | Thread-A => 42 |
(每个线程看到的值不同,且不会互相影响)
4. 应用场景
- 数据库连接、事务管理:保证一个线程在一次请求中使用同一个连接。
- Session 管理:为每个线程保存用户信息。
- Spring 事务管理:底层就大量用到
ThreadLocal保存事务状态。
5. 注意事项
- 内存泄漏风险:
ThreadLocalMap的 key 是弱引用,但 value 是强引用,如果线程长期不结束,value 可能无法回收。- 解决办法:用完一定要调用
remove()。
- 不要把大对象放进 ThreadLocal,否则容易导致内存占用过高。
✅ 一句话总结ThreadLocal 提供了一种 以线程为作用域存储数据 的机制,每个线程都维护自己的副本,常用于解决多线程环境下的 数据隔离问题,是实现无锁线程安全的一种手段。
要不要我帮你画一张 ThreadLocal 工作原理示意图(线程、ThreadLocalMap、变量副本的关系)?
Prompt:我用拦截器怎么remove:
1 |
|
Response:
你这个场景里用到了 ThreadLocal(应该是在 CurrentHolder 里用的),**关键点就是在请求结束时要记得调用 remove()**,否则线程池里的线程不会释放这个变量,会导致内存泄漏或用户信息串号。
在 Spring 的拦截器里,你可以在 afterCompletion 方法里清理:
✅ 修改后的拦截器
1 |
|
✅ CurrentHolder 写法(示例)
1 | public class CurrentHolder { |
这样,每次请求处理完毕后,无论成功失败,afterCompletion 都会被调用,从而清理掉 ThreadLocal 中的数据,避免 内存泄漏 和 数据串号。
Prompt:总结一下我学了什么
Response:
👌 来个简洁总结,你这几轮问答里主要学了这些:
- Spring AOP 基础
- 解决横切关注点(日志、事务、权限)。
- 核心概念:切面、连接点、切入点、通知、目标对象、代理对象。
- 基于 JDK 动态代理 / CGLIB 实现。
- AOP 通知执行顺序
- 同一类里:同类型通知按方法名字典序执行。
- 不同切面:用
@Order或Ordered控制,值越小优先级越高。 @Around优先级最高,可控制整个方法调用流程。
- 切入点表达式写法
- 常用:
execution(方法)、within(类/包)、args(参数)、@annotation(注解)。 - 可以用
&&、||、!组合。 - 熟悉了几个常用场景的写法。
- 常用:
- ThreadLocal
- 每个线程有独立副本,解决线程隔离问题。
- 常见应用:事务管理、保存用户信息、数据库连接。
- 注意:要及时
remove(),避免内存泄漏。
- 在拦截器中使用 ThreadLocal
- 在
preHandle里存入用户信息(比如用户ID)。 - 在
afterCompletion里调用remove()清理,避免线程池复用导致的数据串号。
- 在
👉 一句话总结:
你学会了 Spring AOP 的原理和使用(切面/切入点/通知顺序),掌握了 ThreadLocal 的作用和风险点,并且能在 拦截器里结合 ThreadLocal 正确存取和清理用户信息。


