此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Framework 6.2.4! |
基于 Schema 的 AOP 支持
如果你更喜欢基于 XML 的格式,Spring 还提供了对定义方面的支持
使用aop
namespace 标签。完全相同的切入点表达式和通知类型
与使用 @AspectJ 样式时一样。因此,在本节中,我们重点关注
该语法,并让读者参考上一节中的讨论
(@AspectJ支持)用于理解编写切入点表达式和绑定
的建议参数。
要使用本节中描述的 aop 命名空间标签,您需要导入spring-aop
模式,如 XML 基于模式的配置 中所述。请参阅 AOP 架构,了解如何在aop
Namespace。
在你的 Spring 配置中,所有 aspect 和 advisor 元素都必须放在
一<aop:config>
元素(您可以有多个<aop:config>
元素中
应用程序上下文配置)。一<aop:config>
元素可以包含 pointcut、
advisor 和 aspect 元素(请注意,这些元素必须按此顺序声明)。
这<aop:config> 样式大量使用了 Spring 的自动代理机制。这可能会导致问题(例如建议
not being woven)如果您已经通过使用BeanNameAutoProxyCreator 或类似的东西。建议的使用模式是
仅使用<aop:config> 样式或仅AutoProxyCreator style 和
切勿混合使用它们。 |
声明一个 Aspect
当您使用 schema 支持时,aspect 是定义为 您的 Spring 应用程序上下文。状态和行为在字段中捕获,并且 方法,以及 XML 中的切入点和通知信息。
您可以使用<aop:aspect>
元素,并引用支持 Bean
通过使用ref
属性,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
支持 aspect (aBean
在这种情况下)当然可以配置,并且
依赖项注入,就像任何其他 Spring bean 一样。
声明切入点
您可以在<aop:config>
元素,让切入点
定义在多个方面和顾问之间共享。
表示服务层中任何业务服务的执行的切入点可以 定义如下:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))" />
</aop:config>
请注意,切入点表达式本身使用相同的 AspectJ 切入点表达式
语言,如 @AspectJ 支持中所述。如果使用基于架构的声明
style 中,您还可以引用@Aspect
类型中的
切入点表达式。因此,定义上述切入点的另一种方法如下:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.CommonPointcuts.businessService()" /> (1)
</aop:config>
1 | 引用businessService 在共享命名切入点定义中定义的命名切入点。 |
在 aspect 中声明一个切入点与声明一个顶级切入点非常相似。 如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
与 @AspectJ 方面大致相同,使用基于 schema 的 schema 声明的切入点
定义样式可以收集连接点上下文。例如,以下切入点
收集this
object 作为连接点上下文,并将其传递给通知:
<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 提供的定义样式更受限制
风格。
申报通知书
基于 schema 的 AOP 支持使用与 @AspectJ 样式相同的五种通知,并且它们具有 完全相同的语义。
建议前
Before 通知在匹配的方法执行之前运行。它是在<aop:aspect>
通过使用<aop:before>
元素,如下例所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
在上面的示例中,dataAccessOperation
是id
定义于
顶部 (<aop:config>
) 级别(请参阅声明切入点)。
正如我们在讨论 @AspectJ 样式时所指出的,使用命名切入点可以 显著提高代码的可读性。请参阅共享命名切入点定义 详。 |
要定义内联切入点,请将pointcut-ref
属性替换为pointcut
属性,如下所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
这method
属性标识方法 (doAccessCheck
),它提供
建议。必须为 aspect 元素引用的 bean 定义此方法
其中包含建议。在执行数据访问作之前(方法执行
连接点匹配),则doAccessCheck
aspect 上的 method
bean 被调用。
退货后通知
返回后,通知在匹配的方法执行正常完成时运行。是的
在<aop:aspect>
和之前的建议一样。以下示例
演示如何声明它:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
与 @AspectJ 样式一样,您可以在 Advice Body 中获取返回值。
为此,请使用returning
属性来指定
应传递 return 值,如下例所示:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
这doAccessCheck
method 必须声明一个名为retVal
.此 type of this
parameter 约束匹配,其方式与@AfterReturning
.为
example,您可以按如下方式声明 Method Signature:
-
Java
-
Kotlin
public void doAccessCheck(Object retVal) {...
fun doAccessCheck(retVal: Any) {...
抛出后的建议
抛出后,当匹配的方法执行退出时,通过抛出一个
例外。它是在<aop:aspect>
通过使用after-throwing
元素
如下例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doRecoveryActions"/>
...
</aop:aspect>
与 @AspectJ 样式一样,您可以在通知正文中获取 thrown 异常。
为此,请使用throwing
attribute 来指定参数的名称
应传递该异常,如下例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
这doRecoveryActions
method 必须声明一个名为dataAccessEx
.
此参数的类型以与@AfterThrowing
.例如,方法签名可以按如下方式声明:
-
Java
-
Kotlin
public void doRecoveryActions(DataAccessException dataAccessEx) {...
fun doRecoveryActions(dataAccessEx: DataAccessException) {...
之后(最后)建议
After (finally) 通知运行,无论匹配的方法执行如何退出。
您可以使用after
元素,如下例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doReleaseLock"/>
...
</aop:aspect>
周边建议
最后一种建议是围绕建议。环绕建议 “绕过” 匹配的 方法的执行。它有机会在方法之前和之后都做工作 运行,并确定该方法何时、如何运行,甚至是否真的开始运行。 如果你需要在方法之前和之后共享状态,通常会使用 Around 通知 以线程安全的方式执行 – 例如,启动和停止计时器。
始终使用满足您要求的最弱的建议形式。 例如,如果 before advice 足以满足您的需求,请不要使用 around advice。 |
你可以使用aop:around
元素。通知方法应该
宣Object
作为其返回类型,并且该方法的第一个参数必须为
类型ProceedingJoinPoint
.在 advice 方法的主体中,您必须调用proceed()
在ProceedingJoinPoint
以便底层方法运行。
调用proceed()
不带参数将导致调用者的原始参数
在调用它时提供给底层方法。对于高级用例,有
是proceed()
method 接受一个参数数组
(Object[]
).数组中的值将用作底层
方法。参见 Around Advice 了解打电话的注意事项proceed
替换为Object[]
.
下面的示例展示了如何在 XML 中声明 around advice:
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut="execution(* com.xyz.service.*.*(..))"
method="doBasicProfiling"/>
...
</aop:aspect>
的doBasicProfiling
advice 可以与
@AspectJ示例(当然不包括 Annotation),如下例所示:
-
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 参数
基于 schema 的声明样式支持完全类型化的通知,其方式与
针对 @AspectJ 支持进行描述 — 通过按名称将切入点参数与
通知方法参数。有关详细信息,请参阅 Advice Parameters 。如果您愿意
要显式指定通知方法的参数名称(不依赖于
检测策略),您可以使用arg-names
属性,其处理方式与argNames
属性(如确定参数名称中所述)。
以下示例演示如何在 XML 中指定参数名称:
<aop:before
pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)" (1)
method="audit"
arg-names="auditable" />
1 | 引用publicMethod 命名切入点在 组合切入点表达式 中定义。 |
这arg-names
attribute 接受以逗号分隔的参数名称列表。
以下基于 XSD 的方法的稍微复杂一些的示例显示了 一些 around 建议与许多强类型参数结合使用:
-
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)
}
}
接下来是方面。请注意,profile(..)
method 接受多个
强类型参数,其中第一个参数恰好是用于
继续执行 method 调用。此参数的存在表明profile(..)
将用作around
建议,如下例所示:
-
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())
}
}
}
Finally, the following example XML configuration effects the execution of the
preceding advice for a particular join point:
<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>
Consider the following driver script:
-
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)
}
With such a Boot
class, we would get output similar to the following on standard output:
StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0
-----------------------------------------
ms % Task name
-----------------------------------------
00000 ? execution(getFoo)
Advice Ordering
When multiple pieces of advice need to run at the same join point (executing method)
the ordering rules are as described in Advice Ordering. The precedence
between aspects is determined via the order
attribute in the <aop:aspect>
element or
by either adding the @Order
annotation to the bean that backs the aspect or by having
the bean implement the Ordered
interface.
In contrast to the precedence rules for advice methods defined in the same @Aspect
class, when two pieces of advice defined in the same <aop:aspect>
element both need to
run at the same join point, the precedence is determined by the order in which the advice
elements are declared within the enclosing <aop:aspect>
element, from highest to lowest
precedence.
For example, given an around
advice and a before
advice defined in the same
<aop:aspect>
element that apply to the same join point, to ensure that the around
advice has higher precedence than the before
advice, the <aop:around>
element must be
declared before the <aop:before>
element.
As a general rule of thumb, if you find that you have multiple pieces of advice defined
in the same <aop:aspect>
element that apply to the same join point, consider collapsing
such advice methods into one advice method per join point in each <aop:aspect>
element
or refactor the pieces of advice into separate <aop:aspect>
elements that you can order
at the aspect level.
Introductions
Introductions (known as inter-type declarations in AspectJ) let an aspect declare
that advised objects implement a given interface and provide an implementation of
that interface on behalf of those objects.
You can make an introduction by using the aop:declare-parents
element inside an aop:aspect
.
You can use the aop:declare-parents
element to declare that matching types have a new parent (hence the name).
For example, given an interface named UsageTracked
and an implementation of that interface named
DefaultUsageTracked
, the following aspect declares that all implementors of service
interfaces also implement the UsageTracked
interface. (In order to expose statistics
through JMX for example.)
<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>
The class that backs the usageTracking
bean would then contain the following method:
-
Java
-
Kotlin
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
}
The interface to be implemented is determined by the implement-interface
attribute. The
value of the types-matching
attribute is an AspectJ type pattern. Any bean of a
matching type implements the UsageTracked
interface. Note that, in the before
advice of the preceding example, service beans can be directly used as implementations of
the UsageTracked
interface. To access a bean programmatically, you could write the
following:
-
Java
-
Kotlin
UsageTracked usageTracked = context.getBean("myService", UsageTracked.class);
val usageTracked = context.getBean("myService", UsageTracked.class)
Aspect Instantiation Models
The only supported instantiation model for schema-defined aspects is the singleton
model. Other instantiation models may be supported in future releases.
Advisors
The concept of "advisors" comes from the AOP support defined in Spring
and does not have a direct equivalent in AspectJ. An advisor is like a small
self-contained aspect that has a single piece of advice. The advice itself is
represented by a bean and must implement one of the advice interfaces described in
Advice Types in Spring. Advisors can take advantage of AspectJ pointcut expressions.
Spring supports the advisor concept with the <aop:advisor>
element. You most
commonly see it used in conjunction with transactional advice, which also has its own
namespace support in Spring. The following example shows an advisor:
<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>
As well as the pointcut-ref
attribute used in the preceding example, you can also use the
pointcut
attribute to define a pointcut expression inline.
To define the precedence of an advisor so that the advice can participate in ordering,
use the order
attribute to define the Ordered
value of the advisor.
An AOP Schema Example
This section shows how the concurrent locking failure retry example from
An AOP Example looks when rewritten with the schema support.
The execution of business services can sometimes fail due to concurrency issues (for
example, a deadlock loser). If the operation is retried, it is likely to succeed
on the next try. For business services where it is appropriate to retry in such
conditions (idempotent operations that do not need to go back to the user for conflict
resolution), we want to transparently retry the operation to avoid the client seeing a
PessimisticLockingFailureException
. This is a requirement that clearly cuts across
multiple services in the service layer and, hence, is ideal for implementing through an
aspect.
Because we want to retry the operation, we need to use around advice so that we can
call proceed
multiple times. The following listing shows the basic aspect implementation
(which is a regular Java class that uses the schema support):
-
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
}
}
Note that the aspect implements the Ordered
interface so that we can set the precedence of
the aspect higher than the transaction advice (we want a fresh transaction each time we
retry). The maxRetries
and order
properties are both configured by Spring. The
main action happens in the doConcurrentOperation
around advice method. We try to
proceed. If we fail with a PessimisticLockingFailureException
, we try again,
unless we have exhausted all of our retry attempts.
This class is identical to the one used in the @AspectJ example, but with the
annotations removed.
The corresponding Spring configuration is as follows:
<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>
Notice that, for the time being, we assume that all business services are idempotent. If
this is not the case, we can refine the aspect so that it retries only genuinely
idempotent operations, by introducing an Idempotent
annotation and using the annotation
to annotate the implementation of service operations, as the following example shows:
-
Java
-
Kotlin
@Retention(RetentionPolicy.RUNTIME)
// marker annotation
public @interface Idempotent {
}
@Retention(AnnotationRetention.RUNTIME)
// marker annotation
annotation class Idempotent
The
change to the aspect to retry only idempotent operations involves refining the
pointcut expression so that only @Idempotent
operations match, as follows:
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..)) and
@annotation(com.xyz.service.Idempotent)"/>