news 2026/9/11 19:50:59

Spring XML AOP实战:从动态代理到切点表达式,老项目维护必读指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring XML AOP实战:从动态代理到切点表达式,老项目维护必读指南

这一篇Spring之旅,是被一个老项目逼出来的。原本我的计划是直接开讲注解式AOP,毕竟现在新项目里谁还会打开beans.xml写一大堆<aop:config>呢?可前阵子接手了一个七年前的老系统,核心服务的操作日志、权限校验、参数加密,全部是通过XML AOP织进去的。我盯着那几百行的beans.xml,加上各种<aop:aspect的ref引用,整整理了一下午才把切面关系网搞清楚。后面我花了一周时间,把Spring基于XML的AOP开发从配置文件到代理生成链路认真捋了一遍。这篇就当是整理出来的学习笔记,不管你是刚接触Spring的新手,还是被派去维护老项目的“接盘侠”,应该都能少走一些弯路。

1. 为什么现在还要学XML方式的AOP:老项目维护带来的真实教训

1.1 接手老项目:几百行beans.xml给我的冲击

我去看那个老系统的时候,光打开applicationContext.xml就有点懵。<bean>节点倒还好说,真正让人头皮发麻的是后半段那一大坨<aop:config>。一个服务模块配一个<aop:aspect>,里面挂了前置通知、环绕通知、异常通知,业务日志和权限切面还拆成了两个独立文件引入。

这种配置方式最大的问题是:切面的逻辑散落在AOP相关的事实,在代码里根本看不见。你翻开UserService的源码,方法干干净净,但实际上每次调用都在被外层代理拦截。如果你不知道beans.xml里配了哪些切点表达式,排查问题时压根不会往那个方向想。我当时就吃过这个亏,一个接口响应莫名多出几毫秒,查了半天数据库慢查询,最后才发现是一个环绕通知在每次方法执行前做了一次远程调用。

所以我的第一个建议是:接手任何老项目,第一件事先把所有XML配置里的<aop:config全部扫一遍,把所有切点表达式列成清单。这比先看业务代码重要得多。

1.2 AOP原理:动态代理是理解一切配置的钥匙

不管用XML还是注解,Spring AOP的底层原理都是同一个:动态代理。理解不了动态代理,学AOP配置就只是在背标签。

Spring AOP在运行时会为目标对象生成一个代理对象。调用方拿着的是代理对象,真正执行业务方法之前,代理对象会先按“通知链”的顺序把各个切面逻辑执行一遍,再决定是否调用真实目标方法。

有两种代理方式:

  • JDK动态代理:目标类实现了接口。Spring基于java.lang.reflect.Proxy生成一个实现同一接口的代理类,调用时通过InvocationHandler拦截。
  • CGLIB代理:目标类没有实现接口。Spring用CGLIB生成目标类的子类,通过继承来覆盖方法,从而植入切面逻辑。

Spring在这两者之间是自动选择的。默认规则是:目标类实现了任何一个接口,就用JDK动态代理;一个接口都没有,才退回到CGLIB。如果想让Spring无条件使用CGLIB,可以在<aop:config>上加proxy-target-class="true"

理解了这一点,后面好多坑就能看明白了。比如没有接口的类,Spring生成的代理对象和原类不是同一个对象,如果你在代码里用instanceof去判断或者强转成某个具体类,有时候会得到一些匪夷所思的结果,本质上就是代理对象和原始对象的类结构不一样。

1.3 横切关注点:AOP到底解决了什么问题

在没有AOP之前,解决日志、事务、权限这类“横切关注点”问题的标准姿势是在每个方法里重复写同样的代码。

比如你要给100个service方法打印操作日志,老老实实写的话,每个方法开头两三行日志代码,结尾又两三行,出了异常再写一段。项目刚做出来没问题,等需求改成“日志里必须带上操作人ID”的时候,你就得改100个地方的200多行代码,漏改一个就会出线上事故。

