基于 Schema 的 AOP 支持 (Schema-based AOP Support)
如果你更喜欢基于 XML 的格式,Spring 还提供了使用 aop 命名空间标签定义切面的支持。它支持与 @AspectJ 风格完全相同的切入点表达式和通知类型。因此,在本节中,我们重点介绍 XML 语法,并建议读者参考上一节(@AspectJ 支持)以了解如何编写切入点表达式以及通知参数的绑定。
要使用本节中描述的 aop 命名空间标签,你需要按照基于 XML Schema 的配置中所述导入 spring-aop 架构。请参阅 AOP 架构以了解如何导入 aop 命名空间的标签。
在 Spring 配置中,所有切面和 Advisor 元素必须放置在 <aop:config> 元素内(一个应用上下文配置中可以有多个 <aop:config> 元素)。<aop:config> 元素可以包含切入点(pointcut)、Advisor 和切面(aspect)元素(请注意,它们必须按此顺序声明)。
⚠️ 警告
<aop:config> 风格的配置大量使用了 Spring 的自动代理机制。如果你已经通过 BeanNameAutoProxyCreator 或类似方式使用了显式自动代理,可能会导致问题(例如通知未被织入)。推荐的使用模式是要么只使用 <aop:config> 风格,要么只使用 AutoProxyCreator 风格,切勿混用。
声明切面 (Declaring an Aspect)
当你使用 Schema 支持时,切面是一个在 Spring 应用上下文中定义为 Bean 的普通 Java 对象。状态和行为在对象的字段和方法中捕获,而切入点和通知信息则在 XML 中捕获。
你可以使用 <aop:aspect> 元素来声明切面,并通过 ref 属性引用后台 Bean,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>作为切面后台的 Bean(在本例中为 aBean)可以像任何其他 Spring Bean 一样进行配置和依赖注入。
声明切入点 (Declaring a Pointcut)
你可以在 <aop:config> 元素内声明一个命名的切入点,让该切入点定义在多个切面和 Advisor 之间共享。
代表 Service 层中任何业务执行的切入点可以定义如下:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))" />
</aop:config>请注意,切入点表达式本身使用与 @AspectJ 支持中所述相同的 AspectJ 切入点表达式语言。如果你使用基于 Schema 的声明风格,还可以在切入点表达式中引用在 @Aspect 类型中定义的命名切入点。因此,定义上述切入点的另一种方法如下:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.CommonPointcuts.businessService()" />
</aop:config>在切面内部声明切入点与声明顶层切入点非常相似,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>与 @AspectJ 切面非常类似,使用基于 Schema 定义风格声明的切入点也可以收集连接点上下文。例如,以下切入点将 this 对象收集为连接点上下文,并将其传递给通知:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..)) and this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>通知必须通过包含匹配名称的参数来声明接收收集到的连接点上下文,如下所示:
public void monitor(Object service) {
// ...
}fun monitor(service: Any) {
// ...
}在 XML 文档中组合切入点子表达式时,使用 && 会比较麻烦(需要转义为 &&),因此你可以分别使用 and、or 和 not 关键字来代替 &&、|| 和 !。例如,之前的切入点可以写成这样:
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..)) and this(service)"/>请注意,以此方式定义的切入点通过其 XML id 进行引用,不能作为命名切入点来形成复合切入点。因此,基于 Schema 的定义风格中的命名切入点支持比 @AspectJ 风格所提供的更受限制。
声明通知 (Declaring Advice)
基于 Schema 的 AOP 支持使用与 @AspectJ 风格相同的五种通知,且它们具有完全相同的语义。
前置通知 (Before Advice)
前置通知在匹配的方法执行之前运行。它使用 <aop:before> 元素在 <aop:aspect> 内部声明:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>在此示例中,dataAccessOperation 是在顶层(<aop:config>)定义的命名切入点的 id。如果要内联定义切入点,请改用 pointcut 属性。
后置返回通知 (After Returning Advice)
后置返回通知在匹配的方法执行正常完成时运行。
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>与 @AspectJ 风格一样,你可以获取返回值。使用 returning 属性指定要传递给通知方法的参数名。
异常通知 (After Throwing Advice)
异常通知在匹配的方法执行抛出异常退出时运行。
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>使用 throwing 属性指定要传递给通知方法的异常参数名。
后置最终通知 (After (Finally) Advice)
后置(最终)通知无论匹配的方法执行如何退出都会运行。使用 <aop:after> 元素声明。
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doReleaseLock"/>
</aop:aspect>环绕通知 (Around Advice)
环绕通知运行于匹配方法执行的“周围”。它有机会在方法运行前后执行工作,并决定方法何时、如何甚至是否真正运行。
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut="execution(* com.xyz.service.*.*(..))"
method="doBasicProfiling"/>
</aop:aspect>通知方法的实现(除了没有注解之外)可以与 @AspectJ 示例完全相同:
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// 开始计时
Object retVal = pjp.proceed();
// 结束计时
return retVal;
}fun doBasicProfiling(pjp: ProceedingJoinPoint): Any? {
// 开始计时
val retVal = pjp.proceed()
// 结束计时
return retVal
}通知参数 (Advice Parameters)
基于 Schema 的声明风格以相同的方式支持强类型通知——通过名称匹配将切入点参数与通知方法参数对应。如果你希望显式指定参数名,可以使用通知元素的 arg-names 属性。
<aop:before
pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)"
method="audit"
arg-names="auditable" />arg-names 属性接受以逗号分隔的参数名称列表。
通知顺序 (Advice Ordering)
当多个通知需要在同个连接点运行时,排序规则见通知顺序。切面之间的优先级通过 <aop:aspect> 元素中的 order 属性确定,或者让后台 Bean 实现 Ordered 接口/标注 @Order 注解。
TIP
与同个 @Aspect 类定义通知的规则不同,在同个 <aop:aspect> 元素中定义的通知,其优先级由它们在 XML 中的声明顺序决定(从高到低)。
例如,如果要确保环绕通知比前置通知优先级高,<aop:around> 必须声明在 <aop:before> 之前。
引入 (Introductions)
你可以通过在 aop:aspect 内部使用 aop:declare-parents 元素来实现引入。
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xyz.service.*+"
implement-interface="com.xyz.service.tracking.UsageTracked"
default-impl="com.xyz.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="execution(* com.xyz..service.*.*(..)) and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>usageTracking Bean 对应的类将包含 recordUsage 方法。
切面实例化模型 (Aspect Instantiation Models)
Schema 定义的切面仅支持单例 (Singleton) 实例化模型。
Advisor
“Advisor” 的概念源自 Spring 定义的 AOP 支持,在 AspectJ 中没有直接等价物。一个 Advisor 就像是一个小的、自包含的切面,它只有一个通知。通知本身由 Bean 表示,且必须实现通知类型中描述的某个通知接口。
Spring 通过 <aop:advisor> 元素支持 Advisor。你最常在与事务通知(它在 Spring 中也有自己的命名空间支持)结合使用时看到它。
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="tx-advice" />
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>AOP Schema 示例 (An AOP Schema Example)
本节展示了重试示例使用 Schema 支持重写后的样子。
Java 类实现(无注解版):
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}class ConcurrentOperationExecutor : Ordered {
private val DEFAULT_MAX_RETRIES = 2
private var maxRetries = DEFAULT_MAX_RETRIES
private var order = 1
fun setMaxRetries(maxRetries: Int) {
this.maxRetries = maxRetries
}
override fun getOrder(): Int {
return this.order
}
fun setOrder(order: Int) {
this.order = order
}
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any? {
var numAttempts = 0
var lockFailureException: PessimisticLockingFailureException
do {
numAttempts++
try {
return pjp.proceed()
} catch (ex: PessimisticLockingFailureException) {
lockFailureException = ex
}
} while (numAttempts <= this.maxRetries)
throw lockFailureException
}
}XML 配置:
<aop:config>
<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..)) and
@annotation(com.xyz.service.Idempotent)"/>
<aop:around
pointcut-ref="idempotentOperation"
method="doConcurrentOperation"/>
</aop:aspect>
</aop:config>
<bean id="concurrentOperationExecutor"
class="com.xyz.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>补充教学
1. 为什么还要学习 XML AOP?
尽管现代 Spring 开发强烈推荐使用注解,但 XML AOP 在以下场景仍不可替代:
- 非侵入性配置:有些类(如第三方库)你无法在源代码上加
@Aspect或@Before注解,XML 是唯一的途径。 - 集中化管理:所有的 AOP 逻辑都写在一个 XML 文件里,一目了然,不需要去翻成千上万个 Java 类寻找切面定义。
- 遗留系统维护:在很多大型金融、政府项目中,依然保留着结构清晰的 XML 配置文件。
2. Schema AOP 的“强制性”顺序
在一个 <aop:config> 标签内,子元素的顺序是严格受限的:
<aop:pointcut>(可选)<aop:advisor>(可选)<aop:aspect>(可选) Spring 会严格校验这个顺序,写反了会导致配置启动失败。
3. 理解 Advisor:AOP 的“轻量版”
- Aspect (切面):是全功能的,可以包含很多个通知。
- Advisor (顾问):是单一职责的,它通过
pointcut绑定一个advice(通常是实现 Spring AOP 原始接口的类)。 - 实战提示:在配置事务管理(Transaction Management)时,你用到的
<tx:advice>配合<aop:advisor>其实就是一种最典型的 Advisor 模式。
4. 逻辑运算符的 XML 友好化
在 Java 代码中,我们习惯写 &&。但在 XML 中,直接写 && 是非法的,必须写成 &&。 为了提升可读性,Schema AOP 允许你直接写英语单词:
and(等同于&&)or(等同于||)not(等同于!) 推荐在 XML 中优先使用语义化的单词。