配置AOP这事,很多人的第一反应是看一眼配置能用就行。但一旦涉及复杂切点、多个通知顺序、或者不同代理方式的选择时,光是“能用”就不够了。你迟早会遇到“明明配置没问题,但切面就是不生效”这种问题,然后被迫去翻Spring源码。与其到时候现查,不如一开始就把advisor方式和XML方式这两条主流配置路线的原理理清楚。
这篇文章不聊泛泛的概念,直接从“配置背后到底发生了什么”这个角度切入,说说两种方式的核心差异、运行原理、底层代理机制,以及在实际项目中怎么选、怎么排雷。适合刚学Spring AOP正被各种名词绕晕的新人,也适合写过不少切面但没深究过内部机制的开发者,查漏补缺,看完能实打实解决配置疑惑。
1. 两种配置方式的整体认知:它们到底在配置什么东西
先说个基础判断:advisor和XML配置AOP,本质上做的是同一件事,就是“把切点匹配规则和通知逻辑绑定起来,生成代理对象”。区别在于绑定的方式不同,一个是直接用Spring的Advisor对象体系来组织,另一个是通过XML标签让Spring框架帮你在背后组装出这套体系。
1.1 先搞懂AOP里的三个基础角色
任何AOP配置,不管写法多花哨,都绕不开三个基础概念:切点(Pointcut),通知(Advice),切面(Aspect)。
切点负责回答“在哪里切入”,它定义的是“哪些类的哪些方法需要被拦截”。注意这里包含两个维度:一个是类级别的匹配,比如com.example.service.*;一个是方法级别的匹配,比如public * com.example.service.*.*(..)里面的方法名和参数匹配。这两个维度是独立存在的,Spring里面分别由ClassFilter和MethodMatcher两个子接口来负责。
通知负责回答“切入之后干什么”,它定义了额外的逻辑。Spring里通知有五种类型:Before、AfterReturning、AfterThrowing、After(最终)、Around。其中Around是最特殊的,它把整个方法调用包起来,可以在前后都插入逻辑,甚至可以完全替换原方法的执行。
切面则是一个更高层的概念,它把切点和通知组合起来,形成一个完整的“规则+动作”单元。在Spring AOP的发展历程里,Aspect这个概念最初是从AspectJ那边借鉴过来的,后来Spring自己的注解配置也沿用了这个名词。但在Spring内部真正存储配置信息的,其实是Advisor接口。
1.2 Spring AOP的运行链路:从配置到代理对象
Spring AOP的整个执行流程可以简化为三步:解析配置、生成Advisor、创建代理。
解析配置这一步,就是把XML或注解里的信息读取出来。XML方式通过BeanDefinition解析器来处理<aop:config>标签,注解方式通过AnnotationAwareAspectJAutoProxyCreator来扫描@Aspect标注的类。这个环节输出的是一组Advisor候选对象。
生成Advisor这一步比较关键。不管是哪种配置来源,最终都会被统一转换成Advisor对象。Advisor接口定义很简单,它就是“持有一个Pointcut和一个Advice的组合体”。所以整个Spring AOP在运行时根本不管你原来配置用的是<aop:aspect>标签还是@Aspect注解,也不管是@Before还是<aop:before>,统统都会被抽象成一个一个的Advisor来统一处理。
创建代理这步,是根据前面得到的Advisor列表,判断目标类需要哪种代理方式,然后生成代理对象。判断依据就是目标类是否实现了接口,以及在配置里有没有强制指定proxy-target-class。这里涉及到JDK动态代理和CGLIB动态代理两种底层机制,后面会单独展开讲。
理解了这条链路,就能明白一个重要的推论:不管你用什么样的配置语法,它在Spring运行时都是等价的。XML、注解、Advisor编程式配置,殊途同归,最终都汇入同一套代理机制。
2. Advisor方式配置切面:从接口到运行时背后的原理
Advisor这个名词在Spring AOP里存在感很强,但很多人对它的理解停留在“好像比Aspect低一层”。确实,它是Spring内部更底层的抽象,也是AOP配置在此刻真正操作的实体。
2.1 Advisor为什么比Aspect更底层:接口设计拆解
看Advisor接口的设计,能反映出Spring的设计思路。org.springframework.aop.Advisor只定义了一个方法getAdvice(),用来返回通知对象。而大部分场景使用的PointcutAdvisor在此基础上扩展了两项:getPointcut()返回切点对象,isPerInstance()标记是否按实例创建。
这个设计意味着什么?意味着一个Advisor就是一个“孤立的切面单元”。它不需要知道别的Advisor的存在,不需要感知全局配置,只需要管好自己的切点和通知。这种原子化的设计让AOP配置变得非常灵活,你可以编程式地组装多个Advisor,把它们交给ProxyFactory来批量生成代理,也可以插到BeanPostProcessor里对特定Bean动态增强。
在<aop:advisor>配置中,一个标签就对应一个PointcutAdvisor实例。通常的做法是先声明一个普通Bean,它的类型就是某个Advice实现类(比如MethodBeforeAdvice或AspectJMethodBeforeAdvice),然后在<aop:advisor>标签里通过advice-ref指向这个Bean,再通过expression指定切点表达式。这样配置出来的结果,就是一个典型的DefaultPointcutAdvisor,内部包含AspectJExpressionPointcut和对应的Advice。
2.2 Advisor配置的完整过程和生效链路
实际项目中,直接用编程式Advisor的场景不算特别多,但一旦用到就会非常优雅。比如想对某一组Bean做统一增强,不希望通过<aop:config>大动干戈,这时可以直接写一个BeanPostProcessor,在postProcessAfterInitialization里面针对特定Bean创建Advisor并生成代理。
来看一个典型的编程式Advisor配置流程:
@Configuration public class AdvisorConfig { @Bean public DefaultPointcutAdvisor myAdvisor() { // 声明一个Advisor实例 DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(); // 创建切点:表达式匹配com.example包下所有类的所有方法 AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression("execution(* com.example..*.*(..))"); // 创建通知:这里用一个简单的MethodInterceptor(环绕通知) MethodInterceptor advice = invocation -> { try { System.out.println("开始时间记录:" + System.currentTimeMillis()); return invocation.proceed(); } finally { System.out.println("结束时间记录:" + System.currentTimeMillis()); } }; // 组装 advisor.setAdvice(advice); advisor.setPointcut(pointcut); return advisor; } }但光定义这个Bean还不够,要让Spring在创建目标Bean时发现它并应用上去,需要配合机制把这个Advisor塞进代理链路里。有两种常见做法,一种是使用ProxyFactoryBean,把interceptorNames指向该Advisor;另一种是用强制自动代理:
@Bean public BeanPostProcessor advisorAutoProxyCreator() { // 让所有Bean在初始化后被Scanner检查,匹配的Bean会被自动代理 return new BeanPostProcessor() { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof Advised) { return bean; } // 简易判断:只代理Service后缀的类 if (beanName.endsWith("Service")) { ProxyFactory factory = new ProxyFactory(bean); // myAdvisor从容器里取,这里简化了获取逻辑 factory.addAdvisor(container.getBean("myAdvisor", PointcutAdvisor.class)); return factory.getProxy(); } return bean; } }; }这种方式的优势在于完全掌控注入点。你可以在某个Bean初始化完成后,根据当前状态决定是否要增强它,甚至可以根据Bean的实际属性动态决定用哪个Advisor,这是注解和XML配置很难做到的精细控制。
2.3 为什么说Advisor方式遇到的坑更少
有一类常见问题,用XML或注解配置时特别容易踩:多个切面的执行顺序不可控。注解方式的顺序依赖于Bean名称的字符串排序和AspectJ注解的优先级规则,如果不小心,很容易排列出意外的顺序。
而Advisor方式对顺序的控制非常直观。因为Advisor是一个个独立对象,你可以把它们放进列表,列表的顺序就是代理链执行的顺序。ProxyFactory在创建代理时会遍历Advisor数组,前一个Advisor的执行顺序天然领先于后一个。这种“所见即所得”的顺序控制,在需要严格编排切面逻辑的场合是好用的。
不过也要说清楚,Advisor方式的问题在于样板代码多。每增加一个切面,就要写一个Advisor的构造代码,远不如注解一行@Aspect来得爽快。所以实际项目中,我一般只在特殊场景下用Advisor:比如需要对某些Bean做条件代理,或者实现动态增强时才会选它。
3. XML声明式配置AOP的原理:标签背后的组装逻辑
XML配置虽然在Spring Boot项目里不太常见了,但理解它依然是理解整个Spring AOP机制的钥匙。尤其对于维护老项目的团队来说,<aop:config>几乎是必读内容。
3.1<aop:aspect>和<aop:advisor>到底有什么不同
在XML配置里,<aop:config>是根标签,内部可以放<aop:pointcut>、<aop:advisor>和<aop:aspect>三个子标签。这里最容易混淆的就是后两个。
<aop:advisor>是最贴近底层语义的配置,它直接对应Spring的一个Advisor。它的两个关键属性:advice-ref指向一个通知Bean(这个Bean通常是MethodInterceptor或Advice的实现),pointcut-ref或execution指定切点。Spring解析这个标签时,会创建一个DefaultPointcutAdvisor,把所有信息和关联的Advice包装在一起。
<aop:aspect>则是更高层的抽象,对应AspectJ里的切面概念,通常由一个普通的JavaBean承载,里面的方法配合增强标签来工作。它内部可以有<aop:before>、<aop:after>、<aop:around>等子标签,这些标签会从JavaBean里定位对应方法,把方法反射包装成Spring的Advice,再和切点组合成一个Advisor。这一步非常关键:一个<aop:aspect>里写了多个通知,Spring会把它拆分成多个Advisor,每个Advisor包含同一个Pointcut和其中一个通知方法。
所以在运行时层面,<aop:aspect>和<aop:advisor>的产物是一样的:都是Advisor实例列表。区别只是写法上的“粒度”不同:<aop:advisor>是直接组装,一个标签对应一个Advisor;<aop:aspect>是间接转换,一个标签元素通过内部多个增强标签生成多个Advisor。
3.2 XML配置的执行顺序与Spring的转换步骤
Spring处理XML AOP配置的核心类是ConfigBeanDefinitionParser。这个类会逐个解析<aop:config>里的子元素,把每个子标签解析成对应的BeanDefinition注册到容器中。
解析过程中,它内部还维护一个AopNamespaceUtils工具类,负责注册自动代理创建器。默认情况下,<aop:config>被解析时会向上下文注册一个AspectJAwareAdvisorAutoProxyCreator的BeanDefinition,这就是一个BeanPostProcessor,它的职责是在每个Bean初始化完成后,检查所有装配好的Advisor,看当前Bean是否匹配某个Advisor的Pointcut,如果匹配就创建代理。
那<aop:aspect>的JavaBean方法是怎么变成Advice的?解析器在遇到<aop:before>标签时,会生成一个AspectJMethodBeforeAdvice实例的BeanDefinition。这个Advice内部记录了目标Bean名称和对应的方法名称。在代理创建时,Advice通过反射调用目标Bean的方法来执行切面逻辑。用一句话总结:XML声明式配置的核心逻辑,就是“把标签展开成BeanDefinition,再让自动代理创建器把所有BeanDefinition转换好的Advisor应用到目标Bean上”。
3.3 XML配置的完整示例与关键属性梳理
直接上一份实际可用的XML配置,注释里写了各自的作用。这份配置我常用在生产项目中,兼顾了声明式和面向切面编程中比较复杂的场景。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 1. 定义通知类:这是一个普通Bean,里面写具体逻辑 --> <bean id="logAspect" class="com.example.aop.LogAspect"> <property name="prefix" value="【日志】"/> </bean> <aop:config> <!-- 2. 声明公共切点表达式 --> <aop:pointcut id="servicePointcut" expression="execution(* com.example.service..*.*(..))"/> <!-- 3. 使用aspect方式,直接把切面Bean的方法绑定到通知类型上 --> <aop:aspect ref="logAspect"> <aop:before method="logBefore" pointcut-ref="servicePointcut"/> <aop:after-returning method="logAfter" returning="result" pointcut-ref="servicePointcut"/> <aop:after-throwing method="logException" throwing="ex" pointcut-ref="servicePointcut"/> <aop:around method="performanceMonitor" pointcut-ref="servicePointcut"/> </aop:aspect> </aop:config> </beans>对应的LogAspect类:
public class LogAspect { private String prefix; // getter/setter省略 public void logBefore(JoinPoint joinPoint) { System.out.println(prefix + "前置通知,方法:" + joinPoint.getSignature().getName()); } public void logAfter(Object result) { System.out.println(prefix + "后置通知,返回值:" + result); } public void logException(Throwable ex) { System.out.println(prefix + "异常通知:" + ex.getMessage()); } public Object performanceMonitor(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); System.out.println(prefix + "方法耗时:" + (System.currentTimeMillis() - start) + "ms"); return result; } }需要特别注意的是,<aop:around>的方法签名必须接收ProceedingJoinPoint参数,并且调用proceed()来放行原始方法;而<aop:before>等方法一般使用JoinPoint作为参数,两者虽然名字像但是不能随便互换,Spring对方法参数类型有严格校验,参数类型不匹配容器启动时可能直接报错。
3.4 为什么说XML配置适合大型传统项目
XML配置的优势相当明确:配置和代码完全分离,不需要改Java代码就能调整切面逻辑。这在某些合规要求高、需要运维人员独立调整日志监控的场景里特别实用。另外,XML配置具备“全局可见性”,打开一个文件就能看到系统所有AOP切入点,不用像注解那样各个类里翻来翻去。缺点同样明显:维护成本高,写起来啰嗦,表达式写错了不容易在编译期发现,只能在启动时排查。要是有选择的余地,我更推荐注解方式配合类级别和表达式统一管理;但如果你在维护老项目,这份XML示例可以直接用,同时心里要有数:XML配置和注解配置的未来趋势都是收拢到AspectJ语法体系,表达式语法可以互相移植。
4. 底层代理机制:JDK动态代理与CGLIB动态代理的选择逻辑
了解配置层面的原理之后,必须往下钻一层,看看Spring究竟怎么把Advisor应用上去、生成的目标对象是个什么东西。这就是整个AOP运行原理最核心的一环:动态代理机制。
4.1 JDK动态代理:基于接口的轻量方案
JDK动态代理是Java平台自带的代理机制,位于java.lang.reflect包下,核心类有Proxy和InvocationHandler。它的运行机制是这样:JVM在运行时为目标接口动态生成一个代理类,这个代理类和目标类实现了相同的接口,当外部调用这个接口方法时,实际执行的是InvocationHandler.invoke()方法里的逻辑。
在invoke()方法内部,Spring会根据当前方法名去匹配所有已注册的Pointcut,如果匹配成功,就按顺序执行对应的Advice逻辑,然后在适当的时机调用method.invoke(target, args)放行到真实目标对象。如果不匹配,直接反射调用目标方法,不做额外增强。
JDK动态代理的优点很明显:不依赖任何第三方库,反射开销相对小,生成的类也少。但它的局限也很致命:只能代理接口。如果目标类没有实现任何接口,或者说调用方拿到的Bean不是通过接口引用的,JDK动态代理根本无法生成代理对象。
举一个典型场景:项目里定义了UserService接口和UserServiceImpl实现类,注入时用的是UserService接口类型,那么Spring可以选用JDK动态代理。但如果某天代码里直接@Autowired UserServiceImpl,并且Spring选择了JDK代理方式,就会报BeanNotOfRequiredTypeException,因为注入的实例实际上是一个Proxy对象,它实现的是UserService而不是UserServiceImpl,类型上不匹配。
4.2 CGLIB动态代理:基于继承的强力方案
CGLIB(Code Generation Library)是一种比JDK动态代理更“底层”的代理机制,它通过生成目标类的子类来代理目标对象。子类会重写父类的非final方法,在重写逻辑里插入Advice的执行代码。
CGLIB代理的实现类在Spring 4之后整合进了org.springframework.cglib包中,核心类是Enhancer和MethodInterceptor。Enhancer负责设置父类(即目标类)、设置回调逻辑(即MethodInterceptor),然后生成子类字节码。每次调用代理方法,都会先经过MethodInterceptor.intercept()方法,在这个方法里做切点匹配和Advice链调用。
CGLIB的一个显著特点是不需要目标类实现任何接口,继承机制决定了它能代理任何普通类。但是两个注意点:一,目标类或方法不能是final修饰的,因为是继承生成子类再重写方法,final方法无法重写,此时代理会退化成直连目标类调用,切面逻辑会跳过;二,CGLIB创建代理对象的成本相对JDK动态代理要高一些,因为要生成字节码。
4.3 Spring的代理选择逻辑与proxy-target-class
Spring判断用哪种代理方式的逻辑其实很简单,集中在DefaultAopProxyFactory里:如果proxy-target-class为true,直接用CGLIB;如果为false(默认),再判断目标类是否实现了接口,实现了就选JDK,没实现就退回CGLIB。
Spring Boot 2.x以后默认行为改了:Spring Boot应用默认使用CGLIB代理,即使目标类实现了接口,也会强制proxy-target-class=true。这个变化的官方理由主要是为了避免类型转换问题的困扰,同时CGLIB经过多年优化后性能差距已经很小了。但在老项目的Spring XML环境里,默认仍是JDK优先,所以如果你维护老项目,特别容易遇到“接口和实现类混用导致注入失败”的问题。
给个判断表,方便快速定位场景:
| 目标类情况 | 默认代理方式(Spring Boot 2.x+) | 默认代理方式(老XML项目) | 隐患 |
|---|---|---|---|
| 实现接口,注入时用接口类型 | CGLIB | JDK | 无 |
| 实现接口,注入时用实现类类型 | CGLIB | JDK | 老项目可能BeanNotOfRequiredTypeException |
| 没实现接口的普通类 | CGLIB | CGLIB | 目标类final会有问题 |
| 抽象类 | CGLIB(不可用) | CGLIB(不可用) | 无法被代理,需注意 |
4.4 代理机制对“切面不生效”类问题的影响
搞清楚了代理选择逻辑,很多诡异问题就有了解答思路。比如方法内部自调用不走代理的问题,这和动态代理机制直接相关。自调用指的是this.methodB()这种调用,它绕过的是代理对象,直接在目标类内部执行了原方法。因为代理对象只能在外部调用时介入,内部this调用根本没有经过Proxy引用。这个现象在JDK和CGLIB代理下都存在,除非把目标类自己注入自己,显式通过代理对象调用。
另外,getClass()在代理对象上返回的也是代理类而不是原始类。如果代码里有bean.getClass().getAnnotation()这类操作,在代理对象上很可能拿不到目标类上的注解,这也是动态代理机制带来的坑。遇到这种情况可以通过AopUtils.getTargetClass()获取原始目标类。
5. 实操演练:从零完成一个带日志与事务的AOP配置
前面把原理讲得比较多,现在落到实操层面,从零完整走一遍。这里目标场景是:给com.example.service包下所有Service方法打印日志,再包裹一个简单的性能监控,同时在指定方法上挂载事务拦截。为了体现两种配置方式的差异,我会写上对应实现。
5.1 使用XML方式实现的完整配置
XML配置适合那种不想动Java代码、希望运维随时改切面的场景。先是spring-context.xml里配置AOP相关:
<aop:config> <!-- 全包方法切入,除开某些特殊方法 --> <aop:pointcut id="serviceMethods" expression="execution(public * com.example.service..*.*(..))"/> <!-- 日志切面 --> <aop:aspect ref="logAspect"> <aop:around method="logAround" pointcut-ref="serviceMethods"/> </aop:aspect> <!-- 事务切面:这里直接引用已有的transactionManager --> <aop:advisor advice-ref="transactionInterceptor" pointcut-ref="serviceMethods" order="1"/> </aop:config> <!-- Spring自带的事务拦截器,利用TransactionInterceptor把事务管理器和切面绑定 --> <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"> <property name="transactionManager" ref="transactionManager"/> <property name="transactionAttributes"> <props> <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop> <prop key="save*">PROPAGATION_REQUIRED</prop> <prop key="update*">PROPAGATION_REQUIRED</prop> <prop key="delete*">PROPAGATION_REQUIRED</prop> </props> </property> </bean>这份配置里用到了<aop:advisor>,并且把日志切面和事务拦截器同时配置在同一个<aop:config>下。这两个Registry会被Spring顺序执行:order="1"的优先级更高,事务外层包裹,日志内层执行细节。这里执行的细节很讲究:假如事务通知先于日志通知运行,整个方法入口先进事务,然后进入日志监控方法,日志包裹住了业务逻辑;如果顺序反了,日志先运行,事务再运行,其实出入不大,但如果你在日志里拿到了事务提交/回滚的状态,顺序就很重要了。
5.2 使用Advisor编程方式实现的等价方案
XML虽直观,但你要是想用代码完全控制同样的配置,就可以把上面这套转换成编程式Advisor。这种写法适合在公共模块里自己构建一套统一配置,然后把不同包的不同切入点通过条件装配组合起来。
@Configuration public class AspectConfiguration { @Bean public TransactionInterceptor transactionInterceptor(PlatformTransactionManager transactionManager) { TransactionInterceptor interceptor = new TransactionInterceptor(); interceptor.setTransactionManager(transactionManager); Properties props = new Properties(); props.setProperty("get*", "PROPAGATION_SUPPORTS,readOnly"); props.setProperty("save*", "PROPAGATION_REQUIRED"); props.setProperty("update*", "PROPAGATION_REQUIRED"); props.setProperty("delete*", "PROPAGATION_REQUIRED"); interceptor.setTransactionAttributes(props); return interceptor; } @Bean public AdapterAdvisor logAdvisor() { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression("execution(public * com.example.service..*.*(..))"); MethodInterceptor logAdvice = invocation -> { long start = System.currentTimeMillis(); try { return invocation.proceed(); } finally { System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms"); } }; DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(); advisor.setAdvice(logAdvice); advisor.setPointcut(pointcut); advisor.setOrder(1); return advisor; } @Bean public AdapterAdvisor transactionAdvisor(TransactionInterceptor transactionInterceptor) { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression("execution(public * com.example.service..*.*(..))"); DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(); advisor.setAdvice(transactionInterceptor); advisor.setPointcut(pointcut); advisor.setOrder(2); return advisor; } }这段代码在业务上是基于DefaultPointcutAdvisor组装出两个Advisor,一个是日志的,一个是事务的,顺序控制通过setOrder实现。这个配置写完后,需要有自动代理创建器或者BeanFactoryTransactionAttributeSourceAdvisor这样的扩展点把Advisor应用到具体Bean上。
5.3 运行验证:切面生效后看到什么
配置完之后启动应用,只要切点能正确匹配,调用UserService.saveUser()方法时,输出大致如下:
方法其实还没进来,事务先开启了 【性能】开始时间:2025-01-15T15:30:10.123 实际执行业务代码... 【性能】结束时间:2025-01-15T15:30:10.456,耗时333ms 事务提交了顺序的具体表现取决于order值。如果日志Advisor优先,会先打印“开始”,然后进入事务拦截器,事务开启后执行业务方法,再逐层返回打印耗时,最后提交事务。用这套观察顺序,就能验证自己配置的通知顺序是否正确,没有输出顺序也说明切点没匹配上或者代理未生成。
6. 常见问题与排查技巧:AOP配置不生效时怎么办
这部分是干货里的干货。我把自己踩过的坑按“症状、原因、解法”整理成速查表,遇到问题时对着查即可。
6.1 高频问题对照表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 切面完全不执行 | 切点表达式写得不匹配包名或方法名 | 用AopUtils或日志排查实际调用的类和方法签名 | 调整表达式,建议先写execution(* com.example..*.*(..))再收窄 |
启动报BeanNotOfRequiredTypeException | 注入用具体实现类但代理类型是JDK | 看日志里注入的bean实例class | 注入接口,或强制proxy-target-class=true |
| 自调用时切面不执行 | 方法内部用this调用另一个方法,代理对象被绕过 | 打断点看调用栈是否是ReflectiveMethodInvocation | 注入代理对象引用自身,或者把内部调用拆到另一个Bean |
| 切面时灵时不灵 | 有些方法为final | 检查目标类的final方法定义 | 去掉final,或改用接口设计 |
| 多个切面顺序不对 | 没有设置order值,按默认排序 | 在日志前后打印标记观察顺序 | 给Advisor或@Order显式设定顺序 |
@Aspect类里的方法包装异常 | 方法参数类型和对应通知标签不匹配 | 检查方法是否使用ProceedingJoinPoint却配了<aop:before> | 按通知类型匹配参数类型 |
| XML配置解析报错 | 缺少aop命名空间或schema | 检查xmlns:aop和xsi:schemaLocation | 补全命名空间 |
6.2 定位切面不生效的三个排查步骤
第一步,看Spring版本和代理方式。用AopUtils.isAopProxy(bean)可以快速判断当前Bean是不是代理对象,返回false说明代理环节本身没介入。这不是在代码里硬写,而是说你可以临时在代码输出或者debug表达式里调用这个工具方法,来确认Spring到底有没有创建代理。
第二步,看切点表达式语法。AspectJ切点表达式很容易因为“一个空格”翻车,例如execution (public *)里的空格、通配符数量和包名大小写。建议先在@Aspect里写一个临时切点表达式来做最小化验证,确认能匹配后,再套用到XML里,逐步扩大范围。
第三步,看容器中是否同时注册了多个自动代理创建器。有些项目同时引入了Spring AOP和AspectJ相关依赖,可能造成多个AbstractAutoProxyCreator实例抢占代理。如果出现这种情况,容器里会注册两个都叫“internalAutoProxyCreator”之类的Bean处理器,容易导致代理规则混乱。检查方法是排除重复依赖,尤其注意老项目同时引入spring-boot-starter-aop和旧Spring版本依赖的情况。
6.3 关于代理对象的最强排查技巧
最终极的排查方式就是看Json:把你注入的Bean打出来,看class属性里的内容。JDK代理的类名通常是$Proxy123这种带$的;CGLIB代理的类名通常是$$EnhancerBySpringCGLIB$$这种明显变长的。看到这些形态,就能确认代理生效了。如果你的Bean的class是UserServiceImpl本身,那就说明完全没进代理。
我在排查时的经验是,把这些信息统一输出到日志里,比其他任何方式都快:
System.out.println(bean.getClass().getName()); // 输出样式示例: // com.example.service.UserServiceImpl // com.example.service.UserServiceImpl$$EnhancerBySpringCGLIB$$1234abcd // com.sun.proxy.$Proxy12Class名称直接说明了问题所在的代理流派,也能帮助你判断是不是某个切面类贴错位置导致的代理遗漏。这个方法没有副作用,生产上临时打印也行,排查完删掉即可。
7. 个人经验总结:选择建议与配置心得
把所有原理和坑讲完之后,结合我自己的项目经验,聊聊什么时候用什么方式。这些不是教科书答案,是比较务实的参考。
对于新项目,我强烈推荐注解方式(@Aspect+@Around等),配合Spring Boot默认的CGLIB代理,简单直接,代码可读性好。这时候没必要去碰Advisor接口,除非你在写框架级组件或公共starter。
对于老项目或者对切面配置有“不修改代码就能调整”诉求的项目(比如审计日志切换规则由非开发人员维护),XML方式和<aop:advisor>是更好的载体。它把规则外置化,所见即所得,但注意给关键<aop:advisor>设置order值,防止切面顺序失控。
对于框架开发场景,比如自己封装分布式锁、多租户数据隔离、权限缓存处理等,Advisor编程式配置往往最顺手。因为框架源码里经常需要根据注解或配置动态组装切面,直接在代码里new一个DefaultPointcutAdvisor比解析XML配置要可控得多。
最后分享一个我自己的习惯:无论用哪种方式,都要在项目里维护一份“切面清单”,记录所有切点表达式、通知顺序和试点效果。这个习惯帮我解决过很多次“看起来切面很多,实际作用含糊”的问题。AOP配置本质上是给代码织了一张隐形的逻辑网,你最好在网络外面留一张地图,而不是等到业务事故时才去翻XML。
这篇文章把advisor和XML两种配置方式从原理到实操都过了一遍,底层动态代理机制也单独展开讲了。希望你能用自己的工程实践来佐证这些结论,少踩几个我当年踩过的坑。