AOP的思路是把这些逻辑抽出来,放到一个独立的模块里,然后通过“切点”声明哪些方法要套上这些逻辑。改需求的时候,只改切面类,业务方法一行不动。

这样做的好处有三个:

  1. 业务代码更干净,关注点单一。
  2. 横切逻辑复用,同一套日志、事务逻辑可以挂到任何业务方法上。
  3. 逻辑统一收口,需求变更时只改一个地方。

AOP的典型使用场景我列一下:操作日志记录、事务管理、权限校验、接口限流、参数校验与脱敏、性能监控、缓存处理。我这些年在实际项目里基本就用这几个场景,老系统里那个操作日志切面,就是最典型的XML AOP应用。

2. XML里的AOP配置和AOP核心概念怎么对应

2.1 六大术语对照表:切面、切点、通知、连接点、目标、织入

很多初学者看AOP术语容易昏头,我建议直接把这几个概念和XML标签一一对应起来记。

概念含义XML中的对应
Aspect(切面)一组“横切逻辑+要织入的位置”的集合<aop:aspect ref="切面Bean">
Pointcut(切入点)一个表达式,定义“哪些方法会被拦截”<aop:pointcut expression="execution(...)">
Advice(通知)具体的横切逻辑,比如记录日志、开启事务<aop:before><aop:around>等子标签
JoinPoint(连接点)被拦截到的某一次具体方法调用运行时概念,在切面方法参数里体现
Target(目标对象)被切面横切的原业务对象<bean id="accountService">
Weaving(织入)把切面逻辑应用到目标方法并生成代理的过程<aop:config>整体做的事情

记忆方法很简单:切面是一个“盒子”,盒子里面装着“通知”,通知负责干活;“切点”决定了盒子上的哪几根“针”会扎到目标方法上。目标是针扎的对象,织入是扎针这个动作,连接点则是针尖落下去的那一个具体位置。

2.2 一个最小可用的aop:config骨架

先看一个最精简的XML AOP配置,后面所有内容都围绕这个骨架展开:

<?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"> <bean id="accountService" class="com.example.service.AccountService"/> <bean id="logAspect" class="com.example.aspect.LogAspect"/> <aop:config> <aop:aspect ref="logAspect"> <aop:pointcut id="servicePointcut" expression="execution(* com.example.service.*.*(..))"/> <aop:before method="logBefore" pointcut-ref="servicePointcut"/> </aop:aspect> </aop:config> </beans>

<aop:config>是根节点,里面直接子节点有三种:<aop:pointcut><aop:advisor><aop:aspect>。这三者在实际项目中的使用频率,<aop:aspect>用来定义基于普通POJO的切面,<aop:advisor>适配那些已经实现了Spring通知接口的类(常见于事务配置),而<aop:pointcut>既可以在<aop:aspect>内部定义,也可以定义在<aop:config>顶层,然后被多个aspect引用。

2.3 公共切点的两种定义方式与作用域

切点定义的位置不同,能被谁引用就不一样。

<aop:config>顶层定义的切点属于“全局切点”,能被当前<aop:config>下所有<aop:aspect>引用。如果切点表达式被多个切面复用,建议放顶层。

<aop:aspect>内部定义的切点,作用域只在当前切面内可见,其他切面引用不到。如果你两个切面对同一批方法织入不同逻辑,就得在每个aspect里单独定义一份切点,或者干脆在顶层统一定义。

我见过别人把这两个场景混在一起用,结果在某个aspect里引用了另一个aspect的私有点cutpoint,启动直接报错。所以记一条规则:切点定义在哪里,就在哪里作用域可见,跨aspect引用必须用顶层切点

另外提醒一句,<aop:config>可以有多个,一个配置文件里写多段<aop:config也是允许的,但同一段<aop:config下的aspect会按声明顺序参与代理构建。切面优先级这事儿放到后面第7节讲。

3. 从零搭一个基于XML的AOP案例

3.1 工程依赖怎么配:pom.xml里少了这个包一定会后悔

搭建一个最小的Spring XML AOP项目,Maven依赖只需要两个核心包:

