基于模式的 AOP 支持
如果你更喜欢基于 XML 的格式,Infra 还支持使用 aop 命名空间标签定义切面。
使用 @AspectJ 风格时支持的切入点表达式和 advice 类型与此完全相同。
因此,在本节中,我们重点介绍该语法,并让读者参考上一节(@AspectJ 支持)中的讨论,以了解编写切入点表达式和 advice 参数的绑定。
要使用本节中描述的 aop 命名空间标签,你需要导入 infra-aop 模式,如 基于 XML 模式的配置 中所述。
有关如何在 aop 命名空间中导入标签,请参阅 AOP 模式。
在 Infra 配置中,所有 aspect 和 advisor 元素都必须放置在 <aop:config> 元素内(你可以在应用程序上下文配置中有多个 <aop:config> 元素)。
<aop:config> 元素可以包含 pointcut、advisor 和 aspect 元素(注意这些必须按该顺序声明)。
<aop:config> 配置风格大量使用 Infra 自动代理 机制。
如果你已经通过使用 BeanNameAutoProxyCreator 或类似的东西使用了显式自动代理,这可能会导致问题(例如 advice 未被织入)。
推荐的使用模式是仅使用 <aop:config> 风格或仅使用 AutoProxyCreator 风格,切勿混合使用。
|
声明 Aspect
当你使用模式支持时,切面是在 Infra 应用程序上下文中定义为 bean 的常规 Java 对象。 状态和行为在对象的字段和方法中捕获,而切入点和 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)当然可以像任何其他 Infra bean 一样进行配置和依赖注入。
声明 Pointcut
你可以在 <aop:config> 元素内声明一个 命名 pointcut,让 pointcut 定义在多个 aspect 和 advisor 之间共享。
代表服务层中任何业务服务执行的 pointcut 可以定义如下:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))" />
</aop:config>
请注意,pointcut 表达式本身使用与 @AspectJ 支持 中描述的相同的 AspectJ 切入点表达式语言。
如果你使用基于模式的声明风格,你还可以引用在 pointcut 表达式中的 @Aspect 类型中定义的 命名 pointcut。
因此,定义上述 pointcut 的另一种方法如下:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.CommonPointcuts.businessService()" /> (1)
</aop:config>
| 1 | 引用 共享命名 Pointcut 定义 中定义的 businessService 命名 pointcut。 |
在 aspect 内部 声明 pointcut 与声明顶级 pointcut 非常相似,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
与 @AspectJ 切面非常相似,使用基于模式的定义风格声明的 pointcut 可以收集连接点上下文。
例如,以下 pointcut 收集 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>
必须声明 advice 以通过包含匹配名称的参数来接收收集的连接点上下文,如下所示:
-
Java
public void monitor(Object service) {
// ...
}
组合 pointcut 子表达式时,&& 在 XML 文档中很尴尬,因此你可以使用 and、or 和 not 关键字分别代替 &&、|| 和 !。
例如,前面的 pointcut 可以更好地写成如下:
<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>
请注意,以这种方式定义的 pointcut 由其 XML id 引用,不能用作命名 pointcut 来形成复合 pointcut。
因此,基于模式的定义风格中的命名 pointcut 支持比 @AspectJ 风格提供的支持更有限。
声明 Advice
基于模式的 AOP 支持使用与 @AspectJ 风格相同的五种 advice,并且它们具有完全相同的语义。
前置 Advice
前置 advice 在匹配的方法执行之前运行。它在 <aop:aspect> 内部使用 <aop:before> 元素声明,如下例所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
在上面的示例中,dataAccessOperation 是在顶部(<aop:config>)级别定义的 命名 pointcut 的 id(请参阅 声明 Pointcut)。
| 正如我们在 @AspectJ 风格的讨论中指出的那样,使用 命名 pointcut 可以显着提高代码的可读性。 有关详细信息,请参阅 共享命名 Pointcut 定义。 |
要改为内联定义 pointcut,请将 pointcut-ref 属性替换为 pointcut 属性,如下所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
method 属性标识提供 advice 主体的方法(doAccessCheck)。
此方法必须为包含 advice 的 aspect 元素引用的 bean 定义。
在执行数据访问操作(由 pointcut 表达式匹配的方法执行连接点)之前,将调用 aspect bean 上的 doAccessCheck 方法。
返回后 Advice
返回后 advice 在匹配的方法执行正常完成时运行。它在 <aop:aspect> 内部以与前置 advice 相同的方式声明。
以下示例显示了如何声明它:
<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
public void doAccessCheck(Object retVal) {...
抛出后 Advice
抛出后 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
public void doRecoveryActions(DataAccessException dataAccessEx) {...
After (Finally) Advice
无论匹配的方法执行如何退出,After (finally) advice 都会运行。
你可以使用 after 元素声明它,如下例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doReleaseLock"/>
...
</aop:aspect>
环绕 Advice
最后一种 advice 是 环绕 advice。环绕 advice 在匹配的方法执行“周围”运行。 它有机会在方法运行之前和之后做工作,并确定何时、如何以及即使方法实际上完全运行。 如果你需要以线程安全的方式在方法执行之前和之后共享状态(例如,启动和停止计时器),通常使用环绕 advice。
|
始终使用满足你要求的最不强大的 advice 形式。 例如,如果 前置 advice 足以满足你的需求,请不要使用 环绕 advice。 |
你可以使用 aop:around 元素声明环绕 advice。advice 方法应声明 Object 作为其返回类型,并且方法的第一个参数必须是 ProceedingJoinPoint 类型。
在 advice 方法的主体内,你必须在 ProceedingJoinPoint 上调用 proceed() 才能运行底层方法。
不带参数调用 proceed() 将导致调用者的原始参数在调用时提供给底层方法。
对于高级用例,有一个 proceed() 方法的重载变体,它接受参数数组 (Object[])。
数组中的值将在调用时用作底层方法的参数。
有关使用 Object[] 调用 proceed 的说明,请参阅 环绕 Advice。
以下示例显示了如何在 XML 中声明环绕 advice:
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut="execution(* com.xyz.service.*.*(..))"
method="doBasicProfiling"/>
...
</aop:aspect>
doBasicProfiling advice 的实现可以与 @AspectJ 示例完全相同(当然减去注解),如下例所示:
-
Java
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// 启动秒表
Object retVal = pjp.proceed();
// 停止秒表
return retVal;
}
Advice 参数
基于模式的声明风格支持完全类型化的 advice,方式与 @AspectJ 支持所描述的相同——通过按名称将 pointcut 参数与 advice 方法参数进行匹配。
有关详细信息,请参阅 Advice 参数。
如果你希望显式指定 advice 方法的参数名称(不依赖于前面描述的检测策略),你可以通过使用 advice 元素的 arg-names 属性来实现,
该属性的处理方式与 advice 注解中的 argNames 属性相同(如 确定参数名称 中所述)。
以下示例显示了如何在 XML 中指定参数名称:
<aop:before pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)" (1)
method="audit" arg-names="auditable" />
| 1 | 引用 组合 Pointcut 表达式 中定义的 publicMethod 命名 pointcut。 |
arg-names 属性接受逗号分隔的参数名称列表。
以下稍微复杂一点的基于 XSD 的方法示例显示了一些与许多强类型参数结合使用的环绕 advice:
-
Java
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);
}
}
接下来是切面。请注意 profile(..) 方法接受许多强类型参数的事实,其中第一个恰好是用于继续方法调用的连接点。
此参数的存在表明 profile(..) 将用作 around advice,如下例所示:
-
Java
package com.xyz;
import org.aspectj.lang.ProceedingJoinPoint;
import infra.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());
}
}
}
最后,以下示例 XML 配置对特定连接点执行前面的 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">
<!-- 这是将被 Infra AOP 基础设施代理的对象 -->
<bean id="personService" class="com.xyz.service.DefaultPersonService"/>
<!-- 这是实际的 advice 本身 -->
<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
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);
}
}
使用这样的 Boot 类,我们将在标准输出上获得类似于以下的输出:
StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0 ----------------------------------------- ms % Task name ----------------------------------------- 00000 ? execution(getFoo)
Advice 排序
当多个 advice 需要在同一连接点(执行方法)运行时,排序规则如 Advice 排序 中所述。
切面之间的优先级通过 <aop:aspect> 元素中的 order 属性确定,或者通过向支持切面的 bean 添加 @Order 注解或让 bean 实现 Ordered 接口来确定。
|
与在同一 例如,给定在同一 作为一般经验法则,如果你发现在同一 |
引介
引介(在 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
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
要实现的接口由 implement-interface 属性确定。
types-matching 属性的值是 AspectJ 类型模式。任何匹配类型的 bean 都实现 UsageTracked 接口。
请注意,在前面示例的前置 advice 中,服务 bean 可以直接用作 UsageTracked 接口的实现。
要以编程方式访问 bean,你可以编写以下代码:
-
Java
UsageTracked usageTracked = context.getBean("myService", UsageTracked.class);
Advisors
“advisors”的概念来自 Infra 中定义的 AOP 支持,在 AspectJ 中没有直接等效项。 Advisor 就像一个包含单个 advice 的小型独立切面。advice 本身由 bean 表示,并且必须实现 Infra 中的 Advice 类型 中描述的 advice 接口之一。 Advisors 可以利用 AspectJ 切入点表达式。
Infra 通过 <aop:advisor> 元素支持 advisor 概念。你最常看到它与事务 advice 结合使用,事务 advice 在 Infra 中也有自己的命名空间支持。
以下示例显示了一个 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>
除了前面示例中使用的 pointcut-ref 属性外,你还可以使用 pointcut 属性内联定义 pointcut 表达式。
要定义 advisor 的优先级以便 advice 可以参与排序,请使用 order 属性定义 advisor 的 Ordered 值。
AOP 模式示例
本节展示了当使用模式支持重写时,一个 AOP 示例 中的并发锁定失败重试示例是什么样子的。
业务服务的执行有时可能会由于并发问题(例如,死锁失败者)而失败。如果重试该操作,它很可能会在下一次尝试中成功。
对于适合在此类条件下重试的业务服务(不需要返回给用户进行冲突解决的幂等操作),我们希望透明地重试操作,以避免客户端看到 PessimisticLockingFailureException。
这是一个清楚地跨越服务层中多个服务的需求,因此非常适合通过切面来实现。
因为我们想重试操作,所以我们需要使用环绕 advice,以便我们可以多次调用 proceed。
以下清单显示了基本的切面实现(这是一个使用模式支持的常规 Java 类):
-
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;
}
}
请注意,该切面实现了 Ordered 接口,以便我们可以将切面的优先级设置为高于事务 advice(我们希望每次重试都有一个新的事务)。
maxRetries 和 order 属性均由 Infra 配置。
主要动作发生在 doConcurrentOperation 环绕 advice 方法中。我们尝试继续。
如果我们因 PessimisticLockingFailureException 而失败,我们会重试,除非我们已经用尽了所有重试尝试。
| 此类与 @AspectJ 示例中使用的类相同,但删除了注解。 |
相应的 Infra 配置如下:
<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
@Retention(RetentionPolicy.RUNTIME)
// 标记注解
public @interface Idempotent {
}
对切面的更改以仅重试幂等操作涉及细化 pointcut 表达式,以便仅匹配 @Idempotent 操作,如下所示:
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..)) and
@annotation(com.xyz.service.Idempotent)"/>