- 示例工程
- 文档
【免费下载链接】spring-reading
涵盖了 Spring 框架的核心概念和关键功能,包括控制反转(IOC)容器的使用,面向切面编程(AOP)的原理与实践,事务管理的方式与实现,Spring MVC 的流程与控制器工作机制,以及 Spring 中数据访问、安全、Boot 自动配置等方面的深入研究。此外,它还包含了 Spring 事件机制的应用、高级主题如缓存抽象和响应式编程,以及对 Spring 源码的编程风格与设计模式的深入探讨。
导读
AopContext是 Spring AOP 框架提供的线程级上下文工具类,它解决了 Spring AOP 中最经典的问题之一——代理对象内部自调用(self-invocation)导致切面失效。本篇文章以 spring-reading 仓库中spring-aop-aopContext模块为主线,从源码级剖析AopContext的ThreadLocal存储机制、currentProxy()的调用前提(exposeProxy),并完整演示通过ProxyFactory暴露代理对象、在方法内部重入代理方法的实战方案。读完本文,你将掌握 AopContext 的完整使用姿势、底层实现原理及其性能代价,并学会在需要时正确启用它。
一、AopContext 是什么
AopContext是 Spring AOP 框架提供的一个静态工具类,用于在方法内部访问"当前 AOP 代理对象"。它通过currentProxy()方法,让正在被 AOP 调用的方法内部能够拿到自己被增强后的代理对象,进而调用代理对象上的其他方法或获取相关信息。
从仓库源码 MyService.java 可以看到它的典型使用场景:
@MyAnnotation public class MyService { public void foo() { System.out.println("foo..."); // 直接调用bar会导致切入无效 // this.bar(); // 获取代理对象并调用bar ((MyService) AopContext.currentProxy()).bar(); } public void bar() { System.out.println("bar..."); } }当foo()内部直接调用this.bar()时,调用发生在目标对象内部、绕过了代理对象,因此 AOP 增强逻辑不会触发;而改用AopContext.currentProxy()拿到代理对象后再调用bar(),则能让切面重新生效。
二、核心功能
AopContext对外提供三大核心能力:
- 获取当前 AOP 代理对象:通过
currentProxy()方法,在方法内部获取当前被 AOP 增强后的代理对象; - 在方法内部调用代理对象的方法:获得代理对象后,可以在方法内部直接调用代理对象的其他方法,包括被增强的切面方法或目标对象的方法,从而保证切面逻辑不因自调用而丢失;
- 解决代理对象的传递问题:在一些特定场景下,需要在不同方法间传递 AOP 代理对象而不是直接使用
this,AopContext提供了在方法调用间传递 AOP 代理对象的解决方案。
三、类源码详解
AopContext类为静态方法集合,用于获取当前 AOP 调用信息。其完整源码如下(与仓库 README 中呈现的一致):
public final class AopContext { /** * 线程本地变量,用于保存与该线程关联的AOP代理对象。 * 除非控制代理配置的"exposeProxy"属性被设置为"true",否则将包含{@code null}。 * @see ProxyConfig#setExposeProxy */ private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy"); private AopContext() { } /** * 尝试返回当前AOP代理对象。此方法仅在调用方法通过AOP调用,并且AOP框架已设置为暴露代理对象时可用。 * 否则,此方法将抛出IllegalStateException异常。 */ public static Object currentProxy() throws IllegalStateException { Object proxy = currentProxy.get(); if (proxy == null) { throw new IllegalStateException( "Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and " + "ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context."); } return proxy; } /** * 使给定的代理对象可通过{@code currentProxy()}方法访问。 */ @Nullable static Object setCurrentProxy(@Nullable Object proxy) { Object old = currentProxy.get(); if (proxy != null) { currentProxy.set(proxy); } else { currentProxy.remove(); } return old; } }3.1 关键设计点一:ThreadLocal 存储
currentProxy是一个NamedThreadLocal<Object>,代理对象只与当前线程绑定。这意味着:
- 每个线程有自己独立的代理对象上下文,互不干扰;
- 只有在与 AOP 调用上下文相同的线程中调用
currentProxy()才能拿到代理对象,跨线程调用会得到null并抛出IllegalStateException。
3.2 关键设计点二:构造函数私有化
AopContext构造方法为private,类本身为final,它是一个纯粹的工具类,不允许实例化,所有能力都通过静态方法对外提供。
3.3 关键设计点三:currentProxy() 的失败语义
currentProxy()在以下两种情况下会抛出IllegalStateException:
- 该方法在AOP 调用上下文之外被调用(例如直接在普通对象上调用);
- AOP 框架尚未配置为暴露代理对象(
exposeProxy未开启)。
异常信息本身就是一个非常实用的排错提示:需要将Advised上的exposeProxy属性设置为true,并确保在 AOP 调用上下文的同一线程内调用currentProxy()。
3.4 关键设计点四:setCurrentProxy 的返回值约定
setCurrentProxy(proxy)是包级私有(static)方法,由代理机制内部调用。它会返回旧的代理对象(未绑定时返回null),以便调用方在方法执行完毕后恢复现场。传入null时则执行currentProxy.remove(),清除当前线程的绑定。这一"存旧值、用后恢复"的模式与 JDK 动态代理和 CGLIB 拦截器中的finally恢复逻辑严格对应。
四、默认不暴露代理:性能考量
Spring AOP 框架默认不会暴露代理对象。原因在类源码注释中写得很明确:
如果 AOP 框架配置为暴露当前代理对象(非默认情况),则可使用
currentProxy()方法获取正在使用的 AOP 代理对象。目标对象或通知可以使用此方法进行增强调用,类似于 EJB 中的getEJBObject()。也可用于查找通知配置。Spring 的 AOP 框架默认不暴露代理对象,因为这样做会带来性能开销。
同时官方注释也给出了使用建议:当存在合理替代方案时,不应使用此方法,因为这会使应用程序代码依赖于 AOP 下的使用方式和 Spring AOP 框架,增加代码与框架的耦合。
从源码结构看,每一次暴露都需要进行ThreadLocal的读写操作,并且要求两个代理工厂(JdkDynamicAopProxy与CglibAopProxy)在拦截器入口处做额外的条件判断与现场恢复,这正是"默认关闭"的根本原因——为绝大多数不需要该能力的场景省去这部分开销。
五、最佳实践:完整 Demo 与两种运行结果对比
本节完整对应仓库中的可运行示例 AopContextDemo.java 及其配套类。该模块属于 Maven 聚合工程 spring-aop 的一个子模块(artifactId为spring-aop-aopContext),可直接在 IDE 中运行main方法验证。
5.1 主程序:暴露代理并触发切面
public class AopContextDemo { public static void main(String[] args) { // 创建代理工厂&创建目标对象 ProxyFactory proxyFactory = new ProxyFactory(new MyService()); // 暴露代理对象 proxyFactory.setExposeProxy(true); // 创建通知器 proxyFactory.addAdvisor(new DefaultPointcutAdvisor(new AnnotationMatchingPointcut(MyAnnotation.class), new MyMethodBeforeAdvice())); // 获取代理对象 MyService proxy = (MyService) proxyFactory.getProxy(); // 调用代理对象的方法 proxy.foo(); } }这里的三个关键步骤缺一不可:
| 步骤 | 代码 | 作用 |
|---|---|---|
| 创建代理工厂 | new ProxyFactory(new MyService()) | 将MyService作为目标对象交给代理工厂 |
| 暴露代理对象 | proxyFactory.setExposeProxy(true) | 开启exposeProxy,AopContext.currentProxy()才能拿到代理对象 |
| 添加通知器 | addAdvisor(new DefaultPointcutAdvisor(new AnnotationMatchingPointcut(MyAnnotation.class), new MyMethodBeforeAdvice())) | 让标记了@MyAnnotation的类上的方法被前置通知增强 |
| 获取并调用 | proxy.foo() | 通过代理对象发起调用,切面生效 |
DefaultPointcutAdvisor将切入点(AnnotationMatchingPointcut,匹配标注@MyAnnotation的类型)与通知(MyMethodBeforeAdvice)绑定为一个完整的 Advisor。
5.2 前置通知:MyMethodBeforeAdvice
public class MyMethodBeforeAdvice implements MethodBeforeAdvice { @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("Before method " + method.getName() + " is called."); } }对应源码文件为 MyMethodBeforeAdvice.java,实现MethodBeforeAdvice接口,在目标方法执行前打印日志。
5.3 自定义注解:MyAnnotation
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MyAnnotation { }对应源码文件为 MyAnnotation.java。注意两点:@Target(ElementType.TYPE)表示它标注在类上,@Retention(RUNTIME)保证运行时可通过反射读取,从而被AnnotationMatchingPointcut识别。
5.4 目标类:MyService
@MyAnnotation public class MyService { public void foo() { System.out.println("foo..."); // 直接调用bar会导致切入无效 // this.bar(); // 获取代理对象并调用bar ((MyService) AopContext.currentProxy()).bar(); } public void bar() { System.out.println("bar..."); } }5.5 运行结果对比:自调用 vs AopContext 重入
运行结果 1(若改为this.bar()直调):foo()被调用时先执行前置通知打印日志,然后调用this.bar()。由于直接调用this.bar()绕过了 AOP 代理对象,因此不会触发 AOP 切面逻辑:
Before method foo is called. foo... bar...运行结果 2(使用AopContext.currentProxy()):foo()被调用时先执行前置通知打印日志,然后调用((MyService) AopContext.currentProxy()).bar()。由于拿到的是当前 AOP 代理对象并调用了代理对象的bar()方法,因此会再次触发 AOP 切面逻辑:
Before method foo is called. foo... Before method bar is called. bar...注意运行结果 2 中bar()之前多了一行Before method bar is called.——这正是 AopContext 解决自调用失效问题的直接证据。
六、源码分析:代理上下文是如何写入与恢复的
在 Spring AOP 框架中,无论是 JDK 动态代理还是 CGLIB 动态代理的拦截器,都会在入口处对AopContext.setCurrentProxy(proxy)进行赋值,把当前代理对象写入当前线程上下文,供方法内部通过AopContext.currentProxy()获取;方法执行完毕后,再将旧值恢复,避免上下文泄漏到后续调用或其他线程。
6.1 JDK 动态代理:JdkDynamicAopProxy#invoke
在org.springframework.aop.framework.JdkDynamicAopProxy#invoke方法中,方法执行前会判断this.advised.exposeProxy,若为true则先保存旧代理对象,再写入当前代理;方法执行结束后在finally块中恢复旧值:
@Override @Nullable public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; // ... [代码部分省略以简化] try { // ... [代码部分省略以简化] if (this.advised.exposeProxy) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } // ... [代码部分省略以简化] } finally { // ... [代码部分省略以简化] if (setProxyContext) { // Restore old proxy. AopContext.setCurrentProxy(oldProxy); } } }this.advised.exposeProxy正是ProxyFactory.setExposeProxy(true)最终写入的配置值(AdvisedSupport实现了ProxyConfig,其中exposeProxy默认false,可通过setExposeProxy开启)。
6.2 CGLIB 动态代理:DynamicAdvisedInterceptor#intercept
在org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor#intercept方法中,处理逻辑与 JDK 动态代理完全一致——方法拦截期间若this.advised.exposeProxy为true,则写入代理对象上下文,finally中恢复:
@Override @Nullable public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; // ... [代码部分省略以简化] try { if (this.advised.exposeProxy) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } // ... [代码部分省略以简化] } finally { // ... [代码部分省略以简化] if (setProxyContext) { // Restore old proxy. AopContext.setCurrentProxy(oldProxy); } } }从这两段拦截器源码可以归纳出完整的读写时序:
- 入口:检查
exposeProxy→ 为true则setCurrentProxy(proxy)写入 ThreadLocal,同时用局部变量暂存旧值; - 执行:业务方法内部任意位置调用
AopContext.currentProxy()读取当前代理对象; - 出口:
finally块中setCurrentProxy(oldProxy)恢复旧值(旧值为null时执行remove()),保证上下文干净、不串线程。
七、使用注意与进阶关联
7.1 使用前提总结
| 前提条件 | 说明 |
|---|---|
开启exposeProxy | 必须通过ProxyFactory.setExposeProxy(true)(编程式)或@EnableAspectJAutoProxy(exposeProxy = true)(注解式)开启,否则currentProxy()抛IllegalStateException |
| 同线程调用 | currentProxy()必须在 AOP 调用上下文的同一线程内调用,跨线程拿不到代理对象 |
| 调用必须经过代理 | 如果方法本身就不是通过代理对象调用的,currentProxy()同样无法工作 |
7.2 性能与耦合成本
- 性能:暴露代理对象需要额外的
ThreadLocal读写与finally恢复操作,因此 Spring 默认关闭该开关; - 耦合:在业务代码中直接使用
AopContext.currentProxy()会让业务逻辑依赖 Spring AOP 框架的工作方式,官方注释明确建议"存在合理替代方案时不要使用"。
7.3 进阶关联:ExposeInvocationInterceptor
仓库中还提供了与 AopContext 思想一脉相承的进阶示例模块 spring-aop-exposeInvocationInterceptor,通过ExposeInvocationInterceptor暴露当前MethodInvocation,配合LogUtil等工具实现在 AOP 调用链中读取当前拦截状态。两者的共同点是:都依赖"在代理/拦截入口处向线程上下文写入状态,供调用链内部读取"的设计思路,区别在于 AopContext 暴露的是代理对象,而 ExposeInvocationInterceptor 暴露的是当前方法调用(MethodInvocation)。阅读该模块的 ExposeInvocationInterceptorDemo.java 可加深对这一线程上下文传递模式的理解。
7.4 何时真正需要 AopContext
从仓库实践与官方注释来看,AopContext适合以下场景:
- 方法内部必须重入代理对象(即经典的自调用切面失效问题),且无法通过拆分对象、注入自身引用等更干净的方式解决;
- 目标对象或通知需要在调用链中主动查找通知配置或获取调用中的资源。
八、结语
AopContext用几十行代码、一个ThreadLocal加两个静态方法,就为 Spring AOP 的自调用失效问题提供了标准解法。理解它的关键在于记住三件事:默认不暴露(性能考量)、exposeProxy=true才可用、JDK 与 CGLIB 两套代理都在入口写入、finally恢复。结合本仓库 spring-aop-aopContext 模块的源码与 Demo 亲手运行一遍两种调用方式,对比输出差异,你就能彻底掌握 AopContext 的机制与边界。
- 示例工程
- 文档
【免费下载链接】spring-reading
涵盖了 Spring 框架的核心概念和关键功能,包括控制反转(IOC)容器的使用,面向切面编程(AOP)的原理与实践,事务管理的方式与实现,Spring MVC 的流程与控制器工作机制,以及 Spring 中数据访问、安全、Boot 自动配置等方面的深入研究。此外,它还包含了 Spring 事件机制的应用、高级主题如缓存抽象和响应式编程,以及对 Spring 源码的编程风格与设计模式的深入探讨。
相关推荐
MMPose 2D 动物姿态估计推理实战:topdown_demo_with_mmdet 与 Inferencer 完整指南
MMPose 2D 动物姿态估计推理实战:topdown_demo_with_mmdet 与 Inferencer 完整指南 MMPose(OpenMMLab
示例工程文档DevOps-Bash-tools与Logstash:日志处理管道自动化
DevOps Bash tools与Logstash:日志处理管道自动化 日志处理的现代挑战与自动化机遇 在分布式系统架构下,日志数据呈现 爆发式增长 与 碎片
示例工程文档GitHub_Trending/sp/spring-reading源码分析:Spring AOP切面优先级
GitHub_Trending/sp/spring reading源码分析:Spring AOP切面优先级 1. 切面优先级的核心价值与问题场景 在复杂业务系统
示例工程文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考