<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.30</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.19</version> </dependency> </dependencies>

spring-context会传递引入spring-core、spring-beans、spring-aop这些基础模块。而aspectjweaver是用来解析AspectJ切点表达式的,没有它,<aop:config>里的expression="execution(...)"在运行时就会因为找不到表达式解析器而异常。

另外提醒一下,如果你图省事直接加spring-boot-starter-aop,它会把aspectjweaver带进来,但那种方式默认走自动配置,跟今天我们纯XML的方式不完全是一回事。做纯XML项目,老老实实把这两个依赖写清楚就行。

3.2 写业务类和切面类

先创建业务类,这里用一个简单的账户服务。注意我故意没有让它实现接口,这样在Spring 5.x下默认走CGLIB代理,后续在测试里能看到更明显的行为:

package com.example.service; public class AccountService { public void transfer(String from, String to, Double amount) { System.out.println("执行转账:" + from + " -> " + to + ",金额:" + amount); } public Double queryBalance(String accountNo) { System.out.println("查询余额,账户:" + accountNo); return 8888.00; } public void freezeAccount(String accountNo) { System.out.println("冻结账户:" + accountNo); throw new RuntimeException("账户余额不足,冻结失败"); } }

再写切面类。这个类本身就是一个普通POJO,里面每个方法对应一种通知逻辑,方法签名有约定,具体约定在XML配置里通过method属性指定:

package com.example.aspect; import org.aspectj.lang.ProceedingJoinPoint; public class LogAspect { public void beforeLog() { System.out.println("[前置通知] 开启事务,记录操作日志..."); } public void afterReturningLog(Object result) { System.out.println("[后置返回通知] 方法正常返回,结果:" + result); } public void afterThrowingLog(Throwable ex) { System.out.println("[异常通知] 方法抛出异常:" + ex.getMessage()); } public void afterLog() { System.out.println("[最终通知] 清理资源,关闭连接..."); } public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); System.out.println("[环绕通知] 方法开始:" + joinPoint.getSignature().getName()); Object result = joinPoint.proceed(); System.out.println("[环绕通知] 方法结束,耗时:" + (System.currentTimeMillis() - start) + "ms"); return result; } }

这里先说两个很容易出错的方法签名细节:

  • afterReturningLog(Object result)的参数名必须和XML里returning属性指定的一致,否则参数值注入不进去。
  • afterThrowingLog(Throwable ex)的参数名必须和XML里throwing属性的一致,否则异常信息拿不到。
  • aroundLog的参数必须是ProceedingJoinPoint,并且方法要声明throws Throwable,因为在环绕通知里joinPoint.proceed()执行目标方法时,什么异常都可能抛出来,不能随意吞掉。

3.3 关键来了:beans.xml里的AOP配置

把业务类和切面类都定义成Bean,然后用<aop:config>把它们“锚”在一起:

<?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"> <bean id="accountService" class="com.example.service.AccountService"/> <bean id="logAspect" class="com.example.aspect.LogAspect"/> <aop:config> <aop:pointcut id="servicePointcut" expression="execution(* com.example.service.*.*(..))"/> <aop:aspect ref="logAspect"> <aop:before method="beforeLog" pointcut-ref="servicePointcut"/> <aop:after-returning method="afterReturningLog" pointcut-ref="servicePointcut" returning="result"/> <aop:after-throwing method="afterThrowingLog" pointcut-ref="servicePointcut" throwing="ex"/> <aop:after method="afterLog" pointcut-ref="servicePointcut"/> <aop:around method="aroundLog" pointcut-ref="servicePointcut"/> </aop:aspect> </aop:config> </beans>

注意<aop:aspect ref="logAspect">里面的ref引用的,就是上面定义的那个切面Bean。method属性指向切面类里的方法名。pointcut-ref引用的是切点id。

3.4 运行结果分析:通知是怎么串起来的

写个主方法跑一下正常路径:

