|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
基于 Schema 的 AOP 支持
如果你更喜欢基于 XML 的格式,Spring 也提供了使用 aop 命名空间标签来定义切面的支持。它支持与使用 @AspectJ 风格时完全相同的切入点表达式和通知类型。因此,在本节中,我们将重点介绍这种语法,并请读者参考前一节(@AspectJ 支持)中关于如何编写切入点表达式以及通知参数绑定的讨论。
要使用本节中描述的 aop 命名空间标签,您需要导入 spring-aop 模式,具体方法请参见基于 XML Schema 的配置。有关如何在 xref page 命名空间中导入这些标签,请参见AOP 模式。
在您的 Spring 配置中,所有的切面(aspect)和通知器(advisor)元素都必须放置在 <aop:config> 元素内部(您可以在一个应用上下文配置中包含多个 <aop:config> 元素)。一个 <aop:config> 元素可以包含切入点(pointcut)、通知器(advisor)和切面(aspect)元素(请注意,这些元素必须按照此顺序声明)。
<aop:config> 配置方式大量使用了 Spring 的
自动代理(auto-proxying) 机制。如果你已经通过使用
BeanNameAutoProxyCreator 或类似方式显式地启用了自动代理,这可能会引发问题(例如通知未被织入)。建议的使用模式是:要么仅使用 <aop:config> 风格,要么仅使用 AutoProxyCreator 风格,切勿将两者混合使用。 |
声明一个切面
当你使用 schema 支持时,切面(aspect)就是一个普通的 Java 对象,并在 Spring 应用上下文中被定义为一个 bean。该对象的状态和行为由其字段和方法来体现,而切入点(pointcut)和通知(advice)信息则在 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 一样进行配置和依赖注入。
声明切入点
你可以在 <aop:config> 元素内部声明一个命名切入点,使该切入点定义能够在多个切面和通知器之间共享。
一个切入点(pointcut),用于表示服务层中任意业务服务的执行,可定义如下:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))" />
</aop:config>
请注意,切入点表达式本身使用的是与@AspectJ 支持中所述相同的 AspectJ 切入点表达式语言。如果使用基于 schema 的声明风格,您还可以在切入点表达式中引用在 @Aspect 类型中定义的命名切入点(named pointcuts)。因此,上述切入点的另一种定义方式如下所示:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.CommonPointcuts.businessService()" /> (1)
</aop:config>
| 1 | 引用在共享命名切入点定义中定义的名为ataspectj/pointcuts.html#aop-common-pointcuts的切入点。 |
在切面内部声明一个切入点与声明一个顶层切入点非常相似,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
与 @AspectJ 切面非常相似,使用基于 schema 的定义风格声明的切入点(pointcut)也可以收集连接点(join point)上下文。例如,以下切入点会将 this 对象作为连接点上下文进行收集,并将其传递给通知(advice):
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
通知必须声明为通过包含具有匹配名称的参数来接收所收集的连接点上下文,如下所示:
-
Java
-
Kotlin
public void monitor(Object service) {
// ...
}
fun monitor(service: Any) {
// ...
}
在组合切入点子表达式时,在 XML 文档中使用 && 会显得很笨拙,因此你可以分别用 and、or 和 not 关键字来代替 &&、|| 和 !。例如,前面的切入点可以更好地改写如下:
<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>
请注意,以这种方式定义的切入点通过其 XML id 进行引用,不能作为命名切入点来组合形成复合切入点。因此,基于 schema 的定义风格对命名切入点的支持比 @AspectJ 风格更为有限。
声明通知
基于 schema 的 AOP 支持使用与 @AspectJ 风格相同的五种通知类型,并且它们具有完全相同的语义。
前置通知
前置通知(Before advice)在匹配的方法执行之前运行。它通过使用 <aop:aspect> 元素在 <aop:before> 内部进行声明,如下例所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
在上面的示例中,dataAccessOperation 是在顶层(id 级别)定义的一个命名切入点(named pointcut)的 <aop:config>(参见声明切入点)。
| 正如我们在讨论 @AspectJ 风格时所指出的,使用命名切入点(named pointcuts)可以显著提高代码的可读性。详情请参见共享命名切入点定义。 |
若要改为内联定义切入点,请将 pointcut-ref 属性替换为 pointcut 属性,如下所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
method 属性标识了一个方法(doAccessCheck),该方法提供了通知(advice)的具体实现。此方法必须在包含该通知的切面(aspect)元素所引用的 bean 中定义。在执行数据访问操作之前(即在匹配切入点表达式的连接点处执行方法之前),会调用切面 bean 上的 doAccessCheck 方法。
返回后通知
后置返回通知(After returning advice)在匹配的方法正常执行完成后运行。它的声明方式与前置通知(before advice)相同,位于 <aop:aspect> 元素内部。以下示例展示了如何声明它:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
与 @AspectJ 风格一样,你可以在通知(advice)体中获取返回值。
为此,请使用 returning 属性来指定接收返回值的参数名称,如下例所示:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
doAccessCheck 方法必须声明一个名为 retVal 的参数。该参数的类型对匹配的限制方式与 @AfterReturning 中所述相同。例如,您可以将方法签名声明如下:
-
Java
-
Kotlin
public void doAccessCheck(Object retVal) {...
fun doAccessCheck(retVal: Any) {...
抛出异常后通知
异常抛出后通知(After throwing advice)在匹配的方法执行因抛出异常而退出时运行。它通过使用 <aop:aspect> 元素在 after-throwing 内部进行声明,如下例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doRecoveryActions"/>
...
</aop:aspect>
与 @AspectJ 风格一样,你可以在通知(advice)体中获取抛出的异常。
为此,请使用 throwing 属性来指定参数名称,
该参数将接收所抛出的异常,如下例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
doRecoveryActions 方法必须声明一个名为 dataAccessEx 的参数。
该参数的类型对匹配的限制方式与 @AfterThrowing 中所述相同。例如,方法签名可以如下声明:
-
Java
-
Kotlin
public void doRecoveryActions(DataAccessException dataAccessEx) {...
fun doRecoveryActions(dataAccessEx: DataAccessException) {...
后置(最终)通知
无论匹配的方法执行以何种方式退出,后置(最终)通知都会运行。
你可以使用 after 元素来声明它,如下例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doReleaseLock"/>
...
</aop:aspect>
环绕通知
最后一种通知是环绕通知。环绕通知在匹配方法的执行“周围”运行,它有机会在方法执行前后都进行操作,并且可以决定何时、如何,甚至是否真正执行该方法。 环绕通知通常用于需要以线程安全的方式在方法执行前后共享状态的场景——例如,启动和停止计时器。
|
始终使用满足您需求的最弱形式的通知(advice)。 例如,如果前置通知已能满足您的需求,请不要使用环绕通知。 |
您可以使用 aop:around 元素声明环绕通知。通知方法应将 Object 声明为其返回类型,且该方法的第一个参数必须为 ProceedingJoinPoint 类型。在通知方法体中,您必须在 ProceedingJoinPoint 上调用 proceed(),以便执行底层方法。在不带参数的情况下调用 proceed() 将导致在调用底层方法时传入调用者的原始参数。对于高级用例,proceed() 方法有一个重载变体,它接受一个参数数组(Object[])。数组中的值将在调用底层方法时用作其参数。有关使用 Object[] 调用 proceed 的说明,请参阅 环绕通知。
以下示例展示了如何在 XML 中声明环绕通知(around advice):
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut="execution(* com.xyz.service.*.*(..))"
method="doBasicProfiling"/>
...
</aop:aspect>
doBasicProfiling 通知的实现可以与 @AspectJ 示例中的完全相同(当然,去掉注解部分),如下例所示:
-
Java
-
Kotlin
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
fun doBasicProfiling(pjp: ProceedingJoinPoint): Any? {
// start stopwatch
val retVal = pjp.proceed()
// stop stopwatch
return pjp.proceed()
}
通知参数
基于 schema 的声明方式以与 @AspectJ 支持相同的方式支持完全类型化的通知(advice)——即通过将切入点(pointcut)参数的名称与通知方法参数的名称进行匹配。详情请参见通知参数。如果您希望显式指定通知方法的参数名称(而不是依赖前面描述的自动检测策略),可以通过使用通知元素的 arg-names 属性来实现,该属性的处理方式与通知注解中的 argNames 属性相同(如确定参数名称中所述)。
以下示例展示了如何在 XML 中指定参数名称:
<aop:before
pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)" (1)
method="audit"
arg-names="auditable" />
| 1 | 引用在组合切入点表达式中定义的名为ataspectj/pointcuts.html#aop-pointcuts-combining的切入点。 |
arg-names 属性接受一个以逗号分隔的参数名称列表。
以下是一个稍微复杂一点的基于 XSD 方法的示例,展示了环绕通知(around advice)与多个强类型参数结合使用的情况:
-
Java
-
Kotlin
package com.xyz.service;
public interface PersonService {
Person getPerson(String personName, int age);
}
public class DefaultPersonService implements PersonService {
public Person getPerson(String name, int age) {
return new Person(name, age);
}
}
package com.xyz.service
interface PersonService {
fun getPerson(personName: String, age: Int): Person
}
class DefaultPersonService : PersonService {
fun getPerson(name: String, age: Int): Person {
return Person(name, age)
}
}
接下来是切面(aspect)。请注意,profile(..) 方法接受多个强类型参数,其中第一个参数恰好是用于继续执行方法调用的连接点(join point)。该参数的存在表明 profile(..) 将被用作 around 通知(advice),如下例所示:
-
Java
-
Kotlin
package com.xyz;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class SimpleProfiler {
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}
package com.xyz
import org.aspectj.lang.ProceedingJoinPoint
import org.springframework.util.StopWatch
class SimpleProfiler {
fun profile(call: ProceedingJoinPoint, name: String, age: Int): Any? {
val clock = StopWatch("Profiling for '$name' and '$age'")
try {
clock.start(call.toShortString())
return call.proceed()
} finally {
clock.stop()
println(clock.prettyPrint())
}
}
}
最后,以下示例 XML 配置会在特定的连接点(join point)上执行前述通知(advice):
<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">
<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="personService" class="com.xyz.service.DefaultPersonService"/>
<!-- this is the actual advice itself -->
<bean id="profiler" class="com.xyz.SimpleProfiler"/>
<aop:config>
<aop:aspect ref="profiler">
<aop:pointcut id="theExecutionOfSomePersonServiceMethod"
expression="execution(* com.xyz.service.PersonService.getPerson(String,int))
and args(name, age)"/>
<aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
method="profile"/>
</aop:aspect>
</aop:config>
</beans>
考虑以下驱动脚本:
-
Java
-
Kotlin
public class Boot {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
PersonService person = ctx.getBean(PersonService.class);
person.getPerson("Pengo", 12);
}
}
fun main() {
val ctx = ClassPathXmlApplicationContext("beans.xml")
val person = ctx.getBean(PersonService.class)
person.getPerson("Pengo", 12)
}
使用这样一个 Boot 类,我们将在标准输出上获得类似如下的输出:
StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0 ----------------------------------------- ms % Task name ----------------------------------------- 00000 ? execution(getFoo)
通知排序
当多个通知(advice)需要在同一个连接点(即执行的方法)运行时,其排序规则如通知排序(Advice Ordering)中所述。切面(aspect)之间的优先级通过order元素中的<aop:aspect>属性来确定,也可以通过在支撑该切面的 Bean 上添加@Order注解,或者让该 Bean 实现Ordered接口来实现。
|
与在同一个 例如,对于在同一个 通常情况下,如果你发现同一个 |
简介
引入(在 AspectJ 中称为类型间声明)允许切面声明被通知的对象实现某个接口,并代表这些对象提供该接口的实现。
你可以通过在 aop:declare-parents 元素内部使用 aop:aspect 元素来引入新功能。
你可以使用 aop:declare-parents 元素声明匹配的类型拥有一个新的父类(因此得名)。
例如,给定一个名为 UsageTracked 的接口以及该接口的一个实现类
DefaultUsageTracked,以下切面声明所有服务接口的实现类同时也实现了 UsageTracked 接口。(例如,为了通过 JMX 暴露统计信息。)
<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 的类将包含以下方法:
-
Java
-
Kotlin
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
}
要实现的接口由 implement-interface 属性决定。types-matching 属性的值是一个 AspectJ 类型模式。任何匹配该类型的 bean 都会实现 UsageTracked 接口。请注意,在前面示例的前置通知(before advice)中,服务 bean 可以直接用作 UsageTracked 接口的实现。若要以编程方式访问 bean,可以编写如下代码:
-
Java
-
Kotlin
UsageTracked usageTracked = context.getBean("myService", UsageTracked.class);
val usageTracked = context.getBean("myService", UsageTracked.class)
顾问
“通知器”(advisors)的概念源自 Spring 中定义的 AOP 支持,在 AspectJ 中没有直接对应的等价物。通知器类似于一个小型的、自包含的切面,仅包含一条通知(advice)。该通知本身由一个 bean 表示,并且必须实现 Spring 中的通知类型 所描述的某个通知接口之一。通知器可以利用 AspectJ 的切入点表达式。
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>
除了前面示例中使用的 pointcut-ref 属性外,你还可以使用 pointcut 属性来内联定义一个切入点表达式。
要定义通知器(advisor)的优先级,以便其通知(advice)能够参与排序,请使用 order 属性来定义该通知器的 Ordered 值。
AOP 架构示例
本节展示了当使用模式支持重写时,来自一个AOP示例中的并发锁失败重试示例会是什么样子。
业务服务的执行有时会因并发问题(例如死锁导致的事务回滚)而失败。如果重试该操作,很可能在下一次尝试时成功。对于在这些情况下适合重试的业务服务(即幂等操作,且无需返回用户进行冲突解决),我们希望透明地重试该操作,以避免客户端看到 PessimisticLockingFailureException 异常。这一需求显然横切了服务层中的多个服务,因此非常适合通过切面(aspect)来实现。
由于我们希望重试该操作,因此需要使用环绕通知(around advice),以便可以多次调用 proceed 方法。以下代码清单展示了基本的切面实现(这是一个使用 schema 支持的普通 Java 类):
-
Java
-
Kotlin
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
}
}
请注意,该切面实现了 Ordered 接口,以便我们可以将该切面的优先级设置得高于事务通知(我们希望每次重试时都开启一个全新的事务)。maxRetries 和 order 属性均由 Spring 进行配置。主要逻辑发生在 doConcurrentOperation 环绕通知方法中。我们尝试继续执行;如果因 PessimisticLockingFailureException 而失败,则会再次尝试,除非我们已经用尽了所有重试次数。
| 此类与 @AspectJ 示例中使用的类完全相同,只是移除了注解。 |
对应的 Spring 配置如下:
<aop:config>
<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..))"/>
<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>
请注意,目前我们假设所有业务服务都是幂等的。如果实际情况并非如此,我们可以通过引入一个 Idempotent 注解,并使用该注解来标记服务操作的实现,从而细化切面,使其仅对真正幂等的操作进行重试,如下例所示:
-
Java
-
Kotlin
@Retention(RetentionPolicy.RUNTIME)
// marker annotation
public @interface Idempotent {
}
@Retention(AnnotationRetention.RUNTIME)
// marker annotation
annotation class Idempotent
将切面修改为仅重试幂等操作,需要细化切入点表达式,使其仅匹配 @Idempotent 注解的操作,如下所示:
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..)) and
@annotation(com.xyz.service.Idempotent)"/>