如果你无法使用Java 5,或者你比较喜欢使用XML格式,Spring2.0也提供了使用新的"aop"命名空间来定义一个切面。 和使用@AspectJ风格完全一样,切入点表达式和通知类型同样得到了支持,因此在这一节中我们将着重介绍新的 语法并回顾前一节(第 6.2 节 “@AspectJ支持”)对编写一个切入点表达式和绑定通知参数的讨论。
使用本章所介绍的aop命名空间标签,你需要引入附录 A, XML Schema-based configuration中提及的spring-aop schema。
参见第 A.2.7 节 “The aop
schema”了解如何在aop命名空间中引入标签。
在Spring的配置文件中,所有的切面和通知都必须定义在<aop:config>
元素内部。
(一个application context可以包含多个 <aop:config>
)。
一个<aop:config>
可以包含pointcut,advisor和aspect元素
(注意这三个元素必须按照这个顺序进行声明)。
<aop:config>
风格的配置使得Spring
auto-proxying机制的使用变得很笨重。如果你已经通过
BeanNameAutoProxyCreator
或类似的东西显式使用auto-proxying,它可能会导致问题
(例如通知没有被织入)。 推荐的使用模式是仅仅使用<aop:config>
风格,
或者仅仅使用AutoProxyCreator
风格。
有了schema的支持,切面就和常规的Java对象一样被定义成application context中的一个bean。 对象的字段和方法提供了状态和行为信息,XML文件则提供了切入点和通知信息。
切面使用<aop:aspect>来声明,backing bean(支持bean)通过 ref
属性来引用:
<aop:config> <aop:aspect id="myAspect" ref="aBean"> ... </aop:aspect> </aop:config> <bean id="aBean" class="..."> ... </bean>
切面的支持bean(上例中的"aBean
")可以象其他Spring bean一样被容器管理配置以及依赖注入。
一个命名切入点可以在<aop:config>元素中定义,这样多个切面和通知就可以共享该切入点。
一个描述service层中所有service执行的切入点可以定义如下:
<aop:config> <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..))"/> </aop:config>
注意切入点表达式本身使用了与第 6.2 节 “@AspectJ支持”中描述的相同的AspectJ切入点表达式语言。 如果你在Java 5环境下使用基于schema的声明风格,可参考切入点表达式类型(@Aspects)中定义的命名切入点, 不过这个特性在JDK1.4及以下版本中是不可用的(因为依赖于Java 5中的AspectJ反射API)。所以在JDK 1.5中, 上面的切入点的另外一种定义形式如下:
<aop:config> <aop:pointcut id="businessService" expression="com.xyz.myapp.SystemArchitecture.businessService()"/> </aop:config>
假定你有一个在第 6.2.3.3 节 “共享通用切入点定义”中
描述的SystemArchitecture
切面。
在切面里面声明一个切入点和声明一个顶级的切入点非常类似:
<aop:config> <aop:aspect id="myAspect" ref="aBean"> <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..))"/> ... </aop:aspect> </aop:config>
几乎和@AspectJ切面中的一样,使用基于schema定义风格声明的切入点可以收集(collect) 连接点上下文。例如,下面的切入点收集'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来查找,并且不能定义切入点参数。在基于schema的定义风格中 命名切入点支持较之@AspectJ风格受到了很多的限制。
和@AspectJ风格一样,基于schema的风格也支持5种通知类型并且两者具有同样的语义。
前置通知在匹配方法执行前运行。在<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"会被调用。
后置通知在匹配的方法完全执行后运行。和前置通知一样,可以在<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>
环绕通知是最后一种通知类型。环绕通知在匹配方法运行期的“周围”执行。 它有机会在目标方法的前面和后面执行,并决定什么时候运行,怎么运行,甚至是否运行。 环绕通知经常在需要在一个方法执行前后共享状态信息,并且是在线程安全的情况下使用 (启动和停止一个计时器就是一个例子)。注意选择能满足你需求的最简单的通知类型; 如果简单的前置通知能做的事情就绝对不要使用环绕通知。
Around通知使用aop:around
元素来声明。通知方法的第一个参数的类型必须是
ProceedingJoinPoint
类型。在通知的主体中,调用
ProceedingJoinPoint
的proceed()
方法来执行真正的方法。
proceed
方法也可能会被调用并且传入一个Object[]
对象 -
该数组将作为方法执行时候的参数。参见第 6.2.4.5 节 “环绕通知”中调用具有
Object[]
的proceed方法。
<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一样,支持多种类型的通知:通过通知方法参数名字来匹配切入点参数。
参见第 6.2.4.6 节 “通知参数(Advice parameters)”获取详细信息。如果你希望显式指定通知方法的参数名
(而不是依靠先前提及的侦测策略),可以通过通知元素的arg-names
属性来实现,它的处理和
在第 6.2.4.6.3 节 “确定参数名”中所描述的对通知注解中"argNames"属性的处理方式一样。
示例如下:
<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 FooService { Foo getFoo(String fooName, int age); } public class DefaultFooService implements FooService { public Foo getFoo(String name, int age) { return new Foo(name, age); } }
接下来看切面。注意profile(..)
方法接受多个强类型参数,
首先连接点在方法调用时执行,这个参数指明profile(..)
会被用作
环绕
通知:
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 http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"> <!-- this is the object that will be proxied by Spring's AOP infrastructure --> <bean id="fooService" class="x.y.service.DefaultFooService"/> <!-- this is the actual advice itself --> <bean id="profiler" class="x.y.SimpleProfiler"/> <aop:config> <aop:aspect ref="profiler"> <aop:pointcut id="theExecutionOfSomeFooServiceMethod" expression="execution(* x.y.service.FooService.getFoo(String,int)) and args(name, age)"/> <aop:around pointcut-ref="theExecutionOfSomeFooServiceMethod" method="profile"/> </aop:aspect> </aop:config> </beans>
如果我们有下面的驱动脚本,我们将在标准输出上得到如下的输出:
import org.springframework.beans.factory.BeanFactory; import org.springframework.context.support.ClassPathXmlApplicationContext; import x.y.service.FooService; public final class Boot { public static void main(final String[] args) throws Exception { BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml"); FooService foo = (FooService) ctx.getBean("fooService"); foo.getFoo("Pengo", 12); } }
StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0 ----------------------------------------- ms % Task name ----------------------------------------- 00000 ? execution(getFoo)
当同一个切入点(执行方法)上有多个通知需要执行时,执行顺序的规则如
第 6.2.4.7 节 “通知顺序”所述。切面的优先级通过给切面的支持bean增加
Order
注解或者使切面的支持bean实现
Ordered
接口来决定。
引入(在AspectJ中称为inter-type声明)允许一个切面声明一个通知对象实现指定接口, 并且提供了一个接口实现类来代表这些对象。
引入的定义使用aop:aspect
中的aop:declare-parents
元素。
该元素用于声明所匹配的类型有一个新的父类型(所以有了这个名字)。
例如,给定接口UsageTracked
,
以及这个接口的一个实现类 DefaultUsageTracked
,
下面的切面声明所有实现service接口的类同时实现 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.SystemArchitecture.businessService() and this(usageTracked)" method="recordUsage"/> </aop:aspect>
usageTracking
bean的支持类可以包含下面的方法:
public void recordUsage(UsageTracked usageTracked) { usageTracked.incrementUseCount(); }
要实现的接口由implement-interface
属性来指定。
types-matching
属性的值是一个AspectJ类型模式:任何匹配类型的bean都会实现
UsageTracked
接口。注意在上面前置通知的例子中,
serevice bean可以直接用作UsageTracked
接口的实现。
如果以编程形式访问一个bean,你可以这样来写:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
"advisor"这个概念来自Spring1.2对AOP的支持,而在AspectJ中没有等价的概念。 advisor就像一个小的自包含的切面,这个切面只有一个通知。切面自身通过一个bean表示, 并且必须实现一个在第 7.3.2 节 “Spring里的通知类型”中描述的通知接口。 Advisor可以很好的利用AspectJ的切入点表达式。
Spring 2.0通过<aop:advisor>
元素来支持advisor概念。
你将会发现大多数情况下它会和transactional advice一起使用,在Spring 2.0中它有自己的命名空间。其格式如下:
<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
属性来定义一个内联的切入点表达式。
为了定义一个advisor的优先级以便让通知具有次序,使用order
属性来定义advisor中
Ordered
的值 。
让我们看看第 6.2.7 节 “例子”中并发锁失败重试的例子, 当使用schema重写它时是什么样子。
因为并发的问题,有时候business services可能会失败(例如,死锁失败)。如果重试操作,下一次很可能就会成功。
对于business services来说,这种情况下重试是很正常的(Idempotent操作不需要用户参与,否则会得出矛盾的结论)
我们可能需要透明的重试操作以避免客户看到一个OptimisticLockingFailureException
异常。很明显,在一个横切多层的情况下,这是非常有必要的,因此通过切面来实现是很理想的。
因为想要重试操作,我们需要使用环绕通知,这样就可以多次调用proceed()方法。 下面是简单的切面实现(只是一个schema支持的普通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
这个环绕通知方法中发生。
我们首先会尝试处理,如果得到一个OptimisticLockingFailureException
异常,我们仅仅重试直到耗尽所有预设的重试次数。
这个类跟我们在@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>
请注意我们现在假设所有的bussiness services都是idempotent。如果不是这样,我们可以改写切面,
通过引入一个Idempotent
注解,让它只调用idempotent:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
并且对service操作的实现进行注解。这时如果你只希望改变切面重试idempotent操作,
你只需要改写切入点表达式,让其只匹配@Idempotent
操作:
<aop:pointcut id="idempotentOperation" expression="execution(* com.xyz.myapp.service.*.*(..)) and @annotation(com.xyz.myapp.service.Idempotent)"/>