package com.example; import com.example.service.AccountService; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class MainTest { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); AccountService service = context.getBean("accountService", AccountService.class); service.transfer("张三", "李四", 1000.00); System.out.println("---- 分割线 ----"); try { service.freezeAccount("622200001"); } catch (RuntimeException e) { System.out.println("主程序捕获异常:" + e.getMessage()); } } }

正常路径transfer()的输出如下:

[环绕通知] 方法开始:transfer [前置通知] 开启事务,记录操作日志... 执行转账:张三 -> 李四,金额:1000.0 [后置返回通知] 方法正常返回,结果:null [最终通知] 清理资源,关闭连接... [环绕通知] 方法结束,耗时:5ms

异常路径freezeAccount()的输出如下:

[环绕通知] 方法开始:freezeAccount [前置通知] 开启事务,记录操作日志... 冻结账户:622200001 [异常通知] 方法抛出异常:账户余额不足,冻结失败 [最终通知] 清理资源,关闭连接... 主程序捕获异常:账户余额不足,冻结失败

从这个输出能看到三层结构:环绕通知在最外层,它包住了前置通知和后置通知;前置通知在目标方法执行之前触发;后置返回通知在正常返回后触发;异常通知只会在方法抛异常时触发;最终通知无论正常还是异常都会执行,等价于finally块。

4. 五种通知类型的XML写法、执行顺序与镜像理解

4.1 五种通知XML配置与语义对照

通知类型XML标签语义典型用途
前置通知<aop:before>目标方法执行前触发权限校验、参数校验、开启事务
后置返回通知<aop:after-returning>目标方法正常返回后触发记录成功日志、组装返回值
异常通知<aop:after-throwing>目标方法抛出异常后触发异常告警、事务回滚
最终通知<aop:after>目标方法结束后触发,无论是否抛异常资源释放、连接关闭
环绕通知<aop:around>完全包裹目标方法,可自定义前后逻辑性能监控、分布式锁、事务模板

<aop:after-returning><aop:after>很多人分不清。记住一个关键区别:after-returning只在方法正常返回时执行,方法抛异常它就不管了;after等同于finally,不管成功失败都会执行。这也是“正常返回”和“结束”两种语义的差别。

<aop:after-throwing>里还有一个坑:它的throwing="ex"必须和方法参数名一致,字符串对不上,Spring启动时就报参数绑定错误。这个错误属于特别好排查的那种,因为它会在启动阶段直接暴露。

4.2 正常路径和异常路径的执行顺序实测

还是以上面的输出为例,我把执行顺序画成文本流程图:

正常路径的执行顺序:

环绕通知前段 -> 前置通知 -> 目标方法 -> 后置返回通知 -> 最终通知 环绕通知后段

异常路径的执行顺序:

环绕通知前段 -> 前置通知 -> 目标方法(抛出异常) -> 异常通知 -> 最终通知 (环绕通知后段不执行,异常继续往上抛)

需要特别说明的是,我在Spring 5.3.x上实测得出的顺序是这样。同一个切面里挂了多个不同类型的通知时,通知之间的精确顺序在不同版本下可能有细微差别,因为这严格来说并没有在规范里被完全锁定。所以开发时尽量不要依赖“多个不同类型通知之间的相对顺序”,而是把核心逻辑写进环绕通知里,用try-catch-finally自己控制。

4.3 around通知为什么能包住其他通知:从调用链看执行

环绕通知和其他通知有一个本质区别:其他通知是在代理链的某一环上执行一次,而环绕通知拥有ProceedingJoinPoint,它可以决定“目标方法到底什么时候执行”,甚至可以决定“目标方法还执不执行”。

从调用链的角度看,Spring AOP的代理对象内部维护了一条通知链。环绕通知在这条链上处于最外层anchor的位置,它调用joinPoint.proceed()时,代理链才继续往下走,依次触发前置通知、目标方法、后置通知。如果环绕通知根本不调用proceed(),那目标方法被“短路”了,下面的前置通知也不会执行。

