面向切面编程(AOP)经过提供另一种思考程序结构的途经来弥补面向对象编程(OOP)的不足。在OOP中模块化的关键单元是类(classes),而在AOP中模块化的单元则是切面。切面能对关注点进行模块化,例如横切多个类型和对象的事务管理。(在AOP术语中一般称做横切(crosscutting)关注点。)html
AOP框架是Spring的一个重要组成部分。可是Spring IoC容器并不依赖于AOP,这意味着你有权利选择是否使用AOP,AOP作为Spring IoC容器的一个补充,使它成为一个强大的中间件解决方案。java
AOP在Spring Framework中的做用数据库
提供声明式企业服务,特别是为了替代EJB声明式服务。最重要的服务是声明性事务管理。express
容许用户实现自定义切面,用AOP来完善OOP的使用。编程
若是你只打算使用通用的声明式服务或者封装好的声明式中间件服务,例如缓冲池(pooling),那么你没必要与Spring AOP直接打交道,而且本章的大部份内容能够跳过了。api
首先让咱们从一些重要的AOP概念和术语开始。这些术语不是Spring特有的。不过AOP术语并非特别的直观,若是Spring使用本身的术语,将会变得更加使人困惑。数组
切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。在Spring AOP中,切面可使用基于模式)或者基于@Aspect注解的方式来实现。
链接点(Joinpoint):在程序执行过程当中某个特定的点,好比某方法调用的时候或者处理异常的时候。在Spring AOP中,一个链接点老是表示一个方法的执行。
通知(Advice):在切面的某个特定的链接点上执行的动做。其中包括了“around”、“before”和“after”等不一样类型的通知(通知的类型将在后面部分进行讨论)。许多AOP框架(包括Spring)都是以拦截器作通知模型,并维护一个以链接点为中心的拦截器链。
切入点(Pointcut):匹配链接点的断言。通知和一个切入点表达式关联,并在知足这个切入点的链接点上运行(例如,当执行某个特定名称的方法时)。切入点表达式如何和链接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。
引入(Introduction):用来给一个类型声明额外的方法或属性(也被称为链接类型声明(inter-type declaration))。Spring容许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可使用引入来使一个bean实现IsModified接口,以便简化缓存机制。
目标对象(Target Object): 被一个或者多个切面所通知的对象。也被称作被通知(advised)对象。 既然Spring AOP是经过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。
AOP代理(AOP Proxy):AOP框架建立的对象,用来实现切面契约(例如通知方法执行等等)。在Spring中,AOP代理能够是JDK动态代理或者CGLIB代理。
织入(Weaving):把切面链接到其它的应用程序类型或者对象上,并建立一个被通知的对象。这些能够在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其余纯Java AOP框架同样,在运行时完成织入。
通知类型:
前置通知(Before advice):在某链接点以前执行的通知,但这个通知不能阻止链接点以前的执行流程(除非它抛出一个异常)。
后置通知(After returning advice):在某链接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
异常通知(After throwing advice):在方法抛出异常退出时执行的通知。
最终通知(After (finally) advice):当某链接点退出的时候执行的通知(不管是正常返回仍是异常退出)。
环绕通知(Around Advice):包围一个链接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知能够在方法调用先后完成自定义的行为。它也会选择是否继续执行链接点或直接返回它本身的返回值或抛出异常来结束执行。
环绕通知是最经常使用的通知类型。和AspectJ同样,Spring提供全部类型的通知,咱们推荐你使用尽量简单的通知类型来实现须要的功能。例如,若是你只是须要一个方法的返回值来更新缓存,最好使用后置通知而不是环绕通知,尽管环绕通知也能完成一样的事情。用最合适的通知类型可使得编程模型变得简单,而且可以避免不少潜在的错误。好比,你不须要在JoinPoint上调用用于环绕通知的proceed()方法,就不会有调用的问题。
在Spring 2.0中,全部的通知参数都是静态类型,所以你可使用合适的类型(例如一个方法执行后的返回值类型)做为通知的参数而不是使用Object数组。
经过切入点匹配链接点的概念是AOP的关键,这使得AOP不一样于其它仅仅提供拦截功能的旧技术。 切入点使得通知能够独立对应到面向对象的层次结构中。例如,一个提供声明式事务管理 的环绕通知能够被应用到一组横跨多个对象的方法上(例如服务层的全部业务操做)。
Spring AOP使用纯Java实现。它不须要专门的编译过程。Spring AOP不须要控制类装载器层次,所以它适用于J2EE web容器或应用服务器。
Spring目前仅支持使用方法调用做为链接点(join point)(在Spring bean上通知方法的执行)。虽然能够在不影响到Spring AOP核心API的状况下加入对成员变量拦截器支持,但Spring并无实现成员变量拦截器。若是你须要把对成员变量的访问和更新也做为通知的链接点,能够考虑其它的语言,如AspectJ。
Spring实现AOP的方法跟其余的框架不一样。Spring并非要提供最完整的AOP实现(尽管Spring AOP有这个能力),相反的,它其实侧重于提供一种AOP实现和Spring IoC容器之间的整合,用于帮助解决在企业级开发中的常见问题。
所以,Spring的AOP功能一般都和Spring IoC容器一块儿使用。切面使用普通的bean定义语法来配置(尽管Spring提供了强大的"自动代理(autoproxying)"功能):与其余AOP实现相比这是一个显著的区别。有些事使用Spring AOP是没法轻松或者高效完成的,好比说通知一个细粒度的对象(例如典型的域对象):这种时候,使用AspectJ是最好的选择。不过经验告诉咱们,对于大多数在J2EE应用中适合用AOP来解决的问题,Spring AOP都提供了一个很是好的解决方案。
Spring AOP历来没有打算经过提供一种全面的AOP解决方案来与AspectJ竞争。咱们相信不管是基于代理(proxy-based)的框架如Spring AOP或者是成熟的框架如AspectJ都是颇有价值的,他们之间应该是互补而不是竞争的关系。Spring 2.0能够无缝的整合Spring AOP,IoC和AspectJ,使得全部的AOP应用彻底融入基于Spring的应用体系。这样的集成不会影响Spring AOP API或者AOP Alliance API;Spring AOP保持了向下兼容性。下一章会详细讨论Spring AOP的API。
![]() |
Note |
---|---|
Spring Framework一个重要的原则就是无侵入性(non-invasiveness); 这个思想指你不该当被迫引入框架特定的类和接口到你的业务/领域模型中。然而,Spring Framework在某些地方给你一个是否引入Spring框架特定依赖到你的代码的选项: 给你这个选项的理由是由于在特定的场景中它可能仅仅是容易阅读或用这种方法编写特定的功能块。Spring Framework(几乎)一直会为你提供这种选择:从而使你能作出一个明智的决定,使它最适应你的特定用例或场景。 你能够选择AspectJ或者Spring AOP,以及选择是使用@AspectJ注解风格仍是Spring XML配置风格。事实上本章选择先介绍@AspectJ风格的方法不该当被看做是这样一个暗示:Spring小组喜欢@AspectJ注解风格更胜于Spring XML配置。 在Section 6.4, “AOP声明风格的选择”一章有对使用各个风格理由的一个更全面的讨论。 |
Spring缺省使用J2SE 动态代理(dynamic proxies)来做为AOP的代理。 这样任何接口(或者接口集)均可以被代理。
Spring也可使用CGLIB代理. 对于须要代理类而不是代理接口的时候CGLIB代理是颇有必要的。若是一个业务对象并无实现一个接口,默认就会使用CGLIB。做为面向接口编程的最佳实践,业务对象一般都会实现一个或多个接口。但也有可能会强制使用CGLIB,在这种状况(但愿不常有)下,你可能须要通知一个没有在接口中声明的方法,或者须要传入一个代理对象给方法做为具体类型
为了明白Spring AOP是基于代理(proxy-based)的事实,请参阅Section 6.6.1, “理解AOP代理”。
@AspectJ使用了Java 5的注解,能够将切面声明为普通的Java类。@AspectJ样式在AspectJ 5发布的AspectJ project部分中被引入。Spring 2.0使用了和AspectJ 5同样的注解,并使用AspectJ来作切入点解析和匹配。可是,AOP在运行时仍旧是纯的Spring AOP,并不依赖于AspectJ的编译器或者织入器(weaver)。
使用AspectJ的编译器或者织入器的话就可使用完整的AspectJ语言,咱们将在Section 6.8, “在Spring应用中使用AspectJ”中讨论这个问题。为了在Spring配置中使用@AspectJ切面,你首先必须启用Spring对@AspectJ切面配置的支持,并确保自动代理(autoproxying)的bean是否能被这些切面通知。自动代理是指Spring会判断一个bean是否使用了一个或多个切面通知,并据此自动生成相应的代理以拦截其方法调用,而且确保通知在须要时执行。
经过在你的Spring的配置中引入下列元素来启用Spring对@AspectJ的支持:
<aop:aspectj-autoproxy/>
咱们假定你正在使用Appendix A, XML Schema-based configuration所描述的schema支持。关于如何在aop的命名空间中引入这些标签,请参见Section A.2.7, “The aop schema”
若是你正在使用DTD,你仍然能够经过在你的application context中添加以下定义来启用@AspectJ支持:
<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" />
你须要在你的应用程序的classpath中引入两个AspectJ库:aspectjweaver.jar和aspectjrt.jar。这些库能够在AspectJ的安装包(1.5.1或者以后的版本)的'lib'目录里找到,或者也能够在Spring-with-dependencies发布包的'lib/aspectj'目录下找到。
启用@AspectJ支持后,在application context中定义的任意带有一个@Aspect切面(拥有@Aspect注解)的bean都将被Spring自动识别并用于配置Spring AOP。如下例子展现了为完成一个不是很是有用的切面所须要的最小定义:
application context中一个常见的bean定义,它指向一个使用了@Aspect注解的bean类:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of aspect here as normal -->
</bean>
以及NotVeryUsefulAspect类的定义,使用了 org.aspectj.lang.annotation.Aspect注解。
package org.xyz; import org.aspectj.lang.annotation.Aspect; @Aspect public class NotVeryUsefulAspect { }
切面(用@Aspect注解的类)和其余类同样有方法和字段定义。他们也可能包括切入点,通知和引入(inter-type)声明。
![]() |
通知切面 |
---|---|
在Spring AOP中,拥有切面的类自己不多是其它切面中通知的目标。一个类上面的@Aspect注解标识它为一个切面,而且从自动代理中排除它。 |
在前面咱们提到,切入点决定了链接点关注的内容,使得咱们能够控制通知何时执行。Spring AOP只支持Spring bean的方法执行链接点。因此你能够把切入点看作是Spring bean上方法执行的匹配。一个切入点声明有两个部分:一个包含名字和任意参数的签名,还有一个切入点表达式,该表达式决定了咱们关注那个方法的执行。在@AspectJ注解风格的AOP中,一个切入点签名经过一个普通的方法定义来提供,而且切入点表达式使用@Pointcut注解来表示(做为切入点签名的方法必须返回void 类型)。
用一个例子能帮咱们清楚的区分切入点签名和切入点表达式之间的差异,下面的例子定义了一个切入点'anyOldTransfer',这个切入点将匹配任何名为 "transfer" 的方法的执行:
@Pointcut("execution(* transfer(..))")// the pointcut expression private void anyOldTransfer() {}// the pointcut signature
切入点表达式,也就是组成@Pointcut注解的值,是正规的AspectJ 5切入点表达式。若是你想要更多了解AspectJ的切入点语言,请参见AspectJ编程指南(若是要了解基于Java 5的扩展请参阅AspectJ 5 开发手册)或者其余人写的关于AspectJ的书,例如Colyer et. al.著的“Eclipse AspectJ”或者Ramnivas Laddad著的“AspectJ in Action”。
Spring AOP支持在切入点表达式中使用以下的AspectJ切入点指示符:
execution - 匹配方法执行的链接点,这是你将会用到的Spring的最主要的切入点指示符。
within - 限定匹配特定类型的链接点(在使用Spring AOP的时候,在匹配的类型中定义的方法的执行)。
this - 限定匹配特定的链接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。
target - 限定匹配特定的链接点(使用Spring AOP的时候方法的执行),其中目标对象(被代理的应用对象)是指定类型的实例。
args - 限定匹配特定的链接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。
@target - 限定匹配特定的链接点(使用Spring AOP的时候方法的执行),其中正执行对象的类持有指定类型的注解。
@args - 限定匹配特定的链接点(使用Spring AOP的时候方法的执行),其中实际传入参数的运行时类型持有指定类型的注解。
@within - 限定匹配特定的链接点,其中链接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。
@annotation - 限定匹配特定的链接点(使用Spring AOP的时候方法的执行),其中链接点的主题持有指定的注解。
另外,Spring AOP还提供了一个名为'bean'的PCD。这个PCD容许你限定匹配链接点到一个特定名称的Spring bean,或者到一个特定名称Spring bean的集合(当使用通配符时)。'bean' PCD具备下列的格式:
bean(idOrNameOfBean)
'idOrNameOfBean'标记能够是任何Spring bean的名字:限定通配符使用'*'来提供,若是你为Spring bean制定一些命名约定,你能够很是容易地编写一个'bean' PCD表达式将它们选出来。和其它链接点指示符同样,'bean' PCD也支持&&, ||和 !逻辑操做符。
![]() |
Note |
---|---|
请注意'bean' PCD仅仅 被Spring AOP支持而不是AspectJ. 这是Spring对AspectJ中定义的标准PCD的一个特定扩展。 'bean' PCD不只仅能够在类型级别(被限制在基于织入AOP上)上操做而还能够在实例级别(基于Spring bean的概念)上操做。 |
切入点表达式可使用'&', '||' 和 '!'来组合。还能够经过名字来指向切入点表达式。如下的例子展现了三种切入点表达式: anyPublicOperation(在一个方法执行链接点表明了任意public方法的执行时匹配);inTrading(在一个表明了在交易模块中的任意的方法执行时匹配)和 tradingOperation(在一个表明了在交易模块中的任意的公共方法执行时匹配)。
@Pointcut("execution(public * *(..))") private void anyPublicOperation() {} @Pointcut("within(com.xyz.someapp.trading..*") private void inTrading() {} @Pointcut("anyPublicOperation() && inTrading()") private void tradingOperation() {}
如上所示,用更少的命名组件来构建更加复杂的切入点表达式是一种最佳实践。当用名字来指定切入点时使用的是常见的Java成员可视性访问规则。(好比说,你能够在同一类型中访问私有的切入点,在继承关系中访问受保护的切入点,能够在任意地方访问公共切入点)。成员可视性访问规则不影响到切入点的匹配。
当开发企业级应用的时候,你一般会想要从几个切面来引用模块化的应用和特定操做的集合。咱们推荐定义一个“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.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() {} }
示例中的切入点定义了一个你能够在任何须要切入点表达式的地方可引用的切面。好比,为了使service层事务化,你能够写成:
<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>
咱们将在Section 6.3, “基于Schema的AOP支持”中讨论 <aop:config>和<aop:advisor>标签。在Chapter 9, 事务管理中讨论事务标签。
Spring AOP 用户可能会常用 execution切入点指示符。执行表达式的格式以下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
除了返回类型模式(上面代码片段中的ret-type-pattern),名字模式和参数模式之外, 全部的部分都是可选的。返回类型模式决定了方法的返回类型必须依次匹配一个链接点。 你会使用的最频繁的返回类型模式是*,它表明了匹配任意的返回类型。 一个全限定的类型名将只会匹配返回给定类型的方法。名字模式匹配的是方法名。 你可使用*通配符做为全部或者部分命名模式。 参数模式稍微有点复杂:()匹配了一个不接受任何参数的方法, 而(..)匹配了一个接受任意数量参数的方法(零或者更多)。 模式(*)匹配了一个接受一个任何类型的参数的方法。 模式(*,String)匹配了一个接受两个参数的方法,第一个能够是任意类型, 第二个则必须是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包或其子包中的任意链接点(在Spring AOP中只是方法执行):
within(com.xyz.service..*)
实现了AccountService接口的代理对象的任意链接点 (在Spring AOP中只是方法执行):
this(com.xyz.service.AccountService)
'this'在绑定表单中更加经常使用:- 请参见后面的通知一节中了解如何使得代理对象在通知体内可用。
实现AccountService接口的目标对象的任意链接点 (在Spring AOP中只是方法执行):
target(com.xyz.service.AccountService)
'target'在绑定表单中更加经常使用:- 请参见后面的通知一节中了解如何使得目标对象在通知体内可用。
任何一个只接受一个参数,而且运行时所传入的参数是Serializable 接口的链接点(在Spring AOP中只是方法执行)
args(java.io.Serializable)'args'在绑定表单中更加经常使用:- 请参见后面的通知一节中了解如何使得方法参数在通知体内可用。
请注意在例子中给出的切入点不一样于 execution(* *(java.io.Serializable)): args版本只有在动态运行时候传入参数是Serializable时才匹配,而execution版本在方法签名中声明只有一个 Serializable类型的参数时候匹配。
目标对象中有一个 @Transactional 注解的任意链接点 (在Spring AOP中只是方法执行)
@target(org.springframework.transaction.annotation.Transactional)
'@target'在绑定表单中更加经常使用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。
任何一个目标对象声明的类型有一个 @Transactional 注解的链接点 (在Spring AOP中只是方法执行):
@within(org.springframework.transaction.annotation.Transactional)
'@within'在绑定表单中更加经常使用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。
任何一个执行的方法有一个 @Transactional 注解的链接点 (在Spring AOP中只是方法执行)
@annotation(org.springframework.transaction.annotation.Transactional)
'@annotation'在绑定表单中更加经常使用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。
任何一个只接受一个参数,而且运行时所传入的参数类型具备@Classified 注解的链接点(在Spring AOP中只是方法执行)
@args(com.xyz.security.Classified)
'@args'在绑定表单中更加经常使用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。
任何一个在名为'tradeService'的Spring bean之上的链接点 (在Spring AOP中只是方法执行):
bean(tradeService)
任何一个在名字匹配通配符表达式'*Service'的Spring bean之上的链接点 (在Spring AOP中只是方法执行):
bean(*Service)
通知是跟一个切入点表达式关联起来的,而且在切入点匹配的方法执行以前或者以后或者先后运行。 切入点表达式多是指向已命名的切入点的简单引用或者是一个已经声明过的切入点表达式。
一个切面里使用 @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)。
不论一个方法是如何结束的,最终通知都会运行。使用@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() {
// ...
}
}
最后一种通知是环绕通知。环绕通知在一个方法执行以前和以后执行。它使得通知有机会 在一个方法执行以前和执行以后运行。并且它能够决定这个方法在何时执行,如何执行,甚至是否执行。 环绕通知常常在某线程安全的环境下,你须要在一个方法执行以前和以后共享某种状态的时候使用。 请尽可能使用最简单的知足你需求的通知。(好比若是简单的前置通知也能够适用的状况下不要使用环绕通知)。
环绕通知使用@Around注解来声明。通知的第一个参数必须是 ProceedingJoinPoint类型。在通知体内,调用 ProceedingJoinPoint的proceed()方法会致使 后台的链接点方法执行。proceed 方法也可能会被调用而且传入一个 Object[]对象-该数组中的值将被做为方法执行时的参数。
当传入一个Object[]对象的时候,处理的方法与经过AspectJ编译器处理环绕通知略有不一样。 对于使用传统AspectJ语言写的环绕通知来讲,传入参数的数量必须和传递给环绕通知的参数数量匹配 (不是后台的链接点接受的参数数量),而且特定顺序的传入参数代替了将要绑定给链接点的原始值 (若是你看不懂不用担忧)。Spring采用的方法更加简单而且能更好匹配它基于代理(proxy-based)的执行语法, 若是你使用AspectJ的编译器和编织器来编译为Spring而写的@AspectJ切面和处理参数,你只须要知道这一区别便可。 有一种方法可让你写出100%兼容Spring AOP和AspectJ的表达式,咱们将会在后续的通知参数的章节中讨论它。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() (打印出正在被通知的方法的有用信息)。详细的内容请参考JavaDoc。
咱们已经看到了如何绑定返回值或者异常(使用后置通知和异常通知)。为了能够在通知体内访问参数, 你可使用args来绑定。若是在一个args表达式中应该使用类型名字的地方 使用一个参数名字,那么当通知执行的时候对应的参数值将会被传递进来。用一个例子应该会使它变得清晰。 假使你想要通知以一个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注解来匹配方法执行,并提取Audit代码。
首先是@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();
// ...
}
绑定在通知上的参数依赖切入点表达式的匹配名,并借此在(通知和切入点)的方法签名中声明参数名。 参数名没法 经过Java反射来获取,因此Spring AOP使用以下的策略来肯定参数名字:
若是参数名字已经被用户明确指定,则使用指定的参数名: 通知和切入点注解有一个额外的"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类型的参数特殊处理特别适合 没有集合其它链接上下文的通知。在这种情部下,你能够简单的省略“argNames”属性。 例如,下面的通知不须要声明“argNames”属性:
@Before(
"com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
使用'argNames'属性有一点笨拙,因此若是'argNames' 属性没有被指定,Spring AOP将查看类的debug信息并尝试从本地的变量表肯定参数名。只要类编译时有debug信息, (最少要有'-g:vars')这个信息将会出现。打开这个标志编译的结果是: (1)你的代码稍微容易理解(反向工程), (2)class文件的大小稍微有些大(一般不重要), (3)你的编译器将不会应用优化去移除未使用的本地变量。换句话说,打开这个标志建立时你应当不会遇到困难。
若是一个@AspectJ切面已经被AspectJ编译器(ajc)编译过,即便没有debug信息, 也不须要添加argNames参数,由于编译器会保留必需的信息。若是不加上必要的debug信息来编译的话,Spring AOP将会尝试推断绑定变量到参数的配对。 (例如,要是只有一个变量被绑定到切入点表达式,通知方法只接受一个参数, 配对是显而易见的)。 若是变量的绑定不明确,将会抛出一个AmbiguousBindingException异常。
若是以上全部策略都失败了,将会抛出一个IllegalArgumentException异常。
咱们以前提过咱们将会讨论如何编写一个带参数的的proceed()调用, 使得在Spring AOP和AspectJ中都能正常工做。解决方法是仅仅确保通知签名按顺序绑定方法参数。例如:
@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}); }
大多数状况下你都会这样绑定(就像上面的例子那样)。
若是有多个通知想要在同一链接点运行会发生什么?Spring AOP遵循跟AspectJ同样的优先规则来肯定通知执行的顺序。 在“进入”链接点的状况下,最高优先级的通知会先执行(因此给定的两个前置通知中,优先级高的那个会先执行)。 在“退出”链接点的状况下,最高优先级的通知会最后执行。(因此给定的两个后置通知中, 优先级高的那个会第二个执行)。
当定义在不一样的切面里的两个通知都须要在一个相同的链接点中运行, 那么除非你指定,不然执行的顺序是未知的。你能够经过指定优先级来控制执行顺序。 在标准的Spring方法中能够在切面类中实现org.springframework.core.Ordered 接口或者用Order注解作到这一点。在两个切面中, Ordered.getValue()方法返回值(或者注解值)较低的那个有更高的优先级。
当定义在相同的切面里的两个通知都须要在一个相同的链接点中运行, 执行的顺序是未知的(由于这里没有方法经过反射javac编译的类来获取声明顺序)。 考虑在每一个切面类中按链接点压缩这些通知方法到一个通知方法,或者重构通知的片断到各自的切面类中 - 它能在切面级别进行排序。
引入(在AspectJ中被称为inter-type声明)使得一个切面能够定义被通知对象实现给定的接口, 而且能够为那些对象提供具体的实现。
使用@DeclareParents注解来定义引入。这个注解用来定义匹配的类型 拥有一个新的父类(因此有了这个名字)。好比,给定一个接口UsageTracked, 和接口的具体实现DefaultUsageTracked类, 接下来的切面声明了全部的service接口的实现都实现了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的类型模式:- 任何匹配类型的bean都会实现 UsageTracked接口。请注意,在上面的前置通知的例子中,service beans 能够直接用做UsageTracked接口的实现。若是须要编程式的来访问一个bean, 你能够这样写:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
默认状况下,在application context中每个切面都会有一个实例。AspectJ把这个叫作单例化模型。 也能够用其余的生命周期来定义切面:Spring支持AspectJ的 perthis 和pertarget实例化模型(如今还不支持percflow、percflowbelow 和pertypewithin)。
一个"perthis" 切面经过在@Aspect注解中指定perthis 子句来声明。让咱们先来看一个例子,而后解释它是如何运做的:
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {
private int someState;
@Before(com.xyz.myapp.SystemArchitecture.businessService())
public void recordServiceUsage() {
// ...
}
}
这个'perthis'子句的效果是每一个独立的service对象执行一个业务时都会 建立一个切面实例(切入点表达式所匹配的链接点上的每个独立的对象都会绑定到'this'上)。 在service对象上第一次调用方法的时候,切面实例将被建立。切面在service对象失效的同时失效。 在切面实例被建立前,全部的通知都不会被执行,一旦切面对象建立完成, 定义的通知将会在匹配的链接点上执行,可是只有当service对象是和切面关联的才能够。 请参阅 AspectJ 编程指南了解更多关于per-clauses的信息。
'pertarget'实例模型的跟“perthis”彻底同样,只不过是为每一个匹配于链接点 的独立目标对象建立一个切面实例。
如今你已经看到了每一个独立的部分是如何运做的了,是时候把他们放到一块儿作一些有用的事情了!
由于并发的问题,有时候业务服务(business services)可能会失败(例如,死锁失败)。若是从新尝试一下, 颇有可能就会成功。对于业务服务来讲,重试几回是很正常的(Idempotent操做不须要用户参与,不然会得出矛盾的结论) 咱们可能须要透明的重试操做以免客户看到一个PessimisticLockingFailureException异常。 很明显,在一个横切多层的状况下,这是很是有必要的,所以经过切面来实现是很理想的。
由于咱们想要重试操做,咱们会须要使用到环绕通知,这样咱们就能够屡次调用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; } }
请注意切面实现了 Ordered 接口,这样咱们就能够把切面的优先级设定为高于事务通知 (咱们每次重试的时候都想要在一个全新的事务中进行)。maxRetries和order属性均可以在Spring中配置。主要的动做在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
}
而且对service操做的实现进行注解。为了只重试idempotent操做,切面的修改只须要改写切入点表达式, 使得只匹配@Idempotent操做:
@Around("com.xyz.myapp.SystemArchitecture.businessService() && " + "@annotation(com.xyz.myapp.service.Idempotent)") public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { ... }
若是你没法使用Java 5,或者你比较喜欢使用XML格式,Spring2.0也提供了使用新的"aop"命名空间来定义一个切面。 和使用@AspectJ风格彻底同样,切入点表达式和通知类型一样获得了支持,所以在这一节中咱们将着重介绍新的 语法并回顾前一节(Section 6.2, “@AspectJ支持”)对编写一个切入点表达式和绑定通知参数的讨论。
使用本章所介绍的aop命名空间标签,你须要引入Appendix A, XML Schema-based configuration中说起的spring-aop schema。 参见Section A.2.7, “The aop schema”了解如何在aop命名空间中引入标签。
在Spring的配置文件中,全部的切面和通知都必须定义在<aop:config>元素内部。 (一个application context能够包含多个 <aop:config>)。 一个<aop:config>能够包含pointcut,advisor和aspect元素 (注意这三个元素必须按照这个顺序进行声明)。
![]() |
Warning |
---|---|
<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>
注意切入点表达式自己使用了与Section 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>
假定你有一个在Section 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[]对象 - 该数组将做为方法执行时候的参数。参见Section 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同样,支持多种类型的通知:经过通知方法参数名字来匹配切入点参数。 参见Section 6.2.4.6, “通知参数(Advice parameters)”获取详细信息。若是你但愿显式指定通知方法的参数名 (而不是依靠先前说起的侦测策略),能够经过通知元素的arg-names属性来实现,它的处理和 在Section 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)
当同一个切入点(执行方法)上有多个通知须要执行时,执行顺序的规则如 Section 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表示, 而且必须实现一个在Section 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的值 。
让咱们看看Section 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)"/>
当你肯定切面是实现一个给定需求的最佳方法时,你如何选择是使用Spring AOP仍是AspectJ,以及选择 Aspect语言(代码)风格、@AspectJ声明风格或XML风格?这个决定会受到多个因素的影响,包括应用的需求、 开发工具和小组对AOP的精通程度。
作能起做用的最简单的事。Spring AOP比彻底使用AspectJ更加简单, 由于它不须要引入AspectJ的编译器/织入器到你开发和构建过程当中。 若是你仅仅须要在Spring bean上通知执行操做,那么Spring AOP是合适的选择。 若是你须要通知domain对象或其它没有在Spring容器中管理的任意对象,那么你须要使用AspectJ。 若是你想通知除了简单的方法执行以外的链接点(如:调用链接点、字段get或set的链接点等等), 也须要使用AspectJ。
当使用AspectJ时,你能够选择使用AspectJ语言(也称为“代码风格”)或@AspectJ注解风格。 很显然,若是你用的不是Java 5+那么结论是你只能使用代码风格。 若是切面在你的设计中扮演一个很大的角色,而且你能在Eclipse中使用AspectJ Development Tools (AJDT), 那么首选AspectJ语言 :- 由于该语言专门被设计用来编写切面,因此会更清晰、更简单。若是你没有使用 Eclipse,或者在你的应用中只有不多的切面并无做为一个主要的角色,你或许应该考虑使用@AspectJ风格 并在你的IDE中附加一个普通的Java编辑器,而且在你的构建脚本中增长切面织入(连接)的段落。
若是你选择使用Spring AOP,那么你能够选择@AspectJ或者XML风格。显然若是你不是运行 在Java 5上,XML风格是最佳选择。对于使用Java 5的项目,须要考虑多方面的折衷。
XML风格对现有的Spring用户来讲更加习惯。它可使用在任何Java级别中 (参考链接点表达式内部的命名链接点,虽然它也须要Java 5+) 而且经过纯粹的POJO来支持。当使用AOP做为工具来配置企业服务时XML会是一个很好的选择。 (一个好的例子是当你认为链接点表达式是你的配置中的一部分时,你可能想单独更改它) 对于XML风格,从你的配置中能够清晰的代表在系统中存在那些切面。
XML风格有两个缺点。第一是它不能彻底将需求实现的地方封装到一个位置。 DRY原则中说系统中的每一项知识都必须具备单1、无歧义、权威的表示。 当使用XML风格时,如何实现一个需求的知识被分割到支撑类的声明中以及XML配置文件中。 当使用@AspectJ风格时就只有一个单独的模块 -切面- 信息被封装了起来。 第二是XML风格同@AspectJ风格所能表达的内容相比有更多的限制:仅仅支持"singleton"切面实例模型, 而且不能在XML中组合命名链接点的声明。例如,在@AspectJ风格中咱们能够编写以下的内容:
@Pointcut(execution(* get*())) public void propertyAccess() {} @Pointcut(execution(org.xyz.Account+ *(..)) public void operationReturningAnAccount() {} @Pointcut(propertyAccess() && operationReturningAnAccount()) public void accountPropertyAccess() {}
在XML风格中能声明开头的两个链接点:
<aop:pointcut id="propertyAccess" expression="execution(* get*())"/> <aop:pointcut id="operationReturningAnAccount" expression="execution(org.xyz.Account+ *(..))"/>
可是不能经过组合这些来定义accountPropertyAccess链接点
@AspectJ风格支持其它的实例模型以及更丰富的链接点组合。它具备将切面保持为一个模块单元的优势。 还有一个优势就是@AspectJ切面能被Spring AOP和AspectJ二者都理解 - 因此若是稍后你认为你须要AspectJ的能力去实现附加的需求,那么你很是容易迁移到基于AspectJ的途径。 总而言之,咱们更喜欢@AspectJ风格只要你有切面去作超出简单的“配置”企业服务以外的事情。
咱们彻底能够混合使用如下几种风格的切面定义:使用自动代理的@AspectJ风格的切面, schema-defined <aop:aspect>的切面,和用 <aop:advisor>声明的advisor,甚至是使用Spring 1.2风格的代理和拦截器。 因为以上几种风格的切面定义的都使用了相同的底层机制,所以能够很好的共存。
Spring AOP部分使用JDK动态代理或者CGLIB来为目标对象建立代理。(建议优先使用JDK的动态代理)
若是被代理的目标对象实现了至少一个接口,则会使用JDK动态代理。全部该目标类型实现的接口都将被代理。 若该目标对象没有实现任何接口,则建立一个CGLIB代理。
若是你但愿强制使用CGLIB代理,(例如:但愿代理目标对象的全部方法,而不仅是实现自接口的方法) 那也能够。可是须要考虑如下问题:
没法通知(advise)Final方法,由于他们不能被覆写。
你须要将CGLIB 2二进制发行包放在classpath下面,与之相较JDK自己就提供了动态代理。 当须要CGLIB而在classpath下又没有找到CGLIB类库的话,Spring会自动提醒。
代理对象的构造器会被调用两次。这是很天然的结果由于在CGLIB代理模式下每个代理对象都会 产生一个子类。每个代理实例会生成两个对象:实际代理对象和它的一个实现了通知的子类实例 而是用JDK代理时不会出现这样的行为。一般状况下,调用代理类型的构造器两次并非问题, 由于除了会发生指派外没有任何真正的逻辑被实现。
强制使用CGLIB代理须要将<aop:config>的proxy-target-class 属性设为true:
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>
当使用@AspectJ自动代理时要强制使用CGLIB,请将<aop:aspectj-autoproxy> 的proxy-target-class属性设置为true:
<aop:aspectj-autoproxy proxy-target-class="true"/>
![]() |
Note |
---|---|
多个<aop:config/>片断在运行时被包含到一个统一的自动代理构造器中, 它为任何<aop:config/>片断(通常来自不一样的XML bean定义文件)中指定的内容应用 最强的代理设置。此设置一样也适用于<tx:annotation-driven/> 和<aop:aspectj-autoproxy/>元素。 清楚地讲,在<tx:annotation-driven/>、 <aop:aspectj-autoproxy/>或者<aop:config/> 元素上使用'proxy-target-class="true"'会致使将CGLIB代理应用于此三者之上。 |
Spring AOP是基于代理机制的。实际上在你编写本身的切面或者 使用任何由Spring框架提供的基于Spring AOP切面以前,深入领会这一句的意思是很是重要的。
考虑以下场景,当你拿到一个无代理的、无任何特殊之处的POJO对象引用时,如如下代码段所示
public class SimplePojo implements Pojo { public void foo() { // this next method invocation is a direct call on the 'this' reference this.bar(); } public void bar() { // some logic... } }
当你调用一个对象引用的方法时,此对象引用上的方法直接被调用,以下所示
public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// this is a direct method call on the 'pojo' reference
pojo.foo();
}
}
当客户代码所持有的引用是一个代理的时候则略有不一样了。请考虑以下图示和代码段片段
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
理解此处的关键是Main类main(..)方法中的客户代码 拥有一个代理的引用。这意味着对这个对象引用中方法的调用就是对代理的调用, 而这个代理可以代理全部跟特定方法调用相关的拦截器。不过,一旦调用最终抵达了目标对象 (此处为SimplePojo类的引用),任何对自身的调用例如 this.bar()或者this.foo() 将对this引用进行调用而非代理。这一点意义重大, 它意味着自我调用将不会致使和方法调用关联的通知获得执行的机会。
那好,为此要怎么办呢?最好的办法(这里使用最好这个术语不甚精确)就是重构你的代码使自我调用不会出现。 固然,这的确须要你作一些工做,但倒是最好的,最少侵入性的方法。另外一个方法则很可怕, 也正由于如此我几乎不肯指出这种方法。你能够象以下这样彻底把业务逻辑写在你的Spring AOP类中:
public class SimplePojo implements Pojo { public void foo() { // this works, but... gah! ((Pojo) AopContext.currentProxy()).bar(); } public void bar() { // some logic... } }
这样彻底将你的代码交给了Spring AOP,而且让类自己知道它正被用于一个AOP的上下文中, 而它其中的文件直接面对AOP。当代理在被建立时也须要一些额外的配置:
public class Main { public static void main(String[] args) { ProxyFactory factory = new ProxyFactory(new SimplePojo()); factory.adddInterface(Pojo.class); factory.addAdvice(new RetryAdvice()); factory.setExposeProxy(true); Pojo pojo = (Pojo) factory.getProxy(); // this is a method call on the proxy! pojo.foo(); } }
最后,必须注意AspectJ不存在这种自我调用的问题,由于它并非一个基于代理的AOP框架。
除了在配置文件中使用<aop:config>或者<aop:aspectj-autoproxy>来声明切面。 一样能够经过编程方式来建立代理来通知目标对象。关于Spring AOP API的详细介绍, 请参看下一章。这里咱们重点介绍自动建立代理。
类org.springframework.aop.aspectj.annotation.AspectJProxyFactory能够为一个或多个 @AspectJ切面通知的目标对象建立一个代理。该类的基本用法很是简单,示例以下。请参阅Javadoc获取更详细的信息。
// create a factory that can generate a proxy for the given target object AspectJProxyFactory factory = new AspectJProxyFactory(targetObject); // add an aspect, the class must be an @AspectJ aspect // you can call this as many times as you need with different aspects factory.addAspect(SecurityManager.class); // you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect factory.addAspect(usageTracker); // now get the proxy object... MyInterfaceType proxy = factory.getProxy();
到目前为止本章讨论的一直是纯Spring AOP。在这一节里面咱们将介绍如何使用AspectJ compiler/weaver 来代替Spring AOP或者做为它的补充,由于有些时候Spring AOP单独提供的功能也许并不能知足你的须要。
Spring提供了一个小巧的AspectJ aspect library,你能够在程序发行版本中单独使用 spring-aspects.jar文件,并将其加入到classpath下以使用其中的切面。Section 6.8.1, “在Spring中使用AspectJ进行domain object的依赖注入”和Section 6.8.2, “Spring中其余的AspectJ切面” 讨论了该库以及如何使用该库。 Section 6.8.3, “使用Spring IoC来配置AspectJ的切面”讨论了如何对经过AspectJ compiler织入的AspectJ切面进行依赖注入。 最后Section 6.8.4, “在Spring应用中使用AspectJ加载时织入(LTW)”介绍了使用AspectJ的Spring应用程序如何进行加载期织入(load-time weaving)。
Spring容器对application context中定义的bean进行实例化和配置。一样也能够经过bean factory 来为一个已经存在且已经定义为spring bean的对象应用所包含的配置信息。 spring-aspects.jar中包含了一个annotation-driven的切面, 提供了能为任何对象进行依赖注入的能力。这样的支持旨在为 脱离容器管理而建立的对象进行依赖注入。领域对象常常处于这样的情形: 它们多是经过new操做符建立的对象,也多是由ORM工具查询数据库所返回的结果。
@Configurable注解标记了一个类能够经过Spring-driven方式来配置。 在最简单的状况下,咱们只把它看成标记注解:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable
public class Account {
// ...
}
当只是简单地做为一个标记接口来使用的时候,Spring将采用和该已注解的类型 (好比Account类)全名(com.xyz.myapp.domain.Account) 一致的bean原型定义来配置一个新实例。因为一个bean默认的名字就是它的全名, 因此一个比较方便的办法就是省略定义中的id属性:
<bean class="com.xyz.myapp.domain.Account" scope="prototype"> <property name="fundsTransferService" ref="fundsTransferService"/> </bean>
若是你但愿明确的指定bean原型定义的名字,你能够在注解中直接定义:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable("account")
public class Account {
// ...
}
Spring会查找名字为"account"的bean定义,并使用它做定义来配置一个新的 Account实例。
你也可使用自动装配来避免手工指定原型定义的名字。只要设置@Configurable 注解中的autowire属性就可让Spring进行自动装配: 指定@Configurable(autowire=Autowire.BY_TYPE)或者 @Configurable(autowire=Autowire.BY_NAME可让自动装配分别按照类型或名字进行。 做为另一种选择,在Spring2.5中最好是在域或方法级使用@Autowired和 @Resource为你的@Configurable beans指定 明确的、注解驱动的依赖注入。(详情请参看Section 3.11, “基于注解(Annotation-based)的配置”)
最后,你能够经过使用dependencyCheck 属性,让Spring对新建立和配置的对象的对象引用进行 依赖检查(例如:@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))。 若是这个属性设置为true,Spring会在配置结束后校验(除了primitives和collections类型) 全部的属性是否都被设置。
仅仅使用注解并无作任何事情。可是spring-aspects.jar 中的AnnotationBeanConfigurerAspect会在注解存在时起做用。实质上切面指明: “在初始化一个由@Configurable 注解的新对象时, Spring按照注解中的属性来配置这个新建立的对象”。这种状况下,initialization 指新初始化的(好比用new初始化)的对象以及能进行反序列化的 Serializable对象(例如经过 readResolve()方法)。
![]() |
Note |
---|---|
在上一段中一个关键的阶段就是“inessence”。多数状况下,“ 当从一个新对象初始化返回以后”的精确语义很不错...这种语境下, “初始化以后”的意思是依赖将在对象被构造以后注入 - 这意味着在类的构造器块中依赖将不可用。若是你但愿它能在构造器代码块执行 以前被注入,并从而在构造器中使用它, 那么你须要在@Configurable接口声明上作相似的定义: @Configurable(preConstruction=true) 你能够在 AspectJ Programming Guide一书的附录中 找到更多有关在AspectJ中各类切面类型的语义信息。 |
要实现上述的操做,已注解的类型必须由AspectJ weaver来织入 - 你可使用一个构建时的ant/maven任务来完成 (参见AspectJ Development Environment Guide)或者使用加载时织入(参见 Section 6.8.4, “在Spring应用中使用AspectJ加载时织入(LTW)”)。 类AnnotationBeanConfigurerAspect自己也须要Spring来配置(得到bean factory的引用,使用bean factory配置新的对象)。为此Spring的 context命名空间 定义了一个很是方便的标签。只要简单的在application context配置中包含下面的内容。
<context:spring-configured/>
若是你使用DTD代替Schema,对应的定义以下:
<bean class="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect" factory-method="aspectOf"/>
在切面配置完成以前建立的@Configurable 对象实例会致使在log中留下一个warning,而且任何对于该对象的配置都不会生效。 举一个例子,一个Spring管理配置的bean在被Spring初始化的时候建立了一个domain object。 对于这样的状况,你须要定义bean属性中的"depends-on"属性来手动指定该bean依赖于configuration切面。
<bean id="myService"
class="com.xzy.myapp.service.MyService"
depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">
<!-- ... -->
</bean>
提供@Configurable支持的一个目的就是使得domain object的单元测试 能够独立进行,不须要经过硬编码查找各类倚赖关系。若是@Configurable 类型没有经过AspectJ织入,则在单元测试过程当中注解不会起到任何做用, 测试中你能够简单的为对象的mock或者stub属性赋值,而且和正常状况同样去使用该对象。 若是@Configurable类型经过AspectJ织入, 咱们依然能够脱离容器进行单元测试,不过每次建立一个新的@Configurable 对象时都会看到一个warning,标示该对象没有被Spring配置。
AnnotationBeanConfigurerAspect经过一个AspectJ singleton切面来实现对 @Configurable的支持。一个singleton切面的做用域和一个 静态变量的做用域是同样的,那就是说,对于每个classloader有一个切面来定义类型。 这就意味着若是你在一个classloader层次结构中定义了多个application context的时候就须要考虑 在哪里定义<aop:spring-configured/> bean和在哪一个classpath下 放置spring-aspects.jar。
考虑一下典型的Spring web项目,通常都是由一个父application context定义大部分business service和 所须要的其余资源,而后每个servlet拥有一个子application context定义。全部这些context共存于 同一个classloader体系下,所以AnnotationBeanConfigurerAspect仅保持 一个对context的引用。在这样的状况下,咱们推荐在父application context中定义 <aop:spring-configured/> bean:这里所定义的service多是 你但愿注入domain object的。这样作的结果是你不能为子application context中 使用@Configurable的domain object配置bean引用(可能你也根本就不但愿那么作!)。
当在一个容器中部署多个web-app的时候,请确保每个web-application使用本身的classloader 来加载spring-aspects.jar中的类(例如将spring-aspects.jar放在WEB-INF/lib目录下)。 若是spring-aspects.jar被放在了容器的classpath下(所以也被父classloader加载),则全部的 web application将共享一个aspect实例,这可能并非你所想要的。
除了@Configurable切面, spring-aspects.jar包含了一个AspectJ切面能够用来为 那些使用了@Transactional注解的类型和方法驱动Spring事务管理。 提供这个的主要目的是有些用户但愿脱离Spring容器使用Spring的事务管理。
解析@Transactional注解的切面是 AnnotationTransactionAspect。当使用这个切面时, 你必须注解这个实现类(和/或这个类中的方法),而不是 这个类实现的接口(若是有)。AspectJ容许在接口上注解的Java规则 不被继承。
类之上的一个@Transactional注解为该类中任何 public操做的执行指定了默认的事务语义。
类内部方法上的一个@Transactional注解会覆盖类注解(若是存在) 所给定的默认的事务语义。具备public、protected和default修饰符的方法均可以被注解。 直接注解protected和default方法是让这个操做的执行得到事务划分的惟一途径。
对于AspectJ程序员,但愿使用Spring管理配置和事务管理支持,不过他们不想(或者不能)使用注解, spring-aspects.jar也包含了一些抽象 切面供你继承来提供你本身的切入点定义。参见AbstractBeanConfigurerAspect 和AbstractTransactionAspect的Javadoc获取更多信息。 做为一个例子,下面的代码片段展现了如何编写一个切面,而后经过和类全名匹配的bean原型定义来 配置domian object中定义的全部实例:
public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {
public DomainObjectConfiguration() {
setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
}
// the creation of a new bean (any object in the domain model)
protected pointcut beanCreation(Object beanInstance) :
initialization(new(..)) &&
SystemArchitecture.inDomainModel() &&
this(beanInstance);
}
当在Spring application中使用AspectJ的时候,很天然的会想到用Spring来管理这些切面。 AspectJ runtime自身负责切面的建立,这意味着经过Spring来管理AspectJ 建立切面依赖于切面所使用的AspectJ instantiation model(per-clause)。
大多数AspectJ切面都是singleton切面。管理这些切面很是容易, 和一般同样建立一个bean定义引用该切面类型就能够了,而且在bean定义中包含 'factory-method="aspectOf"'这个属性。 这确保Spring从AspectJ获取切面实例而不是尝试本身去建立该实例。示例以下:
<bean id="profiler" class="com.xyz.profiler.Profiler"
factory-method="aspectOf">
<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
non-singleton切面的配置稍难一点,然而它能够经过定义一个bean原型定义而且使用 spring-aspects.jar中的@Configurable支持, 当切面实例由AspectJ runtime建立后进行配置。
若是你但愿一些@AspectJ切面使用AspectJ来织入(例如使用load-time织入domain object) 而另外一些@AspectJ切面使用Spring AOP,而且这些切面都由Spring来管理,那你就须要告诉Spring AOP @AspectJ自动代理支持那些切面须要被自动代理。你能够经过在 <aop:aspectj-autoproxy>声明中使用一个或多个 <include/>元素。每一个元素指定了一种命名格式, 只有bean命名至少符合其中一种状况下才会使用Spring AOP自动代理配置:
<aop:aspectj-autoproxy> <aop:include name="thisBean"/> <aop:include name="thatBean"/> </aop:aspectj-autoproxy>
![]() |
Note |
---|---|
不要被<aop:aspectj-autoproxy/>元素的名字所误导: 用它会致使Spring AOP 代理的建立。在这中只是使用@AspectJ 类型的切面声明,但并不会涉及AspectJ运行时。 |
加载时织入(Load-time weaving(LTW))指的是在虚拟机载入字节码文件时动态织入AspectJ切面。 本 节关注于在Spring Framework中特的定context下配置和使用LTW:并无LTW的介绍。 关于LTW和仅使用AspectJ配置LTW的详细信息(根本不涉及Spring),请查看 LTW section of the AspectJ Development Environment Guide。
Spring框架的值添加为AspectJ LTW在动态织入过程当中提供了更细粒度的控制。使用Java(5+)的代理 能使用一个叫‘Vanilla’的AspectJ LTW,这须要在启动JVM的时候将某个VM参数设置为开。 这种JVM范围的设置在一些状况下或许不错,但一般状况下显得有些粗颗粒。而用Spring的LTW能让你在 per-ClassLoader的基础上打开LTW, 这显然更加细粒度而且对“单JVM多应用”的环境更具意义(例如在一个典型应用服务器环境中同样)。
另外,在某些环境下,这能让你使用LTW而 不对应用服务器的启动脚本作任何改动,否则则须要添加 -javaagent:path/to/aspectjweaver.jar或者(如下将会说起的)-javaagent:path/to/spring-agent.jar。 开发人员只需简单修改应用上下文的一个或几个文件就能使用LTW,而不需依靠那些管理着部署配置 好比启动脚本的系统管理员。
通过以上讲解以后,先让咱们来过一遍一个使用Spring的AspectJ LTW的快速示例,接着是一个 有对元素详细讲解的示例。若是想要一个完整的示例,请参看Petclinic(宠物诊所)的应用实例。
假设你是一个应用开人员,被指派诊断一个系统的若干性能问题。与其拿出性能分析工具, 咱们不如开启一个简单的分析切面,使咱们能很快地获得一些性能指标,这样咱们就能立刻 针对特定区域使用一些较细粒度的分析工具。
这就是一个分析切面。没什么特别的,只是一个快餐式的基于时间的模拟分析器, 使用类@AspectJ风格的切面声明。
package foo; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Pointcut; import org.springframework.util.StopWatch; import org.springframework.core.annotation.Order; @Aspect public class ProfilingAspect { @Around("methodsToBeProfiled()") public Object profile(ProceedingJoinPoint pjp) throws Throwable { StopWatch sw = new StopWatch(getClass().getSimpleName()); try { sw.start(pjp.getSignature().getName()); return pjp.proceed(); } finally { sw.stop(); System.out.println(sw.prettyPrint()); } } @Pointcut("execution(public * foo..*.*(..))") public void methodsToBeProfiled(){} }
咱们还须要建立一个“META-INF/aop.xml”文件,以告知AspectJ weaver 咱们要把ProfilingAspect织入到类中。这个文件惯例,即在Java classpath中 出现一个文件称做“META-INF/aop.xml”是标准的AspectJ。
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd"> <aspectj> <weaver> <!-- only weave classes in our application-specific packages --> <include within="foo.*"/> </weaver> <aspects> <!-- weave in just this aspect --> <aspect name="foo.ProfilingAspect"/> </aspects> </aspectj>
如今来看Spring特定的配置部分。咱们须要配置一个LoadTimeWeaver (稍后会有解释,暂时很少深究)。当将一个或多个“META-INF/aop.xml”文件中的切面 配置织入你的应用程序的类中时,这个加载时织入器是必须的。这样的好处是不须要不少的配置, 正以下面你看到的同样(还有另一些参数供你指定,咱们将在后面详细介绍)。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <!-- a service object; we will be profiling it's methods --> <bean id="entitlementCalculationService" class="foo.StubEntitlementCalculationService"/> <!-- this switches on the load-time weaving --> <context:load-time-weaver/> </beans>
如今万事俱备 - 切面,META-INF/aop.xml文件,以及Spring的配置 - 让咱们建立一个带有main(..)方法的简单驱动类来演示LTW的做用吧。
package foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);
EntitlementCalculationService entitlementCalculationService
= (EntitlementCalculationService) ctx.getBean("entitlementCalculationService");
// the profiling aspect is 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}
最后还有一件事要作。此节以前的介绍说过能够有选择性的基于Spring的 per-ClassLoader来启动LTW,并且的确如此。不过,对此例来讲, 咱们将使用Java代理(由Spring提供)来启动LTW。这个就是用以运行上面Main 类的命令行语句:
java -javaagent:C:/projects/foo/lib/global/spring-agent.jar foo.Main
-javaagent是一个Java 5+标记,用来指定和激活 使JVM上的程序运行的代理。Spring框架装载了一个InstrumentationSavingAgent 代理,在上面的例子中被做为了-javaagent参数的值打包在 spring-agent.jar中。
Main程序运行的输出以下所示。(我已经在 calculateEntitlement()的实现中插入了Thread.sleep(..) 语句,以避免让模拟分析器获取0毫秒 - 这里的01234毫秒并不是是AOP引入的系统开销。)
Calculating entitlement StopWatch 'ProfilingAspect': running time (millis) = 1234 ------ ----- ---------------------------- ms % Task name ------ ----- ---------------------------- 01234 100% calculateEntitlement
由于这个LTW使用成熟的AspectJ,咱们并不局限于通知Spring beans的方法;接下来这个稍有变化的 Main程序将生成一样的结果。
package foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Main {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("beans.xml", Main.class);
EntitlementCalculationService entitlementCalculationService =
new StubEntitlementCalculationService();
// the profiling aspect will be 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}
注意以上程序咱们只是引导了Spring容器,而后彻底在Spring上下文以外建立了一个 StubEntitlementCalculationService的实例...分析通知仍然获得织入。
上面的例子虽然简单了些,但Spring中基本的LTW支持都已介绍完了, 此节余下内容将对使用这些配置和用法背后的理由做详细解释。
![]() |
Note |
---|---|
类ProfilingAspect在此例中虽然基本可是颇为有用。这是一个很好的开发时切面的例子,开发者能够在开发过程当中使用它(废话), 而后也能从已部署到UAT或者生产环境的应用中轻易的脱离。 |
你在LTW中使用的切面必须是AspectJ切面。你可使用AspectJ语言或者类@AspectJ风格来编写你的切面。 后一种方式固然只能在Java 5+中使用,但它说明了你的切面能够同时对AspectJ和Spring AOP切面有效。 此外,编译后的切面类须要被注册到classpath下。
AspectJ LTW的基础设施是用一个或多个位于Java classpath上的(能够是直接的文件形式, 也能够是更典型的jar包形式)META-INF/aop.xml文件配置起来的。
有关文件的结构和内容都在AspectJ的参考文档中有详细介绍,有兴趣的读者 请参考这些资源。(很庆幸这一节比较简短,但aop.xml文件 是100% AspectJ的 - 没有任何使用Spring特定的信息或语义,所以我也没有什么可贡献的。 与其重写这些已由AspectJ开发者提供的使人满意的章节,我不如领你到这里。)
你至少须要如下类库来让Spring框架支持AspectJ LTW:
spring.jar(2.5或更高版本)
aspectjrt.jar (1.5或更高版本)
aspectjweaver.jar (1.5或更高版本)
若是你正在使用 由Spring提供的代理来激活检测(instrumentation)功能,你会须要:
spring-agent.jar
Spring LTW功能的关键组件是LoadTimeWeaver接口 (在org.springframework.instrument.classloading包中), 以及Spring分发包中大量的实现。LoadTimeWeaver的实现负责 在运行时把一个或多个java.lang.instrument.ClassFileTransformers类添加到 ClassLoader中,这能产生各类各样有趣的应用,LTW切面刚好即是其中之一。
![]() |
Tip |
---|---|
若是你对运行时类文件变换的思想还不熟悉,推荐你在继续以前阅读 java.lang.instrument包的Javadoc API文档。 这其实并不难-反而有些恼人-由于有用的文件并很少...关键的接口和类都将会在此节呈现给你。 |
用XML为ApplicationContext配置一个 LoadTimeWeaver简单得只须要添加一行。 (请注意几乎确定你须要使用ApplicationContext做为你的 Spring容器 - 通常来讲只有BeanFactory是不够的, 由于LTW功能须要用到BeanFactoryPostProcessors。)
当要使用Spring框架的LTW功能时,你须要配置一个LoadTimeWeaver, 通常能够用<context:load-time-weaver/>元素来完成。 下面为一个有效的使用默认设置的<context:load-time-weaver/>定义。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <context:load-time-weaver/> </beans>
上面<context:load-time-weaver/> bean的定义会自动为你定义和注册若干 特定LTW的基础设施beans,好比一个LoadTimeWeaver 和一个AspectJWeavingEnabler。请注意 <context:load-time-weaver/>是怎样在context 命名空间下被定义的;还要注意被引用的XML Schema文件只在Spring 2.5或更高版本中才可用。
上面的配置为你定义并注册了一个默认的LoadTimeWeaver bean。 默认的LoadTimeWeaver是一个 DefaultContextLoadTimeWeaver类,它更倾向于去装饰一个能自动检测的LoadTimeWeaver类:LoadTimeWeaver 的确切类型会根据你的运行时环境“自动检测”出来(概述以下表)。
Table 6.1. DefaultContextLoadTimeWeaver LoadTimeWeaversDefaultContextLoadTimeWeaver类和LoadTimeWeavers接口
运行时环境 | LoadTimeWeaver的接口实现 |
---|---|
WebLogicLoadTimeWeaver |
|
OC4JLoadTimeWeaver |
|
GlassFish环境下 |
GlassFishLoadTimeWeaver |
以SpringInstrumentationSavingAgent 启动的JVM中(java -javaagent:path/to/spring-agent.jar) |
InstrumentationLoadTimeWeaver |
不过,咱们更但愿这些类加载器能遵循共同的规范 (例如适用TomcatInstrumentableClassLoader和Resin) |
ReflectiveLoadTimeWeaver |
请注意当使用DefaultContextLoadTimeWeaver时只有 LoadTimeWeavers实现类能进行自动检测: 固然,你也能够经过指定将类的彻底限定名做为<context:load-time-weaver/> 元素中weaver-class属性的值 来指定究竟想使用哪一个LoadTimeWeaver的实现。以下例:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<context:load-time-weaver
weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</beans>
在<context:load-time-weaver/>元素上定义和注册的 LoadTimeWeaver接口能够在Spring容器中以 loadTimeWeaver名字找到。 记住LoadTimeWeaver接口只是做为Spring LTW基础设施的一个机制 用来添加一个或多个ClassFileTransformers的。 ClassFileTransformer类实际是利用ClassPreProcessorAgentAdapter类(包含在 org.aspectj.weaver.loadtime中)来进行LTW的。 有关ClassPreProcessorAgentAdapter的细节请参见 类级别的javadoc,织入实际怎样生效的具体内容已经超出本节讨论范围。
让咱们来讨论<context:load-time-weaver/>的最后一个属性: aspectj-weaving。 这是一个简单的LTW开关,就这么简单。 它能够接受以下所述的三种值,若是不显示设置此属性则其默认值为autodetect
这最后一节包括全部你在诸如应用服务器和web容器中使用Spring的LTW功能时须要的额外设置和配置。
你可能在各类Java应用中经过使用由Spring提供的检测代理启用Spring的LTW功能 (独立应用或者基于应用服务器的应用)。这样的话,能够经过指定 -javaagent:path/to/spring-agent.jar选项来启动虚拟机。 请注意这须要修改虚拟机的启动脚本,但在某些应用服务器环境下是禁止这么作的 (这取决于你的操做策略)。
对于部署在Apache Tomcat 5.0或更高版本上的web应用,Spring将一个 TomcatInstrumentableClassLoader注册成为web应用的类加载器。 必须的Tomcat设置以下所示,你能够把它放在Tomcat WAR包根目录下的核心文件 server.xml中或放到应用特定的META-INF/context.xml文件中。 Spring的spring-tomcat-weaver.jar须要被包含到Tomcat 的common lib路径下以确保设置生效。
<Context path="/myWebApp" docBase="/my/webApp/location"> <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader" useSystemClassLoaderAsParent="false"/> </Context>
注意:当使用LTW时,咱们通常推荐使用Tomcat 5.5.20或更高版本。 先前的版本对定制的ClassLoader设置会产生问题。
另外,请考虑使用在Tomcat启动脚本中(见上面)指定由Spring提供的通用虚拟机代理。 这样才能使检测功能在全部已部署的web应用中可用,不管其上运行的是哪一种类加载器。
有关更多基于Tomcat织入设置的详细讨论,请参考讨论各类不一样Tomcat版本内容的 Section 12.6.1.3.1, “Tomcat(5.0以上)加载时的织入配置”一节。虽然本节主要关注于 JPA persistence提供者的设置,但也谈到了Tomcat各类特定设置适用于通常加载时织入的状况。
BEA WebLogic(版本10或更高),Oracle的JavaEE容器(OC4J 10.1.3.1或更高)以及 Resin(版本3.1或更高)提供具备本地检测能力的类加载器。 Srping的原生LTW利用这些类加载器来激活AspectJ织入。你能够经过简单地激活以前提到的 context:load-time-weaver来启动LTW功能。具体来讲,即你 不须要经过修改启动脚原本添加 -javaagent:path/to/spring-agent.jar。
GlassFish一样也提供了检测能力的类加载器,不过只能在它的EAR环境下使用。 对于GlassFish的web应用,可使用跟上面tomcat相同的设置。
更多关于AspectJ的信息能够查看 AspectJ website。
Eclipse AspectJ Adrian Colyer等人编著(Addison-Wesley, 2005) 一书提供了针对AspectJ语言全面的介绍和参考。
AspectJ in Action Ramnivas Laddad编著(Manning, 2003)是一本很是出色介绍AOP的书籍; 全书着重介绍了AspectJ,但也对一些通用的AOP场景进行了比较深刻的研究。