如果您更喜欢基于 XML 的格式,Spring 还支持使用aop
命名空间标签定义切面。支持与使用 @AspectJ 样式时完全相同的切入点表达式和通知类型。因此,在本节中,我们将重点放在该语法上,并请读者参考上一节中的讨论(@AspectJ 支持 ),以了解编写切入点表达式和通知参数的绑定。
要使用本节中描述的 aop 命名空间标签,您需要导入 spring-aop
架构,如基于 XML 架构的配置 中所述。 有关如何在aop
命名空间中导入标签的信息,请参阅AOP 模式 。
在您的 Spring 配置中,所有切面和顾问元素都必须放在一个<aop:config>
元素中(您可以在应用程序上下文配置中拥有多个<aop:config>
元素)。一个<aop:config>
元素可以包含切入点、顾问和切面元素(请注意,这些元素必须按此顺序声明)。
<aop:config>
配置风格大量使用了 Spring 的 自动代理 机制。如果您已经通过使用BeanNameAutoProxyCreator
或类似的方式使用显式自动代理,这可能会导致问题(例如未编织通知) 。推荐的使用模式是仅使用<aop:config>
样式或仅使用AutoProxyCreator
样式并且从不混合使用它们。
5.5.1. 声明一个切面
当您使用模式支持时,切面是在 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 一样进行配置和依赖注入。
5.5.2. 声明切入点
您可以在<aop:config>
元素内声明一个命名切入点,让切入点定义在多个切面和顾问之间共享。
表示服务层中任何业务服务执行的切入点可以定义如下:
复制 < aop : config >
< aop : pointcut id = "businessService"
expression = "execution(* com.xyz.myapp.service.*.*(..))" />
</ aop : config >
请注意,切入点表达式本身使用与@AspectJ 支持 中描述的相同的 AspectJ 切入点表达式语言。如果您使用基于模式的声明样式,您可以在切入点表达式中引用类型 (@Aspects) 中定义的命名切入点。定义上述切入点的另一种方法如下:
复制 < aop : config >
< aop : pointcut id = "businessService"
expression = "com.xyz.myapp.CommonPointcuts.businessService()" />
</ aop : config >
假设您有共享通用切入点定义 中描述的CommonPointcuts
切面。
然后在切面内声明切入点与声明顶级切入点非常相似,如以下示例所示:
复制 < aop : config >
< aop : aspect id = "myAspect" ref = "aBean" >
< aop : pointcut id = "businessService"
expression = "execution(* com.xyz.myapp.service.*.*(..))" />
...
</ aop : aspect >
</ aop : config >
与@AspectJ 切面非常相似,使用基于模式的定义样式声明的切入点可以收集连接点上下文。例如,以下切入点收集this
对象作为连接点上下文并将其传递给通知:
复制 < aop : config >
< aop : aspect id = "myAspect" ref = "aBean" >
< aop : pointcut id = "businessService"
expression = "execution(* com.xyz.myapp.service.*.*(..)) && this(service)" />
< aop : before pointcut-ref = "businessService" method = "monitor" />
...
</ aop : aspect >
</ aop : config >
必须通过包含匹配名称的参数来声明通知以接收收集的连接点上下文,如下所示:
复制 public void monitor( Object service) {
// ...
}
组合切入点子表达式时,&&
在 XML 文档中很尴尬,因此您可以分别使用and
、or
和not
关键字来代替&&
、 ||
和!
。例如,前面的切入点可以更好地写成如下:
复制 < aop : config >
< aop : aspect id = "myAspect" ref = "aBean" >
< aop : pointcut id = "businessService"
expression = "execution(* com.xyz.myapp.service.*.*(..)) and this(service)" />
< aop : before pointcut-ref = "businessService" method = "monitor" />
...
</ aop : aspect >
</ aop : config >
请注意,以这种方式定义的切入点由它们的 XML 引用,id
不能用作命名切入点来形成复合切入点。因此,基于模式的定义风格中的命名切入点支持比@AspectJ 风格提供的更有限。
5.5.3. 声明通知
基于模式的 AOP 支持使用与 @AspectJ 样式相同的五种通知,并且它们具有完全相同的语义。
前置通知
Before 通知在匹配的方法执行之前运行。 通过在 <aop:aspect>
内声明使用<aop:before>
元素,如以下示例所示:
复制 < aop : aspect id = "beforeExample" ref = "aBean" >
< aop : before
pointcut-ref = "dataAccessOperation"
method = "doAccessCheck" />
...
</ aop : aspect >
这里,dataAccessOperation
是在顶层 ( <aop:config>
) 级别定义的切入点的id
。要改为内联定义切入点,请将pointcut-ref
属性替换为pointcut
属性,如下所示:
复制 < aop : aspect id = "beforeExample" ref = "aBean" >
< aop : before
pointcut = "execution(* com.xyz.myapp.dao.*.*(..))"
method = "doAccessCheck" />
...
</ aop : aspect >
正如我们在讨论@AspectJ 样式时所指出的,使用命名切入点可以显着提高代码的可读性。
method
属性标识提供通知正文的方法 (doAccessCheck
)。必须为包含通知的切面元素引用的 bean 定义此方法。在执行数据访问操作(切入点表达式匹配的方法执行连接点)之前,将调用切面 bean 上的doAccessCheck
方法。
返回通知
当匹配的方法执行正常完成时,返回通知运行后。它在 <aop:aspect>
内部声明的方式与之前的通知相同。以下示例显示了如何声明它:
复制 < aop : aspect id = "afterReturningExample" ref = "aBean" >
< aop : after-returning
pointcut-ref = "dataAccessOperation"
method = "doAccessCheck" />
...
</ aop : aspect >
与@AspectJ 样式一样,您可以在通知正文中获取返回值。为此,请使用returning
属性指定应将返回值传递到的参数名称,如以下示例所示:
复制 < aop : aspect id = "afterReturningExample" ref = "aBean" >
< aop : after-returning
pointcut-ref = "dataAccessOperation"
returning = "retVal"
method = "doAccessCheck" />
...
</ aop : aspect >
该doAccessCheck
方法必须声明一个名为retVal
的参数。此参数的类型以与@AfterReturning
描述相同的方式约束匹配。例如,您可以如下声明方法签名:
复制 public void doAccessCheck( Object retVal) { ...
异常通知
当匹配的方法执行通过抛出异常退出时,抛出通知运行后。它通过在 <aop:aspect>
内声明使用after-throwing
元素,如以下示例所示:
复制 < aop : aspect id = "afterThrowingExample" ref = "aBean" >
< aop : after-throwing
pointcut-ref = "dataAccessOperation"
method = "doRecoveryActions" />
...
</ aop : aspect >
与@AspectJ 风格一样,您可以在通知正文中获取抛出的异常。为此,请使用throwing
属性指定应将异常传递到的参数的名称,如以下示例所示:
复制 < aop : aspect id = "afterThrowingExample" ref = "aBean" >
< aop : after-throwing
pointcut-ref = "dataAccessOperation"
throwing = "dataAccessEx"
method = "doRecoveryActions" />
...
</ aop : aspect >
doRecoveryActions
方法必须声明一个名为dataAccessEx
的参数。此参数的类型以与@AfterThrowing
描述相同的方式约束匹配 。例如,方法签名可以声明如下:
复制 public void doRecoveryActions( DataAccessException dataAccessEx) { ...
(最终)通知之后
无论匹配的方法执行如何退出,(最终)通知都会运行。您可以使用after
元素来声明它,如以下示例所示:
复制 < aop : aspect id = "afterFinallyExample" ref = "aBean" >
< aop : after
pointcut-ref = "dataAccessOperation"
method = "doReleaseLock" />
...
</ aop : aspect >
环绕通知
最后一种通知是环绕 通知。环绕通知“围绕”匹配方法的执行。它有机会在方法运行之前和之后进行工作,并确定该方法何时、如何以及是否真正开始运行。如果您需要以线程安全的方式在方法执行之前和之后共享状态(例如,启动和停止计时器),则通常使用环绕通知。
始终使用满足您要求的最不强大的通知形式。例如,如果之前 的通知足以满足您的需求,请不要使用环绕通知。
您可以使用aop:around
元素声明环绕通知。通知方法应该声明Object
为它的返回类型,并且方法的第一个参数必须是 ProceedingJoinPoint
。在通知方法的主体中,您必须调用ProceedingJoinPoint
的 proceed()
以使底层方法运行。不带参数调用proceed()
将导致调用者的原始参数在调用时提供给底层方法。对于高级用例,该proceed()
方法有一个重载变体,它接受参数数组 ( Object[]
)。调用时,数组中的值将用作底层方法的参数。有关proceed使用 .
以下示例展示了如何在 XML 中声明环绕通知:
复制 < aop : aspect id = "aroundExample" ref = "aBean" >
< aop : around
pointcut-ref = "businessService"
method = "doBasicProfiling" />
...
</ aop : aspect >
通知的实现doBasicProfiling
可以与@AspectJ 示例中的完全相同(当然,要减去注解),如以下示例所示:
复制 public Object doBasicProfiling( ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp . proceed ();
// stop stopwatch
return retVal;
}
通知参数
基于模式的声明风格以与@AspectJ 支持相同的方式支持完全类型化的通知——通过按名称匹配切入点参数与通知方法参数。有关详细信息,请参阅通知参数 。如果您希望为通知方法显式指定参数名称(不依赖于前面描述的检测策略),您可以使用 通知元素的arg-names
属性来实现,该属性的处理方式与 通知注解中的argNames
属性相同(如确定参数名称 中所述)。以下示例显示如何在 XML 中指定参数名称:
复制 < aop : before
pointcut = "com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
method = "audit"
arg-names = "auditable" />
该arg-names
属性接受以逗号分隔的参数名称列表。
以下基于 XSD 的方法稍微复杂一些的示例显示了一些与许多强类型参数结合使用的环绕通知:
复制 package x . y . 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
通知,如以下示例所示:
复制 package x . y ;
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 ());
}
}
}
最后,以下示例 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 = "x.y.service.DefaultPersonService" />
<!-- this is the actual advice itself -->
< bean id = "profiler" class = "x.y.SimpleProfiler" />
< aop : config >
< aop : aspect ref = "profiler" >
< aop : pointcut id = "theExecutionOfSomePersonServiceMethod"
expression = "execution(* x.y.service.PersonService.getPerson(String,int))
and args(name, age)" />
< aop : around pointcut-ref = "theExecutionOfSomePersonServiceMethod"
method = "profile" />
</ aop : aspect >
</ aop : config >
</ beans >
考虑以下驱动程序脚本:
复制 import org . springframework . beans . factory . BeanFactory ;
import org . springframework . context . support . ClassPathXmlApplicationContext ;
import x . y . service . PersonService ;
public final class Boot {
public static void main ( final String [] args) throws Exception {
BeanFactory ctx = new ClassPathXmlApplicationContext( "x/y/plain.xml" ) ;
PersonService person = (PersonService) ctx . getBean ( "personService" );
person . getPerson ( "Pengo" , 12 );
}
}
使用这样的 Boot 类,我们将在标准输出中获得类似于以下内容的输出:
复制 StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0
-----------------------------------------
ms % Task name
-----------------------------------------
00000 ? execution(getFoo)
通知优先级
当多条通知需要在同一个连接点(执行方法)运行时,排序规则如Advice Ordering 中所述。切面之间的优先级通过元素order
中的属性<aop:aspect>
或通过将@Order
注解添加到支持切面的 bean 或通过让 bean 实现Ordered
接口来确定。
与同一个 @Aspect 类中定义的通知方法的优先级规则相反,当同一个<aop:aspect>
元素中定义的两条通知都需要在同一连接点运行时,优先级由以下顺序决定:其中通知元素在封闭的 <aop:aspect>
元素中声明,优先级从最高到最低。
例如,给定在同一个 <aop:aspect>
元素中定义的应用于同一连接点的 around
通知和 before
通知,为了确保 around
通知比 before
通知具有更高的优先级, 元素必须在 <aop:before>
元素之前声明。
5.5.4. 切面说明
切面说明(在 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.xzy.myapp.service.*+"
implement-interface = "com.xyz.myapp.service.tracking.UsageTracked"
default-impl = "com.xyz.myapp.service.tracking.DefaultUsageTracked" />
< aop : before
pointcut = "com.xyz.myapp.CommonPointcuts.businessService()
and this(usageTracked)"
method = "recordUsage" />
</ aop : aspect >
支持usageTracking
bean 的类将包含以下方法:
复制 public void recordUsage( UsageTracked usageTracked) {
usageTracked . incrementUseCount ();
}
要实现的接口由implement-interface
属性决定。该types-matching
属性的值是一个 AspectJ 类型模式。任何匹配类型的 bean 都会实现该UsageTracked
接口。请注意,在前面示例的之前通知中,服务 bean 可以直接用作UsageTracked
接口的实现。要以编程方式访问 bean,您可以编写以下代码:
复制 UsageTracked usageTracked = (UsageTracked) context . getBean ( "myService" );
5.5.5. 切面实例化模型
唯一受支持的模式定义切面的实例化模型是单例模型。未来版本可能支持其他实例化模型。
5.5.6. 顾问
“顾问”的概念来自 Spring 中定义的 AOP 支持,在 AspectJ 中没有直接的等价物。顾问就像一个独立的小切面,只有一条通知。通知本身由 bean 表示,并且必须实现 Spring 中的 Advice Types 中 描述的通知接口之一。顾问可以利用 AspectJ 切入点表达式。
Spring 通过<aop:advisor>
元素支持顾问概念。您最常看到它与事务通知一起使用,后者在 Spring 中也有自己的命名空间支持。以下示例显示了一个顾问:
复制 < aop : config >
< aop : pointcut id = "businessService"
expression = "execution(* com.xyz.myapp.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
值。
5.5.7。AOP 模式示例
本节展示了 一个 AOP 示例 中的并发锁定失败重试示例在使用模式支持重写时的外观。
由于并发问题(例如,死锁失败者),业务服务的执行有时会失败。如果该操作被重试,则很可能在下一次尝试时成功。对于在这种情况下适合重试的业务服务(不需要返回给用户解决冲突的幂等操作),我们希望透明地重试操作以避免客户端看到 PessimisticLockingFailureException
. 这是一个明确跨越服务层中多个服务的要求,因此非常适合通过切面实现。
因为我们要重试操作,所以我们需要使用around通知,以便我们可以多次调用proceed
。下面的清单显示了基本的切面实现(这是一个使用模式支持的常规 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
接口,以便我们可以将切面的优先级设置为高于事务通知(我们希望每次重试时都有一个新事务)。maxRetries
和order
属性都是由 Spring 配置的。主要动作doConcurrentOperation
发生在around 通知方法中。我们尝试继续。如果我们以 PessimisticLockingFailureException
失败,我们会再试一次,除非我们已经用尽了所有的重试尝试。
此类与@AspectJ 示例中使用的类相同,但删除了注解。
对应的Spring配置如下:
复制 < aop : config >
< aop : aspect id = "concurrentOperationRetry" ref = "concurrentOperationExecutor" >
< aop : pointcut id = "idempotentOperation"
expression = "execution(* com.xyz.myapp.service.*.*(..))" />
< aop : around
pointcut-ref = "idempotentOperation"
method = "doConcurrentOperation" />
</ aop : aspect >
</ aop : config >
< bean id = "concurrentOperationExecutor"
class = "com.xyz.myapp.service.impl.ConcurrentOperationExecutor" >
< property name = "maxRetries" value = "3" />
< property name = "order" value = "100" />
</ bean >
请注意,我们暂时假设所有业务服务都是幂等的。如果不是这种情况,我们可以通过引入注解并使用Idempotent
注解来注解服务操作的实现,来细化切面,使其仅重试真正的幂等操作,如以下示例所示:
复制 @ Retention ( RetentionPolicy . RUNTIME )
public @ interface Idempotent {
// marker annotation
}
对仅重试幂等操作切面的更改涉及改进切入点表达式,以便仅@Idempotent
操作匹配,如下所示:
复制 < aop : pointcut id = "idempotentOperation"
expression = "execution(* com.xyz.myapp.service.*.*(..)) and
@annotation(com.xyz.myapp.service.Idempotent)" />