这个特征非常有用。比如做接口限流,你可以先判断令牌桶里有没有令牌,没令牌直接返回一个包装好的“请求过于频繁”结果,根本不调用proceed()。做权限校验也一样,没有权限直接抛出异常或返回默认值,目标方法就不用执行了。这也是为什么很多基于AOP的通用组件,核心逻辑都写在环绕通知里。

5. 切点表达式execution:从语法到实战的完整拆解

5.1 execution表达式的基本结构

execution是Spring AOP用得最多、也最可靠的切点表达式,它按“看得见的签名”来匹配方法。基本结构是:

execution(访问修饰符 返回类型 包名.类名.方法名(参数列表) 异常类型)

实际写的时候,修饰符和异常类型可以省略。比如我在例子里写的:

execution(* com.example.service.*.*(..))

拆开就是:

  • *:返回类型是任意的。
  • com.example.service.*:只匹配com.example.service这个包下的所有类,不包含子包。
  • .*:匹配类中的任意方法名。
  • (..):匹配任意参数列表。

如果想匹配子包也能匹配上,包的部分要写com.example.service..*,两个点表示包含子包。

5.2 高频写法与语义对照

切点表达式匹配范围
execution(public * com.example.service.*.*(..))service包下所有public方法
execution(* com.example.service.UserService.*(..))指定类中的所有方法
execution(* com.example..*.*(..))com.example及其所有子包中的所有方法
execution(* com.example.service.*.get*(..))service包下所有以get开头的方法
execution(* *(..))所有类的所有方法(慎用)
execution(* com.example.service.OrderService.save*(Long, ..))save开头,第一个参数是Long,后面参数任意

通配符的记忆口诀:*只能匹配一层,..能匹配多层。用在包路径里,..表示子包;用在参数列表里,..表示任意类型、任意个数的参数;单独一个*在参数列表里则表示恰好一个任意类型参数。

5.3 切点匹配失败的典型场景

切点表达式写错,最麻烦的一点是:Spring不会启动报错,只会静默不匹配。你配置了半天,写了个日志切面,运行起来发现日志一条都没打,第一反应往往是代码配错了,而不是表达式写错了。我列几个高频翻车场景:

  1. 包名多写了子包。com.example.service.*匹配不到com.example.service.sub.xxx里的类,因为单层通配符不包含子包。如果业务类实际在子包里,必须改成com.example.service..*
  2. 类名多打了个Impl。老项目里ServiceImpl都是com.xxx.service.impl.UserServiceImpl这种路径,但表达式写成了com.xxx.service.*.*(..)就匹配不上。
  3. 返回类型写成了具体类型。接口方法返回List<User>,你在表达式里写execution(* com.xxx.service.UserService.list(..))没问题,但如果你试图匹配具体泛型,比如List<com.xxx.User>,泛型在运行时会被擦除,基本都会翻车。
  4. (..)(*)混用。(..)表示任意参数,(*)表示恰好一个参数。有人想表达“任意参数”,写成了明文的方法名加(),结果只匹配无参方法,业务方法全被跳过了。

我的排查习惯是,切点不生效时先把表达式简化到最小范围,比如execution(* com.example.service.AccountService.transfer(..)),确定能匹配上之后,再逐步放宽条件。这样能很快定位是表达式的问题还是别的问题。

6. XML AOP踩坑实录:从启动报错到静默失效的排查链路

6.1 启动就报错:aop命名空间与依赖缺失

第一类问题是Spring容器启动阶段直接抛异常,这种其实还算友好,因为它至少给了明确的错误信息。最常见的两种:

  • The matching wildcard is strict, but no declaration can be found...或者org.xml.sax.SAXParseException。十有八九是XML里用了xmlns:aopaop:前缀,但schemaLocation里没有写对应的spring-aop.xsd。解决办法就是把第一节里那段xmlns:aopschemaLocation原样抄进去。
  • java.lang.NoClassDefFoundError: org/aspectj/util/PartialOrder$PartialOrderWrapper。这就是前面说的aspectjweaver依赖缺失。Spring容器里加载<aop:config>时解析切点表达式需要AspectJ的类,没有它必然挂。

