Schema-based AOP Support
如果您更喜欢基于 XML 的格式,Spring 还提供使用 aop
名称空间标签定义方面的支持。在使用 @AspectJ 样式时,支持完全相同的切入点表达式和建议类型。因此,在本节中,我们重点关注该语法,并将读者转到前一节(@AspectJ support)中的讨论,以了解书写切入点表达式和建议参数绑定。
要使用本部分中描述的 aop 命名空间标记,您需要导入 spring-aop
架构,如 XML Schema-based configuration 中所述。有关如何在 aop
命名空间中导入标记,请参阅 the AOP schema。
在 Spring 配置中,所有切面和通知元素都必须放在 <aop:config>
元素内(你可以在应用程序上下文配置中有多个 <aop:config>
元素)。一个 <aop:config>
元素可以包含切入点、通知和切面元素(请注意,它们必须按照此顺序声明)。
<aop:config>
样式的配置大量使用了 Spring 的 auto-proxying 机制。如果你已经通过使用 BeanNameAutoProxyCreator
这样的显式自动代理,这可能会导致问题(例如不织入建议)。建议的使用模式是仅使用 <aop:config>
样式或仅使用 AutoProxyCreator
样式,并且永远不要将它们混合在一起。
Declaring an Aspect
当您使用模式支持时,切面是在 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>
元素中声明 命名切入点,让切入点定义在多个方面和顾问间共享。
可以按照如下方式定义一个切点来表示服务层中任何业务服务的执行:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))" />
</aop:config>
请注意,切入点表达式本身使用与 @AspectJ support 中描述的相同 AspectJ 切入点表达式语言。如果您使用基于模式的声明样式,您还可以引用 @Aspect
类型中 named pointcuts 定义的在切入点表达式中。因此,另一种定义上述切入点的方法如下:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.CommonPointcuts.businessService()" /> 1
</aop:config>
1 | 引用 Sharing Named Pointcut Definitions 中定义的 businessService 命名切入点。 |
如以下示例所示,在切面内声明一个连接点与声明一个顶级连接点非常相似:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
与使用基于模式定义样式声明的 @AspectJ 方面的方式相同,切入点可以通过收集连接点上下文。例如,以下切入点收集“this”对象作为连接点上下文并将它传递给建议:
<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
引用,不能用作命名切点来形成复合切点。因此,基于模式的定义风格中命名的切点支持比 @AspectJ 样式所提供的支持更有限。
Declaring Advice
基于模式的 AOP 支持使用与 @AspectJ 样式相同的 5 种建议,并且它们具有完全相同的语义。
Before Advice
建议在匹配的方法执行前执行。它通过使用 <aop:before>
元素在<aop:aspect>
中声明,如以下示例所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
在上面的示例中,dataAccessOperation
是在顶部(<aop:config>
)级别定义的 named pointcut 的 id
(请参阅 Declaring a Pointcut)。
正如我们在对 @AspectJ 样式的讨论中指出的,使用 named pointcuts 可以大大提高代码的可读性。详情请参阅 Sharing Named Pointcut Definitions。 |
要内联定义切点,请使用 pointcut
属性替换 pointcut-ref
属性,如下所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
method
属性标识提供通知正文的方法 (doAccessCheck
)。必须为包含通知的 aspect 元素引用的 bean 定义此方法。在执行数据访问操作(切点表达式匹配的方法执行连接点)之前,将调用 aspect bean 上的 doAccessCheck
方法。
After Returning Advice
返回通知在匹配的方法执行正常完成之后运行。它在 <aop:aspect>
内声明,方式与 before 通知相同。以下示例显示了如何声明 it:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
与 @AspectJ 样式相同,您可以在通知主体中获取返回值。为此,请使用 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
退出通知在匹配的方法执行通过抛出异常退出时运行。它通过使用 after-throwing
元素在 <aop:aspect>
内声明,如下例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doRecoveryActions"/>
...
</aop:aspect>
与 @AspectJ 样式相同,您可以在通知主体中获取抛出的异常。为此,请使用 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 (Finally) Advice
无论匹配的方法执行如何退出,After(finally)通知都会运行。您可以使用 after
元素声明它,如下例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doReleaseLock"/>
...
</aop:aspect>
Around Advice
最后一种通知是 around 通知。Around 通知在匹配方法的执行周围运行。它有机会在方法运行之前和之后都进行工作,并确定方法何时、如何,甚至是否真正运行。如果您需要在多线程方式下在方法执行之前和之后共享状态(例如,启动和停止计时器),则经常使用 around 通知。
始终使用满足您要求中最不强大的通知形式。 例如,如果 before 通知足以满足您的需要,请勿使用 around 通知。 |
您可以使用 aop:around
元素声明环绕通知。通知方法应声明其返回类型为 Object
,并且方法的第一个参数必须是 ProceedingJoinPoint
类型。在通知方法的主体中,您必须在 ProceedingJoinPoint
上调用 proceed()
以运行底层方法。没有任何参数地调用 proceed()
将导致在调用底层方法时为其提供调用方的原始参数。对于高级用例,proceed()
方法有一个重载变体,它接受参数数组(Object[]
)。数组中的值将作为在调用底层方法时的参数。有关使用 Object[]
调用 proceed
的注释,请参阅 Around Advice。
以下示例显示了如何在 XML 中声明 around 通知:
<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()
}
Advice Parameters
基于模式的声明样式通过按名称对比切点参数和通知方法参数,以与 @AspectJ 支持相同的方式支持完全类型的通知。有关详细信息,请参阅 Advice Parameters。如果您希望明确指定通知方法的参数名称(不依赖于前述检测策略),您可以使用通知元素的 arg-names
属性来执行此操作,该属性的处理方式与通知注释中的 argNames
属性相同(如 Determining Argument Names 所述)。以下示例展示了如何在 XML 中指定参数名称:
<aop:before
pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)" 1
method="audit"
arg-names="auditable" />
1 | 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。 |
arg-names
属性接受一个用逗号分隔的参数名称列表。
以下是基于 XSD 的方法的一个稍微复杂一些的示例,展示了一些与许多强类型参数结合使用的 around 通知:
-
Java
-
Kotlin
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);
}
}
interface PersonService {
fun getPerson(personName: String, age: Int): Person
}
class DefaultPersonService : PersonService {
fun getPerson(name: String, age: Int): Person {
return Person(name, age)
}
}
接下来是方面。请注意:profile(..)
方法接受一些强类型参数,第一个恰好是用于继续方法调用的连接点。此参数的存在指示 profile(..)
将被用作“around”建议,如下例所示:
-
Java
-
Kotlin
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());
}
}
}
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 配置会对特定连接点执行前一个建议:
<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 Ordering
当多条通知需要在同一点连接点(执行方法)上运行时,排序规则如 Advice Ordering 中所述。方面之间的优先级通过 <aop:aspect>
元素中的 order
属性确定,或通过将 @Order
注释添加到支持该方面的 Bean,或通过让 Bean 实现 Ordered
接口。
与在同一 |
Introductions
引入(在 AspectJ 中称为跨类型声明)允许一个方面声明所建议对象实现给定接口,代表这些对象提供此接口的实现。
你可以在 aop:aspect
中使用 aop:declare-parents
元素进行引入。你可以使用 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
接口。请注意,在前一个示例的建议之前,服务 bean 可以直接用作 UsageTracked
接口的实现。若要以编程方式访问 bean,你可以编写以下内容:
-
Java
-
Kotlin
UsageTracked usageTracked = context.getBean("myService", UsageTracked.class);
val usageTracked = context.getBean("myService", UsageTracked.class)
Advisors
“顾问” 的概念源自 Spring 中定义的 AOP 支持,并且在 AspectJ 中没有直接等效项。顾问就像一个小型的、独立的方面,只包含一条通知。通知本身由 Bean 表示,并且必须实现 Advice Types in Spring 中描述的一个通知接口。顾问可以利用 AspectJ 切点表达式。
Spring 通过 <aop: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
属性来内联定义切入点表达式。
若要定义顾问的优先级以便建议能参与排序,请使用 order
属性来定义顾问的 Ordered
值。
An AOP Schema Example
本节展示了当使用模式支持重新编写时,An AOP Example 中的并发锁定失败重试示例是如何表现的。
由于并发问题(例如,死锁失败),业务服务的执行有时会失败。如果重新运行该操作,它在下次尝试时很可能会成功。对于在这种情况(在解决冲突时无需返回给用户的幂等操作)下适合重试的业务服务,我们希望透明地重试操作,以避免客户端看到 PessimisticLockingFailureException
。这是在服务层中明显跨越多个服务的要求,因此非常适合通过一个方面来实现。
因为我们想要重试该操作,因此我们需要使用around建议,以便我们可以多次调用`proceed`。以下清单显示了基本切面实现(这是一个使用模式支持的常规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配置。主操作发生在around建议方法`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)"/>