@AspectJ是一种将aspects声明为用注解来注解Java类的样式。@Aspectj样式是作为Aspectj 5版本的一部分由Aspectj项目引入的。Spring使用和AspectJ相同的注解,它使用了AspectJ提供的用于切入点解析和匹配的库。但是,AOP运行时仍然是纯SpringAOP,并且不依赖于AspectJ编译器或weaver。
要在Spring配置中使用@Aspectj特性,需要启用Spring支持,以便基于@Aspectj特性配置Spring AOP,并根据这些方面是否通知它们来自动代理bean。通过自动代理,我们的意思是,如果Spring确定bean由一个或多个方面通知,它会自动为该bean生成一个代理来拦截方法调用,并确保根据需要执行通知。
可以使用XML或Java风格的配置启用@AspectJ支持。在这两种情况下,你还需要确保aspectj的aaspectjweaver.jar位于应用程序(1.8版或更高版本)的类路径上。这个库可以在AspectJ发行版的lib目录中使用,也可以从Maven中央存储库中使用。
用Java配置启用@AspectJ支持
若要使用Java @Configuration 启用@AspectJ支持,请添加@EnableAspectJAutoProxy注解,如下示例显示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
使用XML开启@AspectJ
要使用基于XML的配置启用@Aspectj支持,请使用aop:aspectj autoproxy元素,如下示例所示:
<aop:aspectj-autoproxy/>
这假定你使用基于XML模式的配置中描述的模式支持。请参阅AOP模式以了解如何导入AOP命名空间中的标记。
在启用@Aspectj支持的情况下,应用程序上下文中定义的具有@Aspectj方面(具有@Aspect注解)类的任何bean都将由spring自动检测并用于配置spring aop。接下来的两个例子展示了一个不太有用的方面所需要的最小定义。
两个示例中的第一个示例显示了应用程序上下文中的常规bean定义,该定义指向具有@Aspect注解的bean类:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
这两个例子中的第二个展示了notveryusefulAspect类定义,它是用org.aspectj.lang.annotation.Aspect注解的:
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
方面(用@Aspect注解的类)可以有方法和字段,与任何其他类相同。它们还可以包含切入点、通知和介绍(inter-type)声明。
通过 component scanning 自动检查AspectJ
你可以在SpringXML配置中将方面类注册为常规bean,或者通过类路径扫描-自动检测它们,与任何其他Spring Managed bean相同。但是,请注意,@Aspect注解不足以在类路径中自动检测。为此,你需要添加一个单独的@Component注解(或者,根据Spring组件扫描程序的规则,添加一个符合条件的自定义原型注解)。
使用其他的aspects来通知aspects?
在SpringAOP中,方面本身不能成为其他方面通知的目标。类上的@Aspect注解将其标记为方面,因此将其从自动代理中排除。
切入点决定连接点,从而使我们能够控制何时执行通知。SpringAOP只支持SpringBean的方法执行连接点,因此你可以将切入点视为匹配SpringBean上方法的执行。一个切入点声明有两部分:一个包含一个名称和任何参数的签名,一个能精确地确定我们感兴趣的执行方法的切入点表达式。在aop的@Aspectj注解样式中,通过常规方法定义提供切入点签名,并使用@Pointcut注解指示切入点表达式(作为切入点签名的方法必须具有void返回类型)。
一个例子可能有助于清晰地区分切入点签名和切入点表达式。下面的示例定义一个名为anyOldTransfer的切入点,该切入点与名为Transfer的任何方法的执行相匹配:
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature
构成@Pointcut注解值的pointcut表达式是一个正则的aspectj 5 pointcut表达式。有关aspectj的切入点语言的完整讨论,请参阅《aspectj编程指南》(和,有关扩展,请参阅aspectj 5开发人员的笔记本)或其中一本关于aspectj的书(例如,由Colyer等人撰写的Eclipse aspectj,或由Ramnivas Laddad撰写的Aspectj in Action)。
支持的切入点指示符
SpringAOP支持在切入点表达式中使用的以下AspectJ切入点指示符(PCD):
- execution:用于匹配方法执行连接点。这是使用SpringAOP时要使用的主要切入点指示符。
- within:限制匹配到特定类型中的连接点(使用SpringAOP时,在匹配类型中声明的方法的执行)。
- this:将匹配限制为连接点(使用SpringAOP时方法的执行),其中bean引用(SpringAOP代理)是给定类型的实例。
- target:限制匹配到连接点(使用SpringAOP时方法的执行),其中目标对象(要代理的应用程序对象)是给定类型的实例。
- args:限制匹配到联接点(使用SpringAOP时方法的执行),其中参数是给定类型的实例。
- @target:限制匹配到连接点(使用SpringAOP时方法的执行),其中执行对象的类具有给定类型的注解。
- @args:限制匹配到联接点(使用SpringAOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注解。
- @within:限制与具有给定注解的类型中的联接点匹配(使用SpringAOP时,在具有给定注解的类型中声明的方法的执行)。
- @annotation:限制匹配到连接点的主题(在SpringAOP中执行的方法)具有给定注解的连接点。
其他的pointcut类型
完整的AspectJ pointcut语言支持Spring中不支持的其他pointcut指示符:call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this和@withincode。在SpringAOP解释的切入点表达式中使用这些切入点指示符会导致引发IllegalArgumentException。
SpringAOP支持的切入点指示符集可以在未来的版本中扩展,以支持更多的AspectJ切入点指示符。
因为SpringAOP只限制与方法执行连接点匹配,所以前面关于切入点指示符的讨论给出了比AspectJ编程指南中更窄的定义。另外,AspectJ本身具有基于类型的语义,并且在执行连接点,this和target都引用同一个对象:执行方法的对象。SpringAOP是一个基于代理的系统,它区分代理对象本身(与this绑定)和代理背后的目标对象(与target绑定)。
由于Spring的AOP框架基于代理的特性,根据定义,目标对象内的调用不会被拦截。对于JDK代理,只能截获对代理的公共接口方法调用。使用cglib,可以截获代理上的公共和受保护的方法调用(如果需要,甚至可以截获包可见的方法)。但是,通过代理进行的公共交互应该始终通过公共签名进行设计。
注意,切入点定义通常与任何被截取的方法相匹配。如果一个切入点严格来说是只公开的,即使在有可能通过代理进行非公开交互的CGLIB代理场景中,也需要相应地定义它。
如果拦截需要在目标类中包含方法调用甚至构造函数,那么考虑使用Spring驱动的native AspectJ weaving,而不是Spring的基于代理的AOP框架。这构成了一种不同的AOP使用模式,具有不同的特性,因此在做出决定之前一定要熟悉weaving。
SpringAOP还支持一个额外的PCD bean。这个PCD允许你将连接点的匹配限制为特定名字的Spring Bean或一组Spring Bean(使用通配符时)。bean PCD具有以下形式:
bean(idOrNameOfBean)
idorNameOfBean令牌可以是任何Spring Bean的名称。提供了使用*字符的有限通配符支持,因此,如果为Spring Bean建立了一些命名约定,则可以编写bean PCD表达式来选择它们。与其他切入点指示符一样,bean pcd可以与&&(and)、||(or)和!(否定)运算符一起使用。
bean PCD仅在SpringAOP中受支持,在native AspectJ编织中不受支持。它是AspectJ定义的标准PCD的特定于Spring的扩展,因此不能用于@Aspect模型中声明的方面。
bean PCD在实例级别(基于Spring Bean名称概念构建)而不是仅在类型级别(基于编织的AOP受到限制)运行。基于实例的切入点指示符是Spring基于代理的AOP框架的一种特殊功能,它与SpringBean工厂紧密集成,在这里可以自然和简单的按名称标识特定的bean。
组合切入点表达式
你可以使用&&、|| 和!组合切入点表达式。也可以按名称引用切入点表达式。下面的示例显示三个切点表达式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
如前所示,最好使用较小的命名组件构建更复杂的切点表达式。当按名称引用切入点时,使用正常的Java可见性规则(可以在同一类型中看到私有切入点、在层次结构中看到protected切入点、在任何地方看到公共切入点等)。可见性不影响切入点匹配。
共享公共切入点定义
在处理企业应用程序时,开发人员通常希望从多个方面引用应用程序的模块和特定的操作集。我们建议定义一个“SystemArchitecture”方面,该方面为此目的捕获公共切入点表达式。此类方面通常类似于以下示例:
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.service..*)")
public void inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.someapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.dao..*)")
public void inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
public void businessService() {}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
你可以在需要切入点表达式的任何地方引用在这样一个方面中定义的切入点。例如,要使服务层具有事务性,可以编写以下内容:
<aop:config>
<aop:advisor
pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
我们会在基于模式的AOP支持中讨论<aop:config>和&l;taop:advisor>元素。在事务管理中讨论事务元素。
例子
SpringAOP用户最经常使用执行切入点指示符。执行表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除返回类型模式(前面代码段中的ret-type-pattern)、名称模式和参数模式之外的所有部分都是可选的。返回类型模式确定方法的返回类型,以便匹配连接点。*最常用作返回类型模式。它匹配任何返回类型。只有当方法返回给定类型时,完全限定的类型名才匹配。名称模式与方法名匹配。你可以使用*通配符作为名称模式的全部或部分。如果指定声明类型模式,请包含后缀.将其连接到名称模式组件。参数模式稍微复杂一点:()匹配不带参数的方法,而(..)匹配任何数量(零个或多个)的参数。(*)模式与采用任何类型参数的方法匹配。(*,string)匹配接受两个参数的方法。第一个可以是任何类型,而第二个必须是字符串。有关更多信息,请参阅AspectJ编程指南的语言语义部分。
以下示例显示了一些常见的切入点表达式:
-
执行任何公共方法:
execution(public * *(..))
-
执行任何以set开头的方法:
execution(* set*(..))
-
执行任何定义在AccountService类的方法:
execution(* com.xyz.service.AccountService.*(..))
-
执行任何定义在service包中的方法:
execution(* com.xyz.service.*.*(..))
-
执行任何定义在service包或者他的一个子包中的方法:
execution(* com.xyz.service..*.*(..))
-
任何在service包中的连接点(仅仅是Spring AOP中执行的方法)
within(com.xyz.service.*)
-
service包或其子包中的任何连接点(仅在SpringAOP中执行的方法):
within(com.xyz.service..*)
-
任何实现了AccountService接口的代理连接点(仅在SpringAOP中执行的方法):
this(com.xyz.service.AccountService)
this更加通常被用在绑定form中。
-
任何实现了AccountService接口的目标对象连接点(仅在SpringAOP中执行的方法):
target(com.xyz.service.AccountService)
target更加通常被用在绑定form中。
-
任何接收一个运行时是Serializable类型参数的连接点(仅在SpringAOP中执行的方法):
args(java.io.Serializable)
args更加通常被用在绑定form中。
注意,在本例中给出的切入点与execution( __(java.io.Serializable))不同。如果在运行时传递的参数是Serializable的,则args版本匹配;如果方法签名声明了Serializable的单个参数,则执行版本匹配。
-
任何目标对象有@Transactional 注解的连接点(仅在SpringAOP中执行的方法):
@target(org.springframework.transaction.annotation.Transactional)
@target也可以被用在绑定form中。
-
目标对象的声明类型具有@transactional注解的任何连接点(仅在Spring AOP中执行的方法):
@within(org.springframework.transaction.annotation.Transactional)
@within也可以被用在绑定form中。
-
执行方法具有@transactional注解的任何连接点(仅在Spring AOP中执行方法):
@annotation(org.springframework.transaction.annotation.Transactional)
@annotation也可以被用在绑定form中。
-
接受单个参数的任何连接点(仅在Spring AOP中执行方法),并且传递参数的运行时类型具有@Classified annotation:
@args(com.xyz.security.Classified)
@args也可以被用在绑定form中。
-
名为tradeService的SpringBean上的任何连接点(仅在SpringAOP中执行方法):
bean(tradeService)
-
SpringBean上的任何连接点(仅在SpringAOP中执行方法),其名称与通配符表达式*Service匹配:
bean(*Service)
写好切入点
在编译期间,AspectJ处理切入点以优化匹配性能。检查代码并确定每个连接点是否匹配(静态或动态)给定的切入点是一个代价高昂的过程。(动态匹配意味着无法从静态分析中完全确定匹配,并且在代码中放置一个测试以确定代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点是用DNF(析取标准形式)重写的,切入点的组件被排序,以便首先检查那些评估成本更低的组件。这意味着你不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何顺序提供它们。
然而,Aspectj只能处理它被告知的内容。为了获得最佳的匹配性能,你应该考虑他们试图实现什么,并在定义中尽可能地缩小匹配的搜索空间。现有的指示符自然分为三类:kinded, scoping, 和 contextual:
- kinded指示符选择特定类型的连接点:execution, get, set, call, 和 handler.。
- scoping指示符选择一组感兴趣的连接点(可能有多种):within和withincode
- contextual指示符根据上下文匹配(可选绑定):this、target和@annotation
写得好的切入点应该至少包括前两种类型(kinded和scoping)。你可以根据连接点上下文包含要匹配的上下文指示符,或者绑定该上下文以在通知中使用。由于额外的处理和分析,可以仅提供一个类似的指示符或上下文指示符,但可能会影响编织性能(使用的时间和内存)。范围指示符匹配非常快,使用它们意味着AspectJ可以很快地消除不应该进一步处理的连接点组。如果可能的话,一个好的切入点应该总是包括一个。
通知与切入点表达式关联,并在切入点匹配的方法执行before, after, 或 around 运行。切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。
Before Advice
可以使用@before在方面中声明before advice:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
如果使用inline切入点表达式,可以将前面的示例重写为以下示例:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
After Returning Advice
当一个匹配的方法正常返回时执行After returning advice。你可以通过@AfterReturning注解来声明:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
你可以在同一方面内拥有多个通知声明(以及其他成员)。在这些示例中,我们只显示一个通知声明,以集中显示每个通知声明的效果。
有时,你需要访问通知主体中返回的实际值。你可以使用@AfterReturn的形式绑定返回值以获得该访问,如下示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
returning属性中使用的名称必须与advice方法中参数的名称相对应。当方法执行返回时,返回值作为相应的参数值传递给通知方法。返回子句还限制只匹配那些返回指定类型值的方法执行(在本例中是Object,它匹配任何返回值)。
请注意,在使用after returning advice时,不可能返回完全不同的引用。
After Throwing Advice
当匹配的方法抛出异常退出时,After throwing advice执行,你可以使用@AfterThrowing来声明它:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
通常,你希望只有在抛出给定类型的异常时才运行通知,并且通常还需要访问通知正文中抛出的异常。可以使用throwing属性来限制匹配(如果需要-使用throwable作为异常类型),并将引发的异常绑定到advice参数。以下示例显示了如何执行此操作:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
throwing属性中使用的名称必须与advice方法中参数的名称相对应。当通过引发异常退出方法执行时,异常作为相应的参数值传递给通知方法。throwing子句还限制只匹配那些引发指定类型的异常(在本例中是DataAccessException)的方法执行。
After (Finally) Advice
After (Finally) Advice当匹配的方法执行退出时执行,通过@After注解来定义。After advice 必须能够处理正常返回和异常返回的情况。它通常用于释放资源和类似用途。下面的示例演示如何在finally advice之后使用:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
Around Advice
最后一种advice是around advice的。around advice运行“around”匹配方法的执行。它有机会在方法执行之前和之后都进行工作,并确定何时、如何以及即使该方法真正开始执行。如果需要以线程安全的方式(例如,启动和停止计时器)共享方法执行前后的状态,则通常使用around建议。始终使用满足你要求的最不强大的advice形式(也就是说,如果在around之前不使用around)。
使用@Around注解声明around通知。advice方法的第一个参数必须是ProceedingJoinPoint类型。在通知正文中,对ProceedingJoinPoint调用proceed()会导致执行基础方法。proceed方法也可以传入Object[]。数组中的值在方法执行过程中用作参数。
当用Object[] 调用时,proceed的行为与由aspectj编译器编译的around advice的proceed的行为稍有不同。对于使用传统aspectj语言编写的around advice,传递给process的参数数量必须与传递给around advice的参数数量(而不是基础连接点接受的参数数量)匹配,并且传递给给定参数位置的值在连接点为值所绑定到的实体添加原始值(如果现在没有意义,请不要担心)。Spring采用的方法更简单,更符合其基于代理的、只执行的语义。只有在编译为Spring编写的@Aspectj方面并使用aspectj编译器和weaver的参数时,才需要注意这一区别。有一种方法可以在SpringAOP和AspectJ中编写100%兼容的方面,这将在下面关于建议参数的小节中讨论。
下面的例子展示了如何使用around advice:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
around通知返回的值是方法调用方看到的返回值。例如,一个简单的缓存方面可以从缓存中返回一个值(如果有),如果没有,则调用proceed()。请注意,可以在around建议的主体中调用proceed一次、多次或根本不调用proceed。所有这些都是合法的。
dvice Parameters
Spring提供了完全类型化的advice,这意味着你声明了advice签名中所需的参数(正如前面我们在返回和抛出示例中看到的那样),而不是一直使用Object[] 数组。我们将在本节后面的部分中了解如何使参数和其他上下文值对advice主体可用。首先,我们来看一下如何编写通用advice,以了解advice当前建议的方法。
访问当前JoinPoint
任何advice方法都可以将org.aspectj.lang.joinpoint类型的参数声明为其第一个参数(请注意,需要使用around advice从而声明ProceedingJoinPoint类型作为第一个参数,该参数是JoinPoint的子类)。JoinPoint接口提供了许多有用的方法:
- getArgs(): 返回方法参数
- getThis(): 返回代理对象
- getTarget(): 返回目标对象
- getSignature(): 返回被advice的方法描述
- toString():打印被advice方法的有用描述
传递参数给advice
我们已经看到了如何绑定返回值或异常值(在返回后和抛出advice后使用)。要使参数值对通知主体可用,可以使用参数的绑定形式。如果在args表达式中使用参数名代替类型名,则在调用通知时,相应参数的值将作为参数值传递。一个例子应该更清楚地说明这一点。假设你想要建议以account对象为第一个参数的DAO操作的执行,并且你需要访问通知主体中的帐户。你可以编写以下内容:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
pointcut中的args(account,..)部分有两个主要目的:首先,它将匹配限制为只执行那些方法,其中该方法至少接受一个参数,并且传递给该参数的参数是帐户的实例。其次,它通过account参数使实际的account对象可用于advice。
另一种编写方法是声明一个切入点,该切入点在与连接点匹配时“提供”account对象值,然后从通知中引用命名的切入点。如下所示:
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
代理对象this,目标对象 target,和注解(@within, @target, @annotation, 和 @args) 都可以用类似的方式装订。接下来的两个示例演示如何匹配用@Auditable注解的方法的执行,并提取审计代码:
两个例子中的第一个展示了@Auditable注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @Interface Auditable {
AuditCode value();
}
第二个例子是匹配执行@Auditable方法的advice:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
Advice参数和泛型
SpringAOP可以处理类声明和方法参数中使用的泛型。假设你有一个如下的泛型类型:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
通过将advice参数键入要拦截方法的参数类型,可以将方法类型的拦截限制为某些参数类型:
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
这种方法不适用于泛型集合。因此,不能按以下方式定义切入点:
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
为了实现这一点,我们必须检查集合中的每一个元素,这是不合理的,因为我们也不能决定一般如何处理空值。要实现与此类似的功能,必须键入collection<?>手动检查元素的类型。
确定参数名称
advice调用中的参数绑定依赖于切入点表达式中使用的匹配名称与通知和切入点方法签名中声明的参数名称。在Java反射中,参数名是不可用的,因此Spring AOP使用以下策略来确定参数名称:
- 如果用户已显式指定参数名,则使用指定的参数名。通知和切入点注解都有一个可选的argNames属性,你可以使用它来指定带注解的方法的参数名。这些参数名在运行时可用。下面的示例演示如何使用argNames属性:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}
如果第一个参数是joinpoint、ProceedingJoinPoint或JoinPoint.StaticPart类型,则可以从argnames属性的值中省略参数的名称。例如,如果修改前面的建议以接收连接点对象,argnames属性不需要包括它:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}
joinPoint、proceedingjoinPoint和joinPoint.staticPart类型作为第一个参数的特殊处理对于不收集任何其他连接点上下文的advice实例特别方便。在这种情况下,可以省略argNames属性。例如,以下建议不需要声明argNames属性:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
使用“argNames”属性有点笨拙,因此如果没有指定“argNames”属性,spring aop将查看类的调试信息,并尝试从局部变量表中确定参数名。只要使用调试信息编译了类(至少为-g:vars),就会出现此信息。使用此标志编译的结果是:(1)代码更容易理解(反向工程);(2)类文件的大小稍大(通常不连续);(3)编译器不应用删除未使用的局部变量的优化。换言之,你不应该因为这个flag而遇到困难。
如果aspectj编译器(ajc)编译了@Aspectj aspect,即使没有调试信息,也不需要添加argnames属性,因为编译器保留了所需的信息。
如果在没有必要的调试信息的情况下编译了代码,那么spring aop会尝试推断绑定变量与参数之间的配对(例如,如果切入点表达式中只绑定了一个变量,而advice方法只接受一个参数,那么这种配对是明显的)。如果给定可用信息,变量的绑定不明确,则抛出一个AmbiguousBindingException。
如果上述所有策略都失败,将引发IllegalArgumentException。
Proceeding with Arguments
我们之前提到过,我们将描述如何使用在SpringAOP和AspectJ中一致工作的参数编写一个proceed调用。解决方案是确保通知签名按顺序绑定每个方法参数。以下示例显示了如何执行此操作:
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
在许多情况下,你无论如何都要进行这种绑定(如前面的示例中所示)。
Advice Ordering
当多条advice都想在同一个连接点上运行时会发生什么?SpringAOP遵循与AspectJ相同的优先级规则来确定通知执行的顺序。最高优先级的通知在“on the way in”首先运行(因此,给定两个before advice,最高优先级的advice首先运行)。从连接点“On the way out”时,最高优先级的通知运行在最后(因此,给定两条after advice,最高优先级的通知将运行在第二)。
当在不同aspects定义的两条advice都需要在同一连接点上运行时,除非你另有指定,否则执行顺序是未定义的。你可以通过指定优先级来控制执行顺序。这可以通过在Aspect类中实现org.springframework.core.Ordered接口或使用order注解来以正常的Spring方式完成。给定两个aspects,从ordered.getValue()返回较低值(或注解值)的方面具有较高的优先级。
当在同一aspect定义的两条advice都需要在同一个连接点上运行时,顺序是未定义的(因为没有办法通过对javac编译类的反射来检索声明顺序)。考虑在每个方面类的每个连接点将这些建议方法折叠成一个建议方法,或者将这些建议重构成单独的方面类,你可以在方面级别对它们进行排序。
Introductions(在AspectJ中称为类型间声明)使方面能够声明实现给定的接口的建议对象,并代表这些对象提供该接口的实现。
你可以使用@DeclareParents注解进行介绍。此注解用于声明具有新的父级的匹配类型(因此是名称)。例如,给定一个名为UsageTracked的接口和一个名为DefaultUsageTracked的接口的实现,下面的方面声明服务接口的所有实现者也实现UsageTracked接口(例如,通过jmx公开统计信息):
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
要实现的接口由注解字段的类型决定。@DeclareParents注解的value属性是AspectJ类型的模式。匹配任何实现UsageTracked接口的bean类型。注意,在前面的示例中,服务bean可以直接用作UsageTracked接口的实现。如果以编程方式访问bean,你将编写以下内容:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
这是一个高级主题。如果你刚开始使用AOP,可以安全地跳过它,直到稍后。
默认情况下,应用程序上下文中每个方面都有一个实例。Aspectj将其称为单例实例化模型。可以用交替的生命周期定义方面。Spring支持AspectJ的perthis和pertarget实例化模型(percflow、percflowbelow, 和 pertypewithin当前不受支持)。
你可以通过在@Aspect注解中指定perthis子句来声明perthis方面。请考虑以下示例:
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {
private int someState;
@Before(com.xyz.myapp.SystemArchitecture.businessService())
public void recordServiceUsage() {
// ...
}
}
在前面的示例中,“perthis”子句的效果是为执行业务服务的每个唯一服务对象(在与切入点表达式匹配的连接点处绑定到“this”的每个唯一对象)创建一个方面实例。第一次对服务对象调用方法时,将创建方面实例。当服务对象超出范围时,方面超出范围。在创建方面实例之前,它内的任何advice都不会执行。只要创建了方面实例,它内部声明的advice就在匹配的连接点上执行,但仅当服务对象是与此方面关联的对象时。有关每个子句的详细信息,请参阅《AspectJ编程指南》。
pertarget实例化模型的工作方式与perthis完全相同,但它在匹配的连接点为每个唯一的目标对象创建一个方面实例。
既然你已经了解了所有组成部分的工作原理,那么我们可以将它们放在一起做一些有用的事情。
业务服务的执行有时会由于并发问题(例如,死锁失败者)而失败。如果重试该操作,则下次尝试可能会成功。对于适合在这种情况下重试的业务服务(不需要返回用户进行冲突解决的等量操作),我们希望透明地重试该操作,以避免客户端看到PessimisticLockingFailureException。这是一个需求,它清楚地跨越服务层中的多个服务,因此是通过一个方面实现的理想选择。
因为我们想重试这个操作,所以我们需要使用around建议,这样我们可以多次调用proceed。下面的列表显示了基本方面的实现:
@Aspect
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;
}
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
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;
}
}
注意,aspect实现了Ordered的接口,因此我们可以将方面的优先级设置为高于事务通知(我们希望每次重试时都有一个新的事务)。maxRetries和order属性都由spring配置。主要操作发生在around advice的doConcurrentOperation中。请注意,目前,我们将重试逻辑应用于每个BusinessService()。我们尝试继续,如果我们因PessimisticLockingFailureException而失败,我们会再试一次,除非我们已经用尽了所有的重试尝试。
相应的Spring配置如下:
<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
为了优化方面以便它只重试Idempotent运算,我们可以定义以下Idempotent注解:
@Retention(RetentionPolicy.RUNTIME)
public @Interface Idempotent {
// marker annotation
}
然后,我们可以使用注解来注解服务操作的实现。将方面更改为仅重试idempotent操作涉及到的切入点表达式,以便只有@Idempotent操作匹配,如下所示:
@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
...
}