遇到这类问题我的处理顺序是:先检查XML头部的命名空间声明,再检查schemaLocation里的URL路径是否和spring-aop.xsd一致,最后看pom.xml里aspectjweaver有没有进来。三步走完基本能解决。

6.2 最隐蔽的坑:切点静默失效

第二类问题最坑人:启动全程无异常,但通知就是不执行。遇到这种问题,我第一步做的是先确认Bean是不是被代理了。

在测试代码里打印service.getClass(),如果是com.example.service.AccountService,说明Bean就是原始对象,代理根本没生成;如果是AccountService$$EnhancerBySpringCGLIB,说明代理生成了,那就是切点表达式没匹配上。

这个区分非常关键。它会直接把问题一半对一半分到两条排查路线上:

  • Bean是原始对象:说明<aop:config>没有生效,去检查<aop:aspect ref="logAspect">的ref是不是写错了Bean名,或者这个Bean根本没有被Spring扫描到。
  • Bean是代理对象但通知没执行:直接怀疑切点表达式,用上一节说的缩小范围法去试。

另外还有一种隐蔽情况:<aop:config>里定义了切点,但<aop:aspect>里的通知标签写的是pointcut而不是pointcut-ref。如果用的是后者,切点必须定义在当前aspect内部;如果用了pointcut-ref引用了一个不存在的id,启动时会报错,这个倒不会静默。

6.3 自调用问题:同一个类里的方法调用为什么绕过了代理

这是我见过最经典的一个AOP失效场景。

public class AccountService { public void transfer(String from, String to, Double amount) { System.out.println("执行转账..."); this.updateBalance("张三", 1000.00); } public void updateBalance(String accountNo, Double amount) { System.out.println("更新余额..."); } }

如果切点表达式是execution(* com.example.service.AccountService.*(..)),理论上两个方法应该都会被拦截。但实测你会发现,transfer被拦截了,里面调的updateBalance没有被拦截。因为在transfer方法内部,this指向的是当前执行的对象,也就是目标对象本身,而不是Spring创建的那个代理对象。代理对象虽然包裹着目标对象,但目标对象内部调用自己的另一个方法时,并没有走代理的入口。

解决自调用问题的常规姿势有三个:

  1. 拆分两个类,把互相调用的方法放到不同Bean里,这样调用别的Bean时会经过代理。
  2. 通过AopContext.currentProxy()拿到当前代理对象,然后调用代理对象的方法,但<aop:config>上需要设置expose-proxy="true",得当心线程无关性问题。
  3. 把目标方法都放到事务、日志之外的地方,业务层调用时自己注意方法是入口方法还是内部方法。

我在维护老项目时遇到过这个问题,当时的代价就是一个库存扣减操作里,“扣减”方法没触发日志切面,导致线上数据异常时连个操作记录都没有。所以如果你在设计接口时知道某个类的某些方法会被切面拦截,内部互相调用要格外小心。

6.4 顺手聊一下XML文件怎么打开和编辑

既然这篇文章主题是XML,顺带说一个新手经常会问的细节:XML文件到底怎么打开和编辑。

不要用记事本打开XML然后硬改,缩进一乱就看不清结构了,而且编码极容易出问题。Windows上我建议直接用VS Code或者IDEA,IDEA对Spring XML有专门的语法提示和命名空间校验。打开一个beans.xml的时候,IDEA会帮你识别xmlns:aop并提示是否有未声明的标签。没有IDE环境的时候,也可以用Chrome直接拖动XML文件进去预览,或者用在线XML格式化工具快速整理结构。但涉及Spring配置修改,永远记住一条:在IDE里改完再用mvn test或启动容器验证,别改完就上生产

7. XML AOP和注解AOP怎么选:我的取舍建议

7.1 两种方式横向对比

我自己的态度是:新项目能上注解就上注解,老项目里的XML配置除非要动那一块逻辑,否则尽量别去大改。这两种方式各有清晰的适用场景:

