在Spring里,AOP是一个绕不开的组件。技术面试常问,生产环境又天天在用:日志、权限、事务、审计,到处都有它的影子。我最早认真读Spring AOP源码,是在排查一个“事务不生效”的问题时被逼着翻源码的——Spring的事务注解本身就是建立在AOP之上的,当自定义切面和事务切面混在一起时,光看配置根本看不出问题出在哪。这篇就以源码为主线,把Spring AOP从入口、代理生成,到方法拦截的完整链路拆开讲,重点说清楚那些“光看文档不会明白”的设计细节。
1. 先说清楚AOP在Spring里到底干了什么
1.1 日志横切逻辑与动态代理的直觉
假设要给订单模块的每个方法打日志,最朴素的做法是在每个方法里手写一行日志,但这样侵入性强,日志逻辑和业务逻辑耦合在一起,改格式就得全量改。AOP的思路不绕弯:业务代码不动,在外面包一层代理对象,代理在调用真正的方法前后插入日志逻辑。
Spring AOP不是第一个发明动态代理的,但它把“代理这件事”流程化了。平时我们说的三个核心概念——切点(Pointcut)决定哪些方法需要处理,通知(Advice)决定插入什么逻辑,切面(Aspect)把两者绑定在一起——这都不是Spring发明的,来自AspectJ的语法规范。而“织入”在Spring AOP里,就是通过动态代理完成的。
有一点要弄明白:Spring AOP有明确的边界,它只支持方法级别的切入点,不支持字段拦截,也不支持构造器拦截。这点和AspectJ原生的编译期织入、加载期织入完全不同。Spring的织入发生在Bean生命周期里,用一个BeanPostProcessor在Bean初始化之后检查它是否需要代理,需要就生成代理对象顶替原来的Bean。这个设计让AOP成了Spring容器的一等公民,事务、缓存、异步、日志全部建在这套机制上。
1.2 Spring AOP与AspectJ的分工边界
很多人会在这一点上混淆。仅仅因为代码里用了@Aspect注解,就以为项目在跑AspectJ编译器,其实不是的。Spring引入aspectjweaver.jar主要为了两样东西:@Aspect注解本身,以及AspectJExpressionPointcut来做切点表达式解析。真正决定“要不要代理、怎么生成代理、怎么拦截调用”的都是spring-aop模块自己的代码。
这带来的直接后果是:Spring AOP只能拦截由Spring容器管理的Bean,而且只能拦截通过代理对象发起的调用。如果你在一个类内部用this.method()调用自身方法,代理对象根本不会参与——这是后面要重点讲的自身调用失效问题,也是日常开发中最常见的“切面不生效”原因。
给一个最小可复现的例子,先建立直觉:
@Aspect @Component public class LogAspect { @Around("execution(* com.example.service.OrderService.*(..))") public Object logAround(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); System.out.println(pjp.getSignature().getName() + " cost " + (System.currentTimeMillis() - start)); return result; } }上面这段代码能跑起来,依赖的是Spring容器里的AnnotationAwareAspectJAutoProxyCreator在起作用。我们第二章就开始看这个入口。
1.3 事务、缓存和异步本质上都在走同一条路
理解Spring AOP的关键一步,是理解@Transactional、@Cacheable、@Async这些注解并不是各写各的代理逻辑,它们最后都会变成某种Advice,挂到同一个拦截器链上。
@Transactional被解析成TransactionInterceptor,是一个MethodInterceptor。@Cacheable被解析成CacheInterceptor,也是MethodInterceptor。@Async被解析成AsyncExecutionInterceptor。
也就是说,当你看到Spring容器里某个Bean的方法被代理时,那个代理对象背后可能挂着好几个切面的拦截器,每个切面提供的Advice会作为拦截器链上的一个节点。这种分层设计让Spring的扩展性很好:你自己写一个MethodInterceptor,也可以通过AOP挂到任意Bean上。
明白了这层关系,才能在排查问题的时候有方向:切面不生效,先从“代理有没有生成”入手;代理生成了但顺序不对,再看“拦截器链的顺序怎么排的”。
2. 自动代理的入口:AnnotationAwareAspectJAutoProxyCreator
2.1 @EnableAspectJAutoProxy到底注册了什么
Spring Boot项目中AOP默认是开启的,不用额外加@EnableAspectJAutoProxy。但老一点的Spring项目或者手动配置的项目里,这个注解是总开关。看一下它的定义:
@Import(AspectJAutoProxyRegistrar.class) public @interface EnableAspectJAutoProxy { boolean proxyTargetClass() default false; boolean exposeProxy() default false; }关键就两件事:@Import了一个Registrar,这个Registrar会在容器里注册一个BeanPostProcessor,类名是AnnotationAwareAspectJAutoProxyCreator;另外两个属性proxyTargetClass和exposeProxy会被读出来设置到这个BeanPostProcessor上。
AnnotationAwareAspectJAutoProxyCreator这个类名虽然长,但每个词都有意义:
| 名称片段 | 含义 |
|---|---|
| AnnotationAware | 能自动扫描容器里所有带@Aspect的Bean,解析它们成为切面 |
| AspectJ | 切点表达式语法遵循AspectJ规范 |
| AutoProxy | 自动代理,不需要手动为每个Bean配置ProxyFactory |
| Creator | 由它创建代理对象 |
它继承了AbstractAutoProxyCreator,实现了SmartInstantiationAwareBeanPostProcessor和BeanFactoryAware。AbstractAutoProxyCreator是Spring AOP的核心骨架,提供了一套统一流程:判断需要代理、收集Advisor、创建代理。
2.2 wrapIfNecessary:三个缓存的含义
AnnotationAwareAspectJAutoProxyCreator在Bean初始化完成后,通过postProcessAfterInitialization进入处理逻辑。AbstractAutoProxyCreator里这个方法不长,但每行都有含义:
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) != bean) { return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; }这里出现了第一个关键缓存:earlyProxyReferences。它记录的是“在循环依赖阶段被提前处理过的Bean”。如果当前Bean已经在早期被包过代理,这里remove出来的值和当前bean相同,就直接返回原始bean,不重复包。这个机制和三级缓存强相关,稍后再展开。
wrapIfNecessary里的处理顺序也很讲究:
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { if (this.targetSourcedBeans.contains(beanName)) { return bean; } if (this.advisedBeans.containsKey(cacheKey)) { return bean; } if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; } Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); if (specificInterceptors != DO_NOT_PROXY) { this.advisedBeans.put(cacheKey, Boolean.TRUE); Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); this.advisedBeans.put(cacheKey, Boolean.FALSE); return proxy; } this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; }三个缓存的角色分别是:
targetSourcedBeans:标记那些通过自定义TargetSource创建出来的Bean,这些Bean不走自动代理流程。advisedBeans:记录某个Bean是否已经被判定为需要代理。它是Boolean值,TRUE表示正在代理中,FALSE表示不需要代理,避免重复判断。earlyProxyReferences:记录循环依赖阶段提前包装过的Bean,防止后置阶段二次包装。
isInfrastructureClass判断的是Advisor、Advice、AopInfrastructureBean这些AOP基础设施类,它们本身不应该被代理。shouldSkip给子类留了扩展点,AspectJAwareAdvisorAutoProxyCreator会在这里跳过“自身就是切面”的Bean。
所以整个判断逻辑可以用一句话概括:先排除不需要代理的,再找有没有可用的拦截器,有就创建代理,没有就原样返回。
2.3 getEarlyBeanReference与三级缓存中的提前代理
循环依赖在Spring里是通过三级缓存解决的,其中第三级缓存存放的是ObjectFactory。当Bean A依赖Bean B、Bean B又依赖Bean A时,A还没初始化完,B就需要拿到A的引用,Spring会通过getEarlyBeanReference提前暴露A。
这里有个容易被忽略的问题:如果A最终需要被AOP代理,那么提前暴露给B的应该是原始A,还是A的代理?
答案必须是代理。否则B拿到的就是原始对象,切面逻辑对B完全失效。所以AbstractAutoProxyCreator实现了SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法:
public Object getEarlyBeanReference(Object bean, String beanName) { Object cacheKey = getCacheKey(bean.getClass(), beanName); this.earlyProxyReferences.put(cacheKey, bean); return wrapIfNecessary(bean, beanName, cacheKey); }注意这里先往earlyProxyReferences里记录了原始bean,然后又调用了wrapIfNecessary。也就是说,循环依赖场景下,代理不是等Bean完全初始化完才创建的,而是在“提前暴露”阶段就可能创建了。这也是为什么说Spring的三级缓存本质上和AOP是深度配合的。
一个有意思的细节:如果A是在早期被代理的,那么后续postProcessAfterInitialization执行时,earlyProxyReferences.remove(cacheKey)拿到的还是原始bean,和当前bean相等,于是直接返回原始bean,不会再执行wrapIfNecessary。这是Spring为了性能做的去重,也避免了两次代理导致的双层嵌套。
3. Advisor从哪里来:切面解析与切点匹配
3.1 从@Aspect到Advisor的转换过程
前面提到,wrapIfNecessary会调用getAdvicesAndAdvisorsForBean获取可用的拦截器。这个方法的返回值类型是Object[],每个元素都是一个Advisor。在AnnotationAwareAspectJAutoProxyCreator的实现里,它会经过findCandidateAdvisors两步走:
protected List<Advisor> findCandidateAdvisors() { List<Advisor> advisors = super.findCandidateAdvisors(); if (this.aspectJAdvisorsBuilder != null) { advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); } return advisors; }第一步,super.findCandidateAdvisors()会去容器里找所有实现了Advisor接口的Bean。第二步,buildAspectJAdvisors()则负责扫描所有Bean,找出带@Aspect注解的类,把它们解析成Advisor。
buildAspectJAdvisors()内部做的事情可以拆成三块:
- 遍历容器里所有BeanName,通过
aspectJAdvisorFactory.isAspect(bean)判断这个Bean是不是切面类。 - 对切面类,用
getAdvisorMethods取出所有方法,凡是带@Before、@Around、@After、@AfterReturning、@AfterThrowing、@Pointcut注解的方法都参与解析。 - 对每一条通知方法,通过
getAdvisor创建对应的Advisor。
解析过程中会用到一个核心组件ReflectiveAspectJAdvisorFactory,它负责把注解和表达式转换成真正的Spring对象。比如@Around方法会被包装成AspectJAroundAdvice,@Before方法会被包装成AspectJMethodBeforeAdvice,而切点表达式会被解析成AspectJExpressionPointcut。
到这里,切面类的职责就清晰了:它本身不被代理,但它的每个通知方法会被拆出来,组合成一个或多个Advisor,等待后续匹配挂在别的Bean上。
3.2 AopUtils.findAdvisorsThatCanApply的类型约束
拿到了所有候选Advisor之后,findAdvisorsThatCanApply会做一次过滤,筛掉对当前Bean不适用的Advisor。这个过滤主要发生在两个层面:
首先是ClassFilter,检查目标类是否能通过切面的类型过滤。然后是MethodMatcher,遍历目标类的所有方法,看是否有方法能匹配切点表达式。
Spring这里做了性能优化:如果一个切面对应的方法没有任何一个能匹配到当前Bean的方法上,那么这个Advisor就会被直接丢弃。而如果某个Advisor的切点是Pointcut类型,Spring会先做一次快速判断,只有候选方法真正匹配上了,才会进入后面的拦截器链构造阶段。
也就是说,切面类虽然写了很多execution(*)表达式,但在运行时不会傻乎乎地对每个方法都做正则匹配。Spring会尽量把不相关的切面在早期排除掉,减少运行时开销。
3.3 切点匹配失败时你会看到什么现象
切点表达式写错是AOP不生效的头号原因,而且它通常是静默失败的——没有异常、没有日志,业务方法正常执行,但切面逻辑就是不触发。
我排查过很多这类问题,最常见的几种情况是:
- 包名写错,比如
execution(* com.example.service..*(..))少写了某个段。 - 方法签名不匹配,比如参数列表漏写了
(..)。 - 目标类不在Spring容器管理范围内,比如直接用
new创建的对象,切点表达式再正确也不会生效。 - 目标类是final方法或private方法,Spring AOP无法拦截。
遇到切面不生效,先别急着怀疑代理工厂,第一件事应该是确认切点表达式能不能匹配上。最简单的方法是临时在切面里加一个@Before无限制切点,比如execution(* *(..)),看它能不能触发。能触发,说明代理链路没问题,问题出在切点表达式上;不能触发,再看代理是否真的生成了。
一个好的习惯是给切面类加上@Component并放在能被扫描到的包下,同时用Debug日志开启org.springframework.aop的日志级别,这样能在启动阶段看到切面是否被找到。
4. 代理工厂的抉择:JDK动态代理还是CGLIB
4.1 DefaultAopProxyFactory的判定逻辑
当wrapIfNecessary判定需要通过createProxy生成代理时,从AbstractAutoProxyCreator到ProxyFactory,最终会走到ProxyFactory.getProxy(),里面调用createAopProxy()选择具体代理策略。这个决策逻辑在DefaultAopProxyFactory里:
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class<?> targetClass = config.getTargetClass(); if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); } else { return new JdkDynamicAopProxy(config); } }翻译成人话就是:当目标类本身是接口、或已经是JDK代理类时,不管怎么配置都只能用JDK动态代理;其他情况下,只要开启了proxyTargetClass或者目标类没有显式提供代理接口,就会用CGLIB。
这里要注意一个常见误解:proxyTargetClass=true不是“必须用CGLIB”的绝对指令,JDK代理仍然会在目标类是接口的情况下被选中。源码判断顺序是先看目标类是不是接口,再看其他条件,所以接口场景永远走JDK。
Spring Boot 2.0之后,官方把spring.aop.proxy-target-class默认值改成了true,这也解释了为什么现在的Spring Boot项目里,默认生成的代理大多是CGLIB代理,哪怕目标类实现了接口也不再自动走JDK。
4.2 JdkDynamicAopProxy.invoke的完整链路
JDK动态代理的核心方法是JdkDynamicAopProxy.invoke。每次通过代理对象调用方法,都会进到这里。它的处理顺序非常有代表性:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; TargetSource targetSource = this.advised.targetSource; Object target = null; try { if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { return equals(args[0]); } else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { return hashCode(); } else if (method.getDeclaringClass() == DecoratingProxy.class) { return ((DecoratingProxy) proxy).getDecoratedClass(); } else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && method.getDeclaringClass().isAssignableFrom(Advised.class)) { return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); } Object retVal; if (this.advised.exposeProxy) { oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } target = targetSource.getTarget(); Class<?> targetClass = (target != null ? target.getClass() : null); List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); if (chain.isEmpty()) { retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args); } else { MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); retVal = invocation.proceed(); } return retVal; } finally { if (targetSource != null) { targetSource.releaseTarget(target); } if (setProxyContext) { AopContext.setCurrentProxy(oldProxy); } } }几个值得注意的点:
第一,equals、hashCode、Advised接口方法不会被拦截器链处理,走的是直通分支,这是为了防止切面逻辑干扰代理对象本身的基础行为。
第二,getInterceptorsAndDynamicInterceptionAdvice是拦截器链构造的核心方法,我下一章单独讲。它的返回值是List<Object>,里面可能是MethodInterceptor,也可能是带了动态方法匹配器的包装对象。
第三,如果拦截器链是空的,就直接反射调用目标方法。这意味着即使生成了代理,只要没有匹配的通知,调用开销也仅仅多了一次反射判断,不会有明显的性能损耗。
4.3 CglibAopProxy的差异化处理
CGLIB代理的入口是CglibAopProxy.DynamicAdvisedInterceptor.intercept。逻辑和JDK版大同小异,但有几个差异化细节值得留意:
- CGLIB通过继承目标类生成子类,所以目标类和目标方法都不能是
final的,否则无法被重写。 - CGLIB生成的代理对象调用目标方法时,不需要经过接口方法的分发逻辑,直接调用父类方法即可。
- 当拦截器链为空时,CGLIB有不同的优化路径:如果类不重写
equals/hashCode,会生成专门的EqualsInterceptor;如果目标是Advised类型,会走StaticUnadvisedInterceptor等特殊分支。
从实践角度看,JDK和CGLIB在绝大多数业务场景下的性能差异可以忽略,真正的选择依据应该是兼容性需求。JDK代理要求目标类必须实现接口,这在服务层很常见;但某些工具类、配置类、第三方库类没有接口,就只能用CGLIB兜底。
Spring Boot默认选择CGLIB,还有一个额外好处:不用为每个接口单独写代理逻辑,也减少了接口调整对代理的干扰。
5. 方法调用时的拦截器链:ReflectiveMethodInvocation.proceed
5.1 getInterceptorsAndDynamicInterceptionAdvice如何建链
拦截器链的构造发生在每次方法调用时,不是在代理创建时。为什么这么设计?因为切点表达式有些是“动态匹配”的,需要根据运行时参数判断,提前缓存一整条链会导致匹配结果过期。
getInterceptorsAndDynamicInterceptionAdvice的核心逻辑在DefaultAdvisorAdapterRegistry.getInterceptors:
public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException { List<MethodInterceptor> interceptors = new ArrayList<>(3); Advice advice = advisor.getAdvice(); if (advice instanceof MethodInterceptor) { interceptors.add((MethodInterceptor) advice); } for (Adapter adapter : this.adapters) { if (adapter.supportsAdvice(advice)) { interceptors.add(adapter.getInterceptor(advisor)); } } return interceptors.toArray(new MethodInterceptor[0]); }Spring内置了三套适配器,把不同类型的Advice转换成统一的MethodInterceptor:
| 原始Advice类型 | 适配器 | 转换结果 |
|---|---|---|
| MethodBeforeAdvice | MethodBeforeAdviceAdapter | 方法执行前执行通知 |
| AfterReturningAdvice | AfterReturningAdviceAdapter | 方法返回后执行通知 |
| ThrowsAdvice | ThrowsAdviceAdapter | 方法抛出异常后执行通知 |
@Around本身就是MethodInterceptor,所以不需要适配器,直接进链。这样设计的好处是:Spring AOP的拦截器链统一了回调模型,不管你是哪种通知类型,最终都表现为一个MethodInterceptor。
5.2 proceed的递归推进与动态方法匹配
拦截器链构造好之后,会被封装进ReflectiveMethodInvocation,然后调用proceed()。这个方法虽然短,但它是整个AOP的“发动机”:
public Object proceed() throws Throwable { if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { return invokeJoinpoint(); } Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass()); if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) { return dm.interceptor.invoke(this); } else { return proceed(); } } else { return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); } }初次看这段代码可能会绕,但核心就是一个索引从-1开始的循环推进。每次proceed先判断当前拦截器是不是最后一个:如果是,就调用真正的目标方法;不是,就把索引加一,取出下一个拦截器,调用它的invoke方法,并把当前MethodInvocation传给它。
关键点在于:MethodInterceptor内部可以继续调用invocation.proceed(),形成一个递归的“洋葱模型”。@Around通知是最典型的例子,它可以在proceed()前后写逻辑,也就实现了方法执行的“环绕”。而@Before通知的invoke实现则是先执行通知代码,再调用proceed(),相当于只占洋葱的外层。@After通知恰恰相反,它把proceed()放在try-finally里,保证无论正常返回还是抛出异常,都能执行。@AfterReturning只在proceed()正常返回后执行,@AfterThrowing只在proceed()抛出异常后执行。
这种设计解释了一个问题:为什么环绕通知能控制整个执行流程,而前置通知不能。因为环绕通知通过控制“是否调用proceed()”以及“什么时候调用proceed()”,掌握了整个方法调用的开关。
5.3 通知顺序的排序规则与常见误区
拦截器链的顺序直接决定了多个切面的执行顺序。Spring通过OrderComparator按Advisor的order属性排序,order越小优先级越高,越在链的外层,也就是越先执行。
这里有一个常见的误区:很多人以为@Order注解加在切面类上,就一定能控制切面执行顺序,但Spring AOP里真正参与排序的是Advisor的order值,这个值从哪来取决于切面类的创建方式。
如果是基于@Aspect注解的切面,Spring在解析时会通过AspectJPrecedenceInformation读取@Order的值作为Advisor的order。如果在纯XML配置里手动声明Advisor,那么order属性需要显式设置,不设置默认是Integer.MAX_VALUE,会排在所有显式排序切面的后面。
另一个容易踩的坑是:@Order的生效范围只限于同一个切面类内部产生的Advice之间的顺序。不同切面类之间的顺序,如果@Order值一样,会退化为不可预期的默认顺序。所以多切面协同工作时,最好给每个切面都设置明确的order值,不要依赖默认值。
事务切面和自定义切面的顺序尤其值得注意。TransactionInterceptor本身是一个MethodInterceptor,如果自定义切面的order比事务小,自定义逻辑就会在事务外执行,此时在自定义切面里访问TransactionSynchronizationManager可能拿不到任何事务状态;反过来,如果自定义切面的order比事务大,它就在事务内执行,可以在控制事务提交时机。这个顺序问题在日志审计场景里会带来很微妙的行为差异。
6. 把源码拉回实战:三个绕不开的坑与验证手段
6.1 this.method()自身调用为什么绕过代理
这是Spring AOP被问烂也踩烂的一个坑:同一个类里的方法A调用了方法B,切点表达式只覆盖了方法B,但B的切面没有生效。
原因现在很清楚了:Spring AOP的代理只包装在容器返回的Bean引用上,类内部的this引用指向的是原始对象,不是代理对象。当外部通过代理调用方法A时,代理对象调到A方法,A内部this.methodB()直接调用原始对象的B方法,代理根本不在调用链上。
解决思路有几种:
- 拆分Bean,把方法B放到另一个被Spring管理的Bean里,从外部注入调用。
- 注入自身代理,比如
@Resource注入SelfProxy,但这要求代理存在且类型兼容。 - 使用
AopContext.currentProxy()获取当前代理,但前提是开启了exposeProxy=true。 - 对于
@Transactional这类场景,把两个方法拆开往往是最干净的方案。 - 如果方法B确实必须留在同一个类里,可以把
proceed()的逻辑提取成一个新的公共方法,让外部代理来调用。
从我自己的经验看,拆Bean虽然改动多一点,但代码最直白,也最容易测试。不到万不得已,不建议用AopContext.currentProxy()这种偏门手段,因为它把代理细节暴露给了业务代码,破坏了AOP的透明性。
6.2 exposeProxy=AopContext.currentProxy()的边界
AopContext是Spring提供的一个线程本地变量,用于在切面内部获取当前代理对象。它有两个明显的限制:
第一,必须先开启@EnableAspectJAutoProxy(exposeProxy = true),否则AopContext.currentProxy()会抛IllegalStateException。第二,它只对当前线程有效,如果把代理对象传给了其他线程,线程上下文就会丢失。
实际使用中,AopContext.currentProxy()最常见的用途是在自身调用场景里取回代理,但它的副作用是引入了对Spring AOP内部机制的强依赖。如果哪天切面被移除,currentProxy()调用就会直接报错,业务代码反而变成了最脆弱的那个点。
所以我通常只在“临时救火”时用它,长期方案永远是调整代码结构。看源码的人都会明白一个道理:AOP的设计初衷是想让业务代码感知不到代理的存在,任何反向感知都是在和框架设计初衷对着干。
6.3 自定义注解+日志记录实践与切面验证方法
用AOP做日志记录是网上讨论最多的话题,这里给一个比较稳妥的实践模板:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface OperationLog { String value() default ""; }@Aspect @Component public class OperationLogAspect { @Around("@annotation(operationLog)") public Object logOperation(ProceedingJoinPoint pjp, OperationLog operationLog) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); long cost = System.currentTimeMillis() - start; // 这里可以接入数据库、MQ或日志平台 System.out.println(operationLog.value() + " cost=" + cost + "ms"); return result; } }这里用的切点表达式是@annotation(operationLog),它和@annotation(com.example.annotation.OperationLog)等价,但好处是能把注解对象直接注入到通知方法参数里,不需要再通过反射去拿。
验证切面是否生效,我习惯分三步走:
先确认代理是否生成。可以在启动日志里开org.springframework.aop的Debug级别,看一下wrapIfNecessary是否对目标Bean返回了代理。也可以直接打印Bean.getClass(),如果类名里带了$$EnhancerBySpringCGLIB$$或者$Proxy,说明代理已经生成。
然后确认切点是否匹配。临时把切点改成execution(* *(..)),能触发就说明链路没问题,问题在表达式上。
最后确认拦截器链的顺序。开启org.springframework.aop.interceptor的日志,协助定位多个Advice的执行顺序。
日志切面本身有一个容易被忽略的性能细节:如果切面内部要做数据库写入或者远程调用,一定不要放在同步的业务线程里,否则每次被拦截的业务调用都会多出一次IO开销。更好的做法是切面只负责采集日志数据,扔进异步队列,由消费端统一落库。
源码读到这一步,我最大的体会是Spring AOP其实只做了两件事:判断哪些Bean需要代理,然后为它们装上一条拦截器链。剩下的所有“魔法”,都是在这两件事周围长出来的工程化细节。如果你也想深入读这段源码,我建议从AbstractAutoProxyCreator入手,先把几个缓存的流转弄明白,再去看ProxyFactory和ReflectiveMethodInvocation,会比从头啃源码快得多。