6.2.4.1. 前置通知(Before advice)
一个切面里使用 @Before 注解声明前置通知:
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() { // ... } }
如果使用一个in-place 的切入点表达式,我们可以把上面的例子换个写法:
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() { // ... } }
返回后通知通常在一个匹配的方法返回的时候执行。使用 @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() { // ... } }说明:你可以在同一个切面里定义多个通知,或者其他成员。我们只是在展示如何定义一个简单的通知。这些例子主要的侧重点是正在讨论的问题。
有时候你需要在通知体内得到返回的值。你可以使用以 @AfterReturning 接口的形式来绑定返回值:
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 属性中使用的名字必须对应于通知方法内的一个参数名。 当一个方法执行返回后,返回值作为相应的参数值传入通知方法。 一个 returning 子句也限制了只能匹配到返回指定类型值的方法。 (在本例子中,返回值是 Object 类,也就是说返回任意类型都会匹配)
抛出后通知在一个方法抛出异常后执行。使用 @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 作为异常类型),还可以将抛出的异常绑定到通知的一个参数上。
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 属性中使用的名字必须与通知方法内的一个参数对应。 当一个方法因抛出一个异常而中止后,这个异常将会作为那个对应的参数送至通知方法。 throwing 子句也限制了只能匹配到抛出指定异常类型的方法(上面的示例为 DataAccessException )。
不论一个方法是如何结束的,在它结束后(finally)后通知(After (finally) advice)都会运行。 使用 @After 注解来声明。这个通知必须做好处理正常返回和异常返回两种情况。通常用来释放资源。
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() { // ... } }
最后一种通知是环绕通知。环绕通知在一个方法执行之前和之后执行。 它使得通知有机会既在一个方法执行之前又在执行之后运行。并且,它可以决定这个方法在什么时候执行,如何执行,甚至是否执行。 环绕通知经常在在某线程安全的环境下,你需要在一个方法执行之前和之后共享某种状态的时候使用。 请尽量使用最简单的满足你需求的通知。(比如如果前置通知(before advice)也可以适用的情况下不要使用环绕通知)。
环绕通知使用 @Around 注解来声明。通知的第一个参数必须是 ProceedingJoinPoint 类型。 在通知体内,调用 ProceedingJoinPoint 的 proceed() 方法将会导致潜在的连接点方法执行。 proceed 方法也可能会被调用并且传入一个 Object[] 对象-该数组将作为方法执行时候的参数。
当传入一个 Object[] 对象的时候,处理的方法与通过AspectJ编译器处理环绕通知略有不同。 对于使用传统AspectJ语言写的环绕通知来说,传入参数的数量必须和传递给环绕通知的参数数量匹配(不是后台的连接点接受的参数数量),并且特定顺序 的传入参数代替了将要绑定给连接点的原始值(如果你看不懂不用担心)。 Spring采用的方法更加简单并且更好得和他的基于代理(proxy-based),只匹配执行的语法相适用。 如果你适用AspectJ的编译器和编织器来编译为Spring而写的@AspectJ切面和处理参数,你只需要了解这一区别即可。 有一种方法可以让你写出100%兼容Spring AOP和AspectJ的,我们将会在后续的通知参数(advice parameters)的章节中讨论它。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; } }
方法的调用者得到的返回值就是环绕通知返回的值。 例如:一个简单的缓存切面,如果缓存中有值,就返回该值,否则调用proceed()方法。 请注意proceed可能在通知体内部被调用一次,许多次,或者根本不被调用。
Spring 2.0 提供了完整的通知类型 - 这意味着你可以在通知签名中声明所需的参数,(就像在以前的例子中我们看到的返回值和抛出异常一样)而不总是使用Object[]。 我们将会看到如何在通知体内访问参数和其他上下文相关的值。首先让我们看以下如何编写普通的通知以找出正在被通知的方法。
任何通知方法可以将第一个参数定义为 org.aspectj.lang.JoinPoint 类型 (环绕通知需要定义为 ProceedingJoinPoint 类型的, 它是 JoinPoint 的一个子类。) JoinPoint 接口提供了一系列有用的方法, 比如 getArgs() (返回方法参数)、getThis() (返回代理对象)、getTarget() (返回目标)、getSignature() (返回正在被通知的方法相关信息)和 toString() (打印出正在被通知的方法的有用信息)。
我们已经看到了如何绑定返回值或者异常(使用后置通知(after returning)和异常后通知(after throwing advice)。 为了可以在通知(adivce)体内访问参数,你可以使用 args 来绑定。 如果在一个参数表达式中应该使用类型名字的地方使用一个参数名字,那么当通知执行的时候对应的参数值将会被传递进来。 可能给出一个例子会更好理解。假使你想要通知(advise)接受某个Account对象作为第一个参数的DAO操作的执行,你想要在通知体内也能访问到 account对象,你可以写如下的代码:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" + "args(account,..)") public void validateAccount(Account account) { // ... }
切入点表达式的 args(account,..) 部分有两个目的: 首先它保证了只会匹配那些接受至少一个参数的方法的执行,而且传入的参数必须是 Account 类型的实例, 其次它使得可以在通知体内通过 account 参数来访问那个account参数。
另外一个办法是定义一个切入点,这个切入点在匹配某个连接点的时候“提供”了一个Account对象, 然后直接从通知中访问那个命名的切入点。你可以这样写:
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" + "args(account,..)") private void accountDataAccessOperation(Account account) {} @Before("accountDataAccessOperation(account)") public void validateAccount(Account account) { // .. }
如果想要知道更详细的内容,请参阅 AspectJ 编程指南。
代理对象(this )、目标对象(target ) 和注解(@within, @target, @annotation, @args )都可以用一种简单格式绑定。 以下的例子展示了如何使用 @Auditable 注解来匹配方法执行,并提取AuditCode。
首先是 @Auditable 注解的定义:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Auditable { AuditCode value(); }
然后是匹配 @Auditable 方法执行的通知:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && " + "@annotation(auditable)") public void audit(Auditable auditable) { AuditCode code = auditable.value(); // ... }
绑定在通知上的参数依赖切入点表达式的匹配名,并借此在(通知(advice)和切入点(pointcut))的方法签名中声明参数名。 参数名 无法 通过Java反射来获取,所以Spring AOP使用如下的策略来决定参数名字:
-
如果参数名字已经被用户明确指定,则使用指定的参数名: 通知(advice)和切入点(pointcut)注解有一个额外的"argNames"属性,该属性用来指定所注解的方法的参数名 - 这些参数名在运行时是 可以 访问的。例子如下:
@Before( value="com.xyz.lib.Pointcuts.anyPublicMethod() && " + "@annotation(auditable)", argNames="auditable") public void audit(Auditable auditable) { AuditCode code = auditable.value(); // ... }
如果一个@AspectJ切面已经被AspectJ编译器(ajc)编译过了,那么就不需要再添加 argNames 参数了,因为编译器会自动完成这一工作。 -
使用 'argNames' 属性有点不那么优雅,所以如果没有指定'argNames' 属性, Spring AOP 会寻找类的debug信息,并且尝试从本地变量表(local variable table)中来决定参数名字。 只要编译的时候使用了debug信息(至少要使用 '-g:vars' ),就可获得这些信息。 使用这个flag编译的结果是: (1)你的代码将能够更加容易的读懂(反向工程), (2)生成的class文件会稍许大一些(不重要的), (3)移除不被使用的本地变量的优化功能将会失效。 换句话说,你在使用这个flag的时候不会遇到任何困难。
-
如果不加上debug信息来编译的话,Spring AOP将会尝试推断参数的绑定。 (例如,要是只有一个变量被绑定到切入点表达式(pointcut expression)、通知方法(advice method)将会接受这个参数, 这是显而易见的)。 如果变量的绑定不明确,将会抛出一个 AmbiguousBindingException 异常。
-
如果以上所有策略都失败了,将会抛出一个 IllegalArgumentException 异常。
和@AspectJ风格一样,基于schema的风格也支持5种通知类型并且两者具有同样的语义。
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中。 在一个数据访问操作执行之前(执行连接点和切入点表达式匹配),切面中的"doAccessCheck"会被调用。
After returning通知在匹配的方法完全执行后运行。和Before通知一样,可以在<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) {...
After throwing通知在匹配方法抛出异常退出时执行。在 <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" thowing="dataAccessEx" method="doRecoveryActions"/> ... </aop:aspect>
doRecoveryActions方法必须声明一个名字为 dataAccessEx 的参数。 参数的类型强制匹配,和先前我们在@AfterThrowing中讲到的一样。例如:方法签名可以如下这般声明:
public void doRecoveryActions(DataAccessException dataAccessEx) {...
After (finally)通知在匹配方法退出后执行。使用 after 元素来声明:
<aop:aspect id="afterFinallyExample" ref="aBean"> <aop:after pointcut-ref="dataAccessOperation" method="doReleaseLock"/> ... </aop:aspect>
Around通知是最后一种通知类型。Around通知在匹配方法运行期的“周围”执行。 它有机会在目标方法的前面和后面执行,并决定什么时候运行,怎么运行,甚至是否运行。 Around通知经常在需要在一个方法执行前或后共享状态信息,并且是线程安全的情况下使用(启动和停止一个计时器就是一个例子)。 注意选择能满足你需求的最简单的通知类型(i.e.如果简单的before通知就能做的事情绝对不要使用around通知)。
Around通知使用 aop:around 元素来声明。 通知方法的第一个参数的类型必须是 ProceedingJoinPoint 类型。 在通知的主体中,调用 ProceedingJoinPoint 的proceed() 方法来执行真正的方法。 proceed 方法也可能会被调用并且传入一个 Object[] 对象 - 该数组将作为方法执行时候的参数。 参见 Section 6.2.4.5, “环绕通知(Around Advice)” 中提到的一些注意点。
<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; }
Schema-based声明风格和@AspectJ支持一样,支持通知的全名形式 - 通过通知方法参数名字来匹配切入点参数。 参见 Section 6.2.4.6, “通知参数(Advice parameters)” 获取详细信息。
如果你希望显式指定通知方法的参数名(而不是依靠先前提及的侦测策略),可以通过 arg-names 属性来实现。示例如下:
<aop:before pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)" method="audit" arg-names="auditable"/>
The arg-names attribute accepts a comma-delimited list of parameter names.
arg-names 属性接受由逗号分割的参数名列表。