维度XML AOP注解AOP
配置集中度所有切面集中在一个或几个XML文件里切面散布在各自的Java类上
可读性切面关系和全局扫描清晰,但业务代码看不出切面痕迹直接看注解就知道哪个方法被切了
开发效率低,每次新增切面要改XML高,一个@Aspect类搞定
类型安全弱,方法名用字符串,写错运行时才知道相对强,IDE能提示方法签名
适合场景老项目维护、切面需要动态调整、第三方类无法加注解新项目、团队成员对注解熟悉
切点复用顶层pointcut可被多个aspect引用通过@Pointcut方法复用

注解AOP在代码里能直接看到@Before@Around,查问题时顺着注解就能找到切面类。XML AOP的优点则在于所有拦截规则集中在一个配置文件里,对运维和架构审计更友好,生产环境排查时可以只开一个XML文件看全貌,不用翻遍整个项目找注解。这两种思路没有绝对的对错,纯粹是取舍。

7.2 advisor与aspect的区别:事务配置里的老朋友

讲到XML AOP,还有一个概念必须提,那就是<aop:advisor>

advisor在Spring里是一个更古老的抽象:它等于“一条通知+一个切点”的最小组合。在XML配置里,<aop:advisor>通常和<tx:advice>搭配使用,实现声明式事务:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="save*" propagation="REQUIRED"/> <tx:method name="update*" propagation="REQUIRED"/> <tx:method name="find*" read-only="true"/> </tx:attributes> </tx:advice> <aop:config> <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.example.service.*.*(..))"/> </aop:config>

advisoraspect最核心的区别是:advisor只能有一个通知和一个切点,而aspect可以挂多个通知。事务这种只需要一套切面逻辑的场景,用advisor正合适。你要是想在一个aspect里既做日志又做权限还做事务,那就用aspect

7.3 一个过来人的选择思路

最后给个实际参考。

第一,简历上写“熟悉Spring AOP”,不能光会@Around一种写法。你至少要能说出XML AOP里的<aop:config>aop:aspect、五种通知标签的语义区别,讲清楚advisoraspect的差别,最好能现场写一个简单的execution切点表达式。

第二,接手老项目,优先把XML AOP里的切点表达式全部摘出来形成文档,标注每个切面对应哪几个业务方法。这些切点表达式就是老系统的“隐形地图”,不画出来,后面每个改动都像在盲人摸象。

第三,新项目如果不涉及特殊要求,直接注解AOP,开发效率高很多。但如果是对外发布的中间件、基础组件,XML方式反而更容易和不同业务方对接。

在写这一篇的过程里我又翻了翻spring-aop.xsd的标签定义,愈发觉得XMI配置虽然啰嗦,但对理解AOP的完整链路特别有帮助:你被迫把每一个概念都想清楚,才能在XML里准确写出对应的标签。把这一块啃下来之后,再回头用注解AOP会顺手得多。老项目的经验不白踩,这些坑填平之后,后面的Spring学习之路反而更顺了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 19:50:21

SSM校园人才市场系统开发与优化实战

1. 项目概述&#xff1a;SSM校园人才市场系统全栈开发实战这套SSM校园人才市场系统是一个典型的Java Web全栈项目&#xff0c;包含完整的程序源码、数据库设计、调试部署文档以及1万字以上的配套论文。作为面向高校就业场景的解决方案&#xff0c;系统采用SSM&#xff08;Sprin…

作者头像 李华
网站建设 2026/9/11 19:49:52

YOLO11n实战指南:轻量目标检测模型部署全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 19:49:52

Android相机拍完照跳转进大图链路拆解

Android相机拍完照跳转进大图链路拆解 实际是一条跨进程、跨系统服务、跨渲染链路: 相机点击抬手 → 相机处理缩略图点击 → 确认刚拍照片/Uri/Intent → startActivity → system_server ActivityTaskManager/WindowManager 启动图库 → 图库进程冷启动 → 图库 Application…

作者头像 李华