🔍 总体概览
Spring 提供了两种方式定义 AOP 切面:
- @AspectJ 注解风格 :使用 Java 类 + 注解(如
@Aspect,@Before,@After等) - Schema-based(基于 XML 的 aop 命名空间) :通过
<aop:xxx>标签在 XML 中配置切面逻辑
本文讲的是第二种------用 XML 写 AOP。
虽然现在主流开发更倾向于注解+Java配置,但了解 XML 方式有助于:
- 维护老项目
- 深入理解 Spring AOP 底层机制
- 在某些场景下更灵活控制(比如动态修改配置文件)
🧱 5.5 Schema-based AOP 支持
✅ 前提条件
要使用 <aop:config> 这些标签,必须先引入 Spring AOP 的命名空间(schema)。
xml
<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 https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
然后才能使用 <aop:config>、<aop:aspect> 等标签。
🏗️ 5.5.1 声明一个 Aspect(切面)
在注解风格里,我们写一个类加上 @Aspect 就是一个切面;
而在 XML 风格中,普通 Java Bean + XML 配置 = 切面
示例代码
xml
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="com.example.MyAspectBean"/>
ref="aBean"表示这个切面的行为由MyAspectBean这个 bean 提供。- 所有通知方法(advice methods),比如
doAccessCheck(),都要在这个 bean 里实现。 - 这个 bean 可以像其他任何 Spring Bean 一样被依赖注入!
📌 重点:没有 @Aspect 注解!切面是由 XML 定义的。
🔮 5.5.2 声明 Pointcut(切入点)
Pointcut 是"哪些方法需要被拦截"的规则表达式,语法与 AspectJ 相同。
① 全局定义(可在多个 aspect 中复用)
xml
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
</aop:config>
id="businessService":给 pointcut 起名字,后面可以引用。expression:标准的 AspectJ 切入点表达式,表示匹配 service 包下的所有公共方法。
② 在某个 aspect 内部定义
xml
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="serviceMethod"
expression="execution(* com.xyz.service.*.*(..))"/>
</aop:aspect>
③ 使用已有的 @Aspect 类中的命名切入点
如果你已经有类似这样的类:
java
@Aspect
public class CommonPointcuts {
@Pointcut("execution(* com.xyz.service.*.*(..))")
public void businessService() {}
}
那么可以在 XML 中引用它:
xml
<aop:pointcut id="myPC"
expression="com.xyz.CommonPointcuts.businessService()"/>
⚠️ 注意限制
XML 定义的 pointcut 不能作为命名切入点用于组合新 pointcut(不像 @AspectJ 那样支持嵌套组合)。所以它的复用能力较弱。
💡 绑定 JoinPoint 上下文(如 this, target, args)
你可以从连接点提取参数传入 advice 方法:
xml
<aop:pointcut id="serviceWithThis"
expression="execution(* com.xyz.service.*.*(..)) and this(service)"/>
<aop:before method="monitor" pointcut-ref="serviceWithThis"/>
对应的方法:
java
public void monitor(Object service) { ... }
👉 this(service) 把代理对象绑定到变量 service,并自动传递给 advice 方法。
✅ XML 特殊字符处理
因为 && 在 XML 中会报错,可以用单词代替:
| 原符号 | XML 替代 |
|---|---|
&& |
and |
| ` | |
! |
not |
推荐写法:
xml
expression="execution(* com.xyz.service.*.*(..)) and this(service)"
📢 5.5.3 声明 Advice(通知)
共有五种通知类型,与 @AspectJ 完全一致:
1. 前置通知(Before Advice)
xml
<aop:before
pointcut-ref="businessService"
method="doAccessCheck"/>
- 在目标方法执行前运行。
method="doAccessCheck"指向 aspect bean 中的方法。
也可以内联 pointcut:
xml
<aop:before
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
2. 后置返回通知(After Returning)
正常返回后执行:
xml
<aop:after-returning
pointcut-ref="dataAccessOperation"
returning="retVal"
method="doLogResult"/>
对应方法需接收 retVal 参数:
java
public void doLogResult(Object retVal) { ... }
⚠️ 如果指定了 returning,只有当返回值类型匹配时才会触发。
3. 异常通知(After Throwing)
抛出异常后执行:
xml
<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="ex"
method="doRecoveryActions"/>
方法签名:
java
public void doRecoveryActions(DataAccessException ex) { ... }
只对特定异常类型生效。
4. 最终通知(After Finally)
无论成功或失败都执行(类似 try-finally):
xml
<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>
常用于释放资源、解锁等操作。
5. 环绕通知(Around Advice)
最强大的通知类型,可控制是否继续执行原方法:
xml
<aop:around
pointcut-ref="businessService"
method="doBasicProfiling"/>
实现方法:
java
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("开始计时");
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 执行原方法
long elapsed = System.currentTimeMillis() - start;
System.out.println("耗时:" + elapsed + "ms");
return result;
}
✅ 第一个参数必须是 ProceedingJoinPoint。
🧩 5.5.4 Introductions(引介 / 引入新接口)
类似于"让已有类额外实现某个接口"。
例如:让所有 Service 类都实现 UsageTracked 接口,记录调用次数。
XML 配置
xml
<aop:aspect id="usageTracker" ref="usageTracking">
<aop:declare-parents
types-matching="com.xyz.myapp.service.*+"
implement-interface="com.xyz.service.tracking.UsageTracked"
default-impl="com.xyz.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="execution(* com.xyz.myapp.service.*.*(..)) and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
types-matching:哪些类要被增强?implement-interface:新增实现哪个接口?default-impl:该接口的默认实现类。
之后就可以把任意 service 强转为 UsageTracked:
java
UsageTracked tracked = (UsageTracked) context.getBean("userService");
tracked.getUseCount();
🪄 5.5.5 实例化模型(Instantiation Models)
目前只支持 singleton(单例)模式。
也就是说每个 <aop:aspect ref="..."> 对应的 bean 必须是单例的,不支持 per-this 或 per-target 等高级实例化模型。
📝 5.5.6 Advisors(顾问)
Advisor 是 Spring 特有的轻量级"切面",通常只包含一个 advice 和一个 pointcut。
适合简单场景,比如事务管理。
示例:结合事务管理器使用
xml
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="txAdvice"/>
</aop:config>
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
advice-ref指向一个实现了 Spring Advice 接口的 bean(如 MethodInterceptor)。- 常见用途:事务、缓存、安全等通用横切逻辑。
🛠️ 5.5.7 完整示例:重试机制(并发锁失败自动重试)
场景需求
某些业务方法可能因数据库死锁失败(PessimisticLockingFailureException),但我们希望自动重试几次再抛异常。
这属于典型的横切关注点 → 适合用 AOP 实现。
Step 1:编写切面类(无注解)
java
public class ConcurrentOperationExecutor implements Ordered {
private int maxRetries = 2;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int attempts = 0;
PessimisticLockingFailureException lastException;
do {
attempts++;
try {
return pjp.proceed(); // 尝试执行
} catch (PessimisticLockingFailureException e) {
lastException = e;
}
} while (attempts <= maxRetries);
throw lastException; // 重试完仍失败则抛出
}
}
Step 2:XML 配置
xml
<aop:config>
<aop:aspect id="concurrentRetry" ref="concurrentOperationExecutor">
<aop:pointcut id="idempotentOp"
expression="execution(* com.xyz.service.*.*(..))"/>
<aop:around pointcut-ref="idempotentOp"
method="doConcurrentOperation"/>
</aop:aspect>
</aop:config>
<bean id="concurrentOperationExecutor"
class="com.xyz.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
✅ 效果
所有 service 方法如果抛出 PessimisticLockingFailureException,会最多重试 3 次。
🔐 更精细控制:仅对幂等方法重试
定义一个注解:
java
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {}
标记幂等方法:
java
@Service
public class OrderService {
@Idempotent
public void cancelOrder(String id) { ... }
}
修改 pointcut:
xml
<aop:pointcut id="idempotentOp"
expression="execution(* com.xyz.service.*.*(..))
and @annotation(com.xyz.Idempotent)"/>
→ 只对加了 @Idempotent 的方法进行重试。
🧠 总结对比:Schema vs @AspectJ
| 功能 | Schema-based (XML) | @AspectJ 注解 |
|---|---|---|
| 定义位置 | XML 文件 | Java 类 |
是否需要 @Aspect |
❌ 不需要 | ✅ 需要 |
| Pointcut 复用性 | 较差(无法嵌套组合) | 强(可用方法调用) |
| 类型安全性 | 弱(字符串表达式) | 强(编译期检查) |
| 参数绑定 | 支持(按名称匹配) | 支持 |
| 调试难度 | 高(分散在 XML 和 Java 中) | 低(集中在一个类) |
| 适用场景 | 老项目、事务配置、集中式策略 | 新项目、复杂切面逻辑 |
✅ 学习建议
- 新手入门 :优先学习
@AspectJ风格(更直观)。 - 维护旧系统:必须掌握 schema-based 配置。
- 最佳实践 :避免混合使用
<aop:config>和BeanNameAutoProxyCreator,否则可能导致代理失效。 - 性能提示:大量使用 AOP 会影响启动速度和内存占用,合理设计切面粒度。
📚 结语
你看到的这部分文档非常全面,涵盖了 Spring AOP 的 XML 配置方式的所有关键特性。虽然现代开发中较少手写这些 XML,但它背后的原理(pointcut 表达式、advice 执行顺序、around 控制流程、introduction 扩展功能等)在各种框架(如 Spring Security、Spring Retry、Spring Cache)中都有广泛应用。
理解这一章,等于掌握了 Spring AOP 的底层运作机制。
如果你想动手练习,我可以帮你生成完整的 Maven 项目结构 + 示例代码 👇
是否需要?