面向切面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象的编程(OOP)。OOP中模块化的关键单元是类,而在AOP中模块化是切面。我们知道,使用OOP有一些弊端,当需要为多个不具备继承关系的对象引入同一个公共行为时,例如日志,安全检测等,我们只有在每个对象里引用公共行为,这样就会产生大量重复的代码。而这种场景正是AOP擅长的领域。
一. AOP概念
让我们首先定义一些主要的AOP概念和术语:
- 切面(Aspect):切面由切点和增强组成,它既包括了横切逻辑的定义,也包括了连接点的定义。在Spring AOP中,切面可以使用基于模式或基于
@Aspect
注解(@AspectJ样式)的方式来实现; - 连接点(join point):连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用;
- 增强(Advice):切面在特定的连接点处采取的操作。不同类型的增强包括
around
,before
和after
。包括Spring在内的许多AOP框架都将增强建模为拦截器,并在连接点周围维护一系列拦截器; - 切入点(Pointcut):与连接点匹配的谓词(用于定位连接点)。增强与切入点表达式关联,并在与该切入点匹配的任何连接点处运行(例如,执行具有特定名称的方法)。切入点表达式匹配的连接点的概念是AOP的核心,并且Spring默认使用AspectJ切入点表达语言;
- 引入(Introduction):允许我们向现有的类添加新的方法或者属性。Spring AOP允许您向任何要增强的对象引入新的接口(和相应的实现);
- 目标对象(Target object:):一个或多个切面增强的对象。由于Spring AOP是使用运行时代理实现的,因此该对象始终是被代理的对象;
- AOP代理(AOP proxy):由AOP框架创建的一个对象,用于实现切面约定(增强方法执行等)。在Spring Framework中,AOP代理是JDK动态代理或CGLIB代理;
- 织入(Weaving):将切面与其他应用程序类型或对象链接以创建增强对象。这可以在编译时(例如,使用AspectJ编译器),加载时或在运行时完成。像其他纯Java AOP框架一样,Spring AOP在运行时执行编织
Spring AOP包括以下类型的增强:
- Before advice(前置增强): 在连接点之前运行的增强,但是它不能阻止执行流程继续进行到连接点(除非它引发异常)
- After returning advice(后置增强):在连接点正常完成之后要运行的增强
- After throwing advice(异常抛出增强):如果方法因引发异常而退出,则运行增强
- After (finally) advice(finally增强):不管是抛出异常或者正常退出都会执行
- Around advice(环绕增强):包围一个连接点的增强,如方法调用。这是最强大的一种增强类型。环绕增强可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行
二. Spring AOP的能力和目标
Spring AOP当前仅支持方法执行连接点(建议在Spring Bean上执行方法)。虽然可以在不影响到Spring AOP核心API的情况下加入对成员变量拦截器支持,但Spring并没有实现成员变量拦截器。如果需要增强字段访问和更新连接点,请考虑使用诸如AspectJ之类的语言。Spring AOP的AOP方法不同于大多数其他AOP框架。目的不是提供最完整的AOP实现(尽管Spring AOP相当强大)。相反,其目的是在AOP实现和Spring IoC之间提供紧密的集成,以帮助解决企业应用程序中的常见问题。
三. AOP代理
Spring AOP默认将标准JDK动态代理用于AOP代理,这使得可以代理任何接口(或一组接口)。Spring AOP也可以使用CGLIB代理。这对于代理类而不是接口是必需的。默认情况下,如果业务对象未实现接口,则使用CGLIB。由于最好是对接口而不是对类进行编程,因此业务类通常实现一个或多个业务接口。在某些情况下(可能极少发生),当您需要建议未在接口上声明的方法或需要将代理对象作为具体类型传递给方法时,可以 强制使用CGLIB。
四. @AspectJ支持
@AspectJ
是一种将切面声明为带有注释的常规Java类的样式。@AspectJ
风格是AspectJ项目在AspectJ 5版本中引入的 。Spring
使用AspectJ
提供的用于切入点解析和匹配的库来解释与AspectJ 5
相同的注解。但是,AOP
运行时仍然是纯Spring AOP
,并且不依赖于AspectJ
编译器或编织器。
1. 启用@AspectJ支持
通过自动代理,如果Spring确定一个bean被一个或多个切面增强,它将自动为该bean生成一个代理以拦截方法调用并确保按需运行增强。可以使用XML或Java风格的配置来启用@AspectJ支持。无论哪种情况,您都需要确保AspectJ的aspectjweaver.jar
库位于应用程序的类路径(版本1.8或更高版本)上。
通过Java配置启用@AspectJ支持
使用@Configuration
启用@AspectJ
支持,添加@EnableAspectJAutoProxy
注解 ,如以下示例所示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
通过XML配置启用@AspectJ支持
要使用基于XML的配置启用@AspectJ支持,请使用aop:aspectj-autoproxy
元素,如以下示例所示:
<aop:aspectj-autoproxy/>
2. 声明切面(Aspect)
启用@AspectJ
支持后,Spring会自动检测在应用程序上下文中使用@Aspect
注解定义的任何bean,并用于配置Spring AOP。下面的例子展示了NotVeryUsefulAspect
的定义。如下:
@Aspect
@Component
public class NotVeryUsefulAspect {
}
注: 在Spring AOP中,拥有切面的类本身不可能是其它切面中通知的目标。一个类上面的@Aspect注解标识它为一个切面,并且从自动代理中排除它。
Advising aspects with other aspects?
In Spring AOP, aspects themselves cannot be the targets of advice from other aspects. The
@Aspect
annotation on a class marks it as an aspect and, hence, excludes it from auto-proxying.
3. 声明切入点(Pointcut)
切入点确定了感兴趣的连接点,从而使我们能够控制何时运行增强。Spring AOP仅支持Spring Bean的方法执行连接点。切入点声明由两部分组成:一个包含名称和任意参数的签名,以及一个切入点表达式,该切入点表达式精确确定我们感兴趣的执行方法。在AOP的@AspectJ注解风格中,使用一个常规方法定义提供切入点签名 ,并通过使用@Pointcut
注解表达切入点表达式(用作切入点签名的方法必须具有void
返回类型),示例如下,定义了一个名为anyOldTransfer
的切入点,用于匹配名为transfer
的方法。
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
构成@Pointcut
注解的切入点表达式是一个常规的AspectJ 5切入点表达式,有关AspectJ的切入点语言的完整讨论,请参见AspectJ编程指南。
支持的切入点指示符
Spring AOP支持以下在切入点表达式中使用的AspectJ切入点指示符(PCD):
- execution: 用于匹配方法执行的连接点。这是使用Spring AOP时要使用的主要切入点指示符
- within: 将匹配限制为某些类型内的连接点(使用Spring AOP时,在匹配类型内声明的方法执行)
- this: 限定匹配特定的连接点,其中bean reference(生成的Spring AOP 代理)是指定类型的实例
- target: 限定匹配特定的连接点,其中目标对象(被代理的应用对象)是指定类型的实例
- args: 限定匹配特定的连接点,其中参数是指定类型的实例
- @target: 限定匹配特定的连接点,其中正执行对象的类持有指定类型的注解
- @args: 限定匹配特定的连接点,其中实际传入参数的运行时类型持有指定类型的注解
- @within: 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)
- @annotation: 限定匹配特定的连接点,其中连接点的主体持有指定的注解
另外,Spring AOP还提供了一个名为bean
的PCD。这个PCD允许你限定匹配连接点到一个特定名称的Spring bean,或者到一个特定名称Spring bean的集合(当使用通配符时)。bean
PCD具有下列的格式:
bean(idOrNameOfBean)
idOrNameOfBean
标记可以是任何Spring bean的名字:限定通配符使用*
来提供,如果你为Spring bean制定一些命名约定,你可以非常容易地编写一个bean
PCD表达式将它们选出来。和其它连接点指示符一样,bean
PCD也支持&&, ||和 !逻辑操作符。
注意:请注意
bean
PCD仅仅 被Spring AOP支持而不是AspectJ. 这是Spring对AspectJ中定义的标准PCD的一个特定扩展。bean
PCD不仅仅可以在类型级别(被限制在基于织入AOP上)上操作而还可以在实例级别(基于Spring bean的概念)上操作。
组合切入点表达式
您可以使用&&,
||
和组合切入点表达式!
。您也可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:
// 代表了任意public方法的执行时匹配
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
// 代表了在交易模块中的任意的方法执行时匹配
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {}
// 代表了在交易模块中的任意的公共方法执行时匹配
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
如上所示,用更少的命名组件来构建更加复杂的切入点表达式是一种最佳实践。当用名字来指定切入点时使用的是常见的Java成员可见性访问规则。(比如说,你可以在同一类型中访问私有的切入点,在继承关系中访问受保护的切入点,可以在任意地方访问公共切入点)。成员可视性访问规则不影响到切入点的匹配。
共享通用切入点定义
在使用企业应用程序时,开发人员通常希望从多个切面引用应用程序的模块和特定的操作集。我们建议为此定义一个切面CommonPointcuts
,以捕获常见的切入点表达式。这样的切面通常类似于以下示例:
package com.xyz.myapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class CommonPointcuts {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.myapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.myapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.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.myapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.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.myapp.abc.service and com.xyz.myapp.def.service) then
* the pointcut expression "execution(* com.xyz.myapp..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.myapp..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.myapp.dao.*.*(..))")
public void dataAccessOperation() {}
您可以在需要切入点表达式的任何地方引用在这样的切面中定义的切入点。例如,要使服务层具有事务性,您可以编写以下内容:
<aop:config>
<aop:advisor
pointcut="com.xyz.myapp.CommonPointcuts.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
示例
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)
实现
AccountService
接口的目标对象的任意连接点 (在Spring AOP中只是方法执行)target(com.xyz.service.AccountService)
任何一个只接受一个参数,并且运行时所传入的参数是
Serializable
接口的连接点(在Spring AOP中只是方法执行)args(java.io.Serializable)
请注意在例子中给出的切入点不同于
execution(* *(java.io.Serializable))
:args
版本只有在动态运行时候传入参数是Serializable
时才匹配,而execution
版本在方法签名中声明只有一个Serializable
类型的参数时候匹配。目标对象中有一个
@Transactional
注解的任意连接点 (在Spring AOP中只是方法执行):@target(org.springframework.transaction.annotation.Transactional)
任何一个目标对象声明的类型有一个
@Transactional
注解的连接点 (在Spring AOP中只是方法执行):@within(org.springframework.transaction.annotation.Transactional)
执行方法带有
@Transactional
注解的任何连接点(仅在Spring AOP中为方法执行) :@annotation(org.springframework.transaction.annotation.Transactional)
任何一个只接受一个参数,并且运行时所传入的参数类型具有
@Classified
注解的连接点(在Spring AOP中只是方法执行)@args(com.xyz.security.Classified)
名为
tradeService
:的Spring bean上的任何连接点(仅在Spring AOP中执行方法)bean(tradeService)
任何一个在名字匹配通配符表达式
*Service
的Spring bean之上的连接点 (在Spring AOP中只是方法执行):bean(*Service)
编写好的切入点
为了获得最佳的匹配性能,我们应该考虑他们试图实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然会属于以下三类之一:kinded, scoping, and contextual:
- Kinded指示符选择特定类型的连接点:
execution
,get
,set
,call
,和handler
。 - scoping指示符选择一组感兴趣的连接点(可能有多种):
within
和withincode
- contextual指示符基于上下文匹配(和任选的绑定): ,
this
,target
和@annotation
编写正确的切入点至少应包括前两种类型(种类和作用域)。您可以包括contextual指示符以根据连接点上下文进行匹配,也可以绑定该上下文以在建议中使用。仅提供Kinded标识符或仅提供上下文的标识符是可行的,但是由于额外的处理和分析,可能会影响编织性能(使用的时间和内存)。范围指定符的匹配非常快,使用它们的使用意味着AspectJ可以非常迅速地消除不应进一步处理的连接点组。
4. 声明增强(Advice)
增强是跟一个切入点表达式关联起来的,并且在切入点匹配的方法执行之前或者之后或者前后运行。 切入点表达式可能是指向已命名的切入点的简单引用或者是一个声明的切入点表达式。
前置增强
一个切面里使用 @Before
注解声明前置通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
// 或者
// @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.CommonPointcuts.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.CommonPointcuts.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.CommonPointcuts.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.CommonPointcuts.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
Note that
@AfterThrowing
does not indicate a general exception handling callback. Specifically, an@AfterThrowing
advice method is only supposed to receive exceptions from the join point (user-declared target method) itself but not from an accompanying@After
/@AfterReturning
method.
finally增强
不论一个方法是如何结束的,最终通知都会运行。使用@After
注解来声明。最终通知必须准备处理正常返回和异常返回两种情况。通常用它来释放资源等类似目的。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
环绕增强
环绕增强在一个方法执行之前和之后执行。它使得通知有机会在一个方法执行之前和执行之后运行。而且它可以决定这个方法在什么时候执行,如何执行,甚至是否执行。 环绕通知经常在某线程安全的环境下,你需要在一个方法执行之前和之后共享某种状态的时候使用。 请尽量使用最简单的满足你需求的通知。(比如如果简单的前置通知也可以适用的情况下不要使用环绕通知)。
环绕通知使用@Around
注解来声明。通知的第一个参数必须是 ProceedingJoinPoint
类型。在通知体内,调用 ProceedingJoinPoint
的proceed()
方法会导致 后台的连接点方法执行。proceed
方法也可能会被调用并且传入一个 Object[]
对象,该数组中的值将被作为方法执行时的参数。
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.CommonPointcuts.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
方法的调用者得到的返回值就是环绕通知返回的值。 例如:一个简单的缓存切面,如果缓存中有值,就返回该值,否则调用proceed()方法。 请注意proceed可能在通知体内部被调用一次,许多次,或者根本不被调用,所有这些都是合法的。
增强参数
Spring 提供了完整的增强类型 - 这意味着你可以在增强签名中声明所需的参数, (就像我们在前面看到的后置和异常通知一样)而不总是使用Object[]。 我们将会看到如何使得参数和其他上下文值对通知体可用。 首先让我们看以下如何编写普通的增强以找出正在被增强的方法。
访问当前 JoinPoint
任何增强方法可以将第一个参数定义为org.aspectj.lang.JoinPoint
类型 (环绕增强需要定义第一个参数为ProceedingJoinPoint
类型, 它是 JoinPoint
的一个子类)。JoinPoint
接口提供了一系列有用的方法,比如 getArgs()
(返回方法参数)、 getThis()
(返回代理对象)、getTarget()
(返回目标)、 getSignature()
(返回正在被增强的方法相关信息)和 toString()
(打印出正在被增强的方法的有用信息)。
将参数传递给增强
我们已经看到了如何绑定返回值或者异常(使用后置通知和异常通知)。为了可以在增强体内访问参数, 你可以使用args
来绑定。如果在一个args表达式中应该使用类型名字的地方使用一个参数名字,那么当增强执行的时候对应的参数值将会被传递进来。用一个例子应该会使它变得清晰。 假使你想要增强以一个Account对象作为第一个参数的DAO操作的执行, 你想要在通知体内也能访问account对象,可以编写如下的代码:
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
切入点表达式的 args(account,..)
部分有两个目的:首先它保证了只会匹配那些接受至少一个参数的方法的执行,而且传入的参数必须是Account
类型的实例, 其次它使得在增强体内可以通过account
参数访问实际的Account
对象。
另外一个办法是定义一个切入点,这个切入点在匹配某个连接点的时候“提供”了 Account
对象的值,然后直接从增强中访问那个命名切入点。看起来和下面的示例一样:
@Pointcut("com.xyz.myapp.CommonPointcuts.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();
// ...
}
增强参数和泛型
Spring AOP可以处理类声明和方法参数中使用的泛型。假设您具有如下泛型类型:
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
}
为了使这项工作有效,我们将不得不检查集合中的每个元素,这是不合理的,因为我们也无法决定null
值总体上如何处理。要实现与此类似的功能,您必须在上键入参数Collection<?>
并手动检查元素的类型。
确定方法参数名
增强调用中的参数绑定依赖于切入点表达式中使用的名称与增强和切入点方法签名中声明的参数名称匹配。 参数名无法 通过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.CommonPointcuts.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()
方法返回值(或者注解值)较低的那个有更高的优先级。
从Spring Framework 5.2.7开始,在相同@Aspect类中定义的,需要在同一连接点运行的通知方法将根据其建议类型按照从最高优先级到最低优先级的以下顺序分配优先级:@Around
, @Before
, @After
, @AfterReturning
, @AfterThrowing
。但是请注意,在遵循切面的AspectJ的@After语义之后,在相同切面中的@AfterReturning或@AfterThrowing通知方法之后,将有效地调用@After通知方法。
当在同一@Aspect类中定义的两个相同类型的建议(例如,两个@After建议方法)都需要在同一连接点上运行时,其顺序是不确定的(因为无法通过反射为javac编译的类检索源代码声明顺序。考虑将此类建议方法折叠为每个@Aspect类中每个连接点的一个建议方法,或将建议重构为单独的@Aspect类,您可以通过Ordered或@Order在方面级别进行排序。
5. 引入(Introductions)
引入(在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.CommonPointcuts.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");
6. 切面实例化模型
默认情况下,在application context中每一个切面都会有一个实例。AspectJ把这个叫做单例化模型。 也可以用其他的生命周期来定义切面:Spring支持AspectJ的 perthis
和pertarget
实例化模型(现在还不支持percflow、percflowbelow
和pertypewithin
)。
一个”perthis” 切面通过在@Aspect
注解中指定perthis
子句来声明。让我们先来看一个例子,然后解释它是如何运作的:
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {
private int someState;
@Before("com.xyz.myapp.CommonPointcuts.businessService()")
public void recordServiceUsage() {
// ...
}
}
这个perthis
子句的效果是每个独立的service对象执行一个业务时都会创建一个切面实例(切入点表达式所匹配的连接点上的每一个独立的对象都会绑定到this
上)。 在service对象上第一次调用方法的时候,切面实例将被创建。切面在service对象失效的同时失效。 在切面实例被创建前,所有的增强都不会被执行,一旦切面对象创建完成, 定义的通知将会在匹配的连接点上执行,但是只有当service对象是和切面关联的才可以。 请参阅 AspectJ 编程指南了解更多关于per-clauses的信息。
pertarget
实例模型的跟“perthis”完全一样,只不过是为每个匹配于连接点 的独立目标对象创建一个切面实例。
7. 示例
现在你已经看到了每个独立的部分是如何运作的了,是时候把他们放到一起做一些有用的事情了!
因为并发的问题,有时候业务服务(business services)可能会失败(例如,死锁失败)。如果重新尝试一下, 很有可能就会成功。对于业务服务来说,重试几次是很正常的,我们可能需要透明的重试操作以避免客户看到一个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.CommonPointcuts.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
注释:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
并且对service操作的实现进行注解。为了只重试幂等操作,切面的修改只需要改写切入点表达式, 使得只匹配@Idempotent
操作:
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// ...
}
五. 代理机制
Spring AOP使用JDK动态代理或CGLIB创建给定目标对象的代理。JDK内置了JDK动态代理,而CGLIB是一个通用的开源类定义库(重新打包为spring-core
)。
如果要代理的目标对象实现至少一个接口,则使用JDK动态代理。代理了由目标类型实现的所有接口。如果目标对象未实现任何接口,则将创建CGLIB代理。
如果要强制使用CGLIB代理(例如,代理为目标对象定义的每个方法,而不仅是由其接口实现的方法),都可以这样做。但是,您应该考虑以下问题:
- 使用CGLIB,
final
不能建议方法,因为不能在运行时生成的子类中覆盖方法。 - 从Spring 4.0开始,由于CGLIB代理实例是通过Objenesis创建的,因此不再调用代理对象的构造函数两次。仅当您的JVM不允许绕过构造函数时,您才可以从Spring的AOP支持中看到两次调用和相应的调试日志条目。
要强制使用CGLIB代理,请将<aop:config>
的proxy-target-class
属性值设置为true,如下所示:
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>
要在使用@AspectJ自动代理风格支持时强制使用CGLIB代理,请将元素的proxy-target-class
属性设置 <aop:aspectj-autoproxy>
为true
,如下所示:
<aop:aspectj-autoproxy proxy-target-class="true"/>
多个
<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代理应用于此三者之上
1. 理解AOP代理
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.addInterface(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框架。
六. 以编程的方法创建@Aspectj代理
除了在配置文件中使用<aop:config>或者<aop:aspectj-autoproxy>来声明切面。 同样可以通过编程方式来创建代理来增强目标对象。关于Spring AOP API的详细介绍, 请参看下一章。在这里,我们要重点介绍通过使用@AspectJ方面自动创建代理的功能。
可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory
为一个或多 @AspectJ切面增强的目标对象创建一个代理。该类的基本用法非常简单,示例如下:
// 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();
有关更多信息,请参见javadoc。
七. 在Spring应用程序中使用AspectJ
到目前为止本章讨论的一直是纯Spring AOP。在这一节里面我们将介绍如何使用AspectJ compiler/weaver 来代替Spring AOP或者作为它的补充,因为有些时候Spring AOP单独提供的功能也许并不能满足你的需要。
Spring附带了一个小的AspectJ切面库,可以在您的发行版中独立使用spring-aspects.jar
。您需要将其添加到类路径中才能使用其中的切面。在Spring中使用AspectJ进行domain object的依赖注入和Spring中其他的AspectJ切面注入域对象讨论了该库的内容以及如何使用它。使用Spring IoC来配置AspectJ的切面讨论了如何对通过AspectJ compiler织入的AspectJ切面进行依赖注入。最后, 在Spring应用中使用AspectJ加载时织入(LTW)介绍了使用AspectJ的Spring应用程序如何进行加载期织入(load-time weaving)。
1. 在Spring中使用AspectJ进行domain object的依赖注入
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
可以让自动装配分别按照类型或名字进行。 作为另外一种选择,最好是在字段或方法级使用@Autowired
和 @Inject
为你的@Configurable
beans指定明确的、注解驱动的依赖注入。
最后,你可以通过使用dependencyCheck
属性,让Spring对新创建和配置的对象的对象引用进行依赖检查(例如:@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true)
)。 如果这个属性设置为true,Spring会在配置结束后校验(除了primitives和collections类型) 所有的属性是否都被设置。
仅仅使用注解并没有做任何事情。但是spring-aspects.jar
中的AnnotationBeanConfigurerAspect
会在注解存在时起作用。从本质上讲,实质上切面指明: “在初始化一个由@Configurable
注解的新对象时, Spring按照注解中的属性来配置这个新创建的对象”。这种情况下,initialization 指新初始化的(比如用new
初始化)的对象以及能进行反序列化的 Serializable
对象(例如通过 readResolve()方法)。
在上一段中一个关键的阶段就是“inessence”。多数情况下,“ 当从一个新对象初始化返回之后”的精确语义很不错…这种语境下, “初始化之后”的意思是依赖将在对象被构造之后注入 - 这意味着在类的构造器块中依赖将不可用。如果你希望它能在构造器代码块执行 之前被注入,并从而在构造器中使用它, 那么你需要在
@Configurable
接口声明上做类似的定义:@Configurable(preConstruction = true)
为此,必须将带注释的类型与AspectJ编织器编织在一起。您可以使用构建时的Ant或Maven任务来执行此操作(例如,请参阅《 AspectJ开发环境指南》),也可以使用加载时编织(请参阅Spring Framework中的使用AspectJ进行加载时编织)。类
AnnotationBeanConfigurerAspect
本身也需要Spring来配置(获得bean factory的引用,使用bean factory配置新的对象)。如果使用基于Java的配置,则可以将其添加@EnableSpringConfigured
到任何@Configuration
类,如下所示:
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
如果您更喜欢基于XML的配置,Spring context
名称空间 定义了一个方便的context:spring-configured
元素,您可以按如下方式使用它:
<context:spring-configured/>
在切面配置完成之前创建的@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>
除非您真的想在运行时依赖它的语义,否则不要通过bean配置器方面来激活@Configurable处理。特别是,请确保不要在通过容器注册为常规Spring bean的bean类上使用@Configurable。这样做会导致两次初始化,一次是通过容器,一次是通过切面
1.1 单元测试@Configurable对象
提供@Configurable
支持的一个目的就是使得domain object的单元测试可以独立进行,不需要通过硬编码查找各种倚赖关系。如果@Configurable
类型没有通过AspectJ织入,则在单元测试过程中注解不会起到任何作用, 测试中你可以简单的为对象的mock或者stub属性赋值,并且和正常情况一样去使用该对象。 如果@Configurable
类型通过AspectJ织入, 我们依然可以脱离容器进行单元测试,不过每次创建一个新的@Configurable
对象时都会看到一个warning,标示该对象没有被Spring配置。
1.2 用多个应用程序上下文
AnnotationBeanConfigurerAspect
通过一个AspectJ singleton切面来实现对 @Configurable
的支持。一个singleton切面的作用域和一个 静态变量的作用域是一样的,那就是说,对于每一个classloader有一个切面来定义类型。 这就意味着如果你在一个classloader层次结构中定义了多个application context的时候就需要考虑 在哪里定义@EnableSpringConfigured
bean和在哪个classpath下 放置spring-aspects.jar
。
考虑一个典型的Spring Web应用程序配置,该配置具有一个共享的父应用程序上下文,该上下文定义了通用的业务服务,支持那些服务所需的一切,以及每个Servlet的一个子应用程序上下文(其中包含该Servlet的特定定义)。所有这些上下文共存于相同的类加载器层次结构中,因此AnnotationBeanConfigurerAspect
只能保留对其中一个的引用。在这种情况下,我们建议@EnableSpringConfigured
在共享(父)应用程序上下文中定义bean。这定义了您可能想注入域对象的服务。结果是,您无法使用@Configurable机制来配置域对象,而该域对象将引用在子(特定于servlet的)上下文中定义的bean的引用(无论如何,这都不是您想做的事情)。
当在一个容器中部署多个web-app的时候,请确保每一个web-application使用自己的classloader 来加载spring-aspects.jar中的类(例如将spring-aspects.jar放在WEB-INF/lib目录下)。 如果spring-aspects.jar被放在了容器的classpath下(因此也被父classloader加载),则所有的 web application将共享一个aspect实例,这可能并不是你所想要的。
2. Spring中其他的AspectJ切面
除了@Configurable
切面, spring-aspects.jar
包含了一个AspectJ切面可以用来为 那些使用了@Transactional
注解的类型和方法驱动Spring事务管理。 提供这个的主要目的是有些用户希望脱离Spring容器使用Spring的事务管理。
解析@Transactional
注解的切面是 AnnotationTransactionAspect
。当使用这个切面时, 你必须注解这个实现类(或该类中的方法,或两者),而不是 这个类实现的接口(如果有)。AspectJ遵循Java的规则,即不继承接口上的注释,参考JAVA注解的继承性。
类之上的一个@Transactional
注解为该类中任何 public操作的执行指定了默认的事务语义。
类内部方法上的一个@Transactional
注解会覆盖类注解(如果存在) 所给定的默认的事务语义。可以标注任何可见性的方法,包括私有方法。直接标注非公共方法是执行此类方法而获得事务划分的唯一方法。
从Spring Framework 4.2开始,
spring-aspects
提供了类似的切面,为标准javax.transaction.Transactional
注释提供了完全相同的功能。检查JtaAnnotationTransactionAspect
更多细节。
对于希望使用Spring配置和事务管理支持但又不想(或不能)使用注解的AspectJ程序员,spring-aspects.jar还包含抽象方面,您可以扩展这些抽象方面以提供自己的切入点定义。有关更多信息,请参见AbstractBeanConfigurerAspect
和 AbstractTransactionAspect
方面的资源。作为示例,以下摘录显示了如何编写切面来使用与完全限定的类名匹配的原型Bean定义来配置域模型中定义的对象的所有实例:
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(..)) &&
CommonPointcuts.inDomainModel() &&
this(beanInstance);
}
3. 使用Spring IoC配置AspectJ Aspects
当您将AspectJ方面与Spring应用程序一起使用时,很自然的会想到用Spring来管理这些切面。AspectJ运行时本身负责方面的创建,这意味着通过Spring来管理AspectJ 创建切面依赖于切面所使用的AspectJ instantiation model(per-xxx 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>
非单例切面的配置稍难一点,然而它可以通过定义一个prototype 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>
不要被
<aop:aspectj-autoproxy/>
元素的名字所误导: 用它会导致Spring AOP 代理的创建。在这中只是使用@AspectJ 类型的切面声明,但并不会涉及AspectJ运行时。
4. 在Spring应用中使用AspectJ加载时织入(LTW)
加载时织入(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带来的价值在于能够对编织过程进行更精细的控制。“ Vanilla” AspectJ LTW是通过使用Java(5+)代理来实现的,该代理在启动JVM时通过指定VM参数来打开。 这种JVM范围的设置在一些情况下或许不错,但通常情况下显得有些粗颗粒。而用Spring的LTW能让你在per-ClassLoader
的基础上打开LTW, 这显然更加细粒度并且对“单JVM多应用”的环境更具意义(例如在一个典型应用服务器环境中一样)。
此外,在某些环境中,加载时编织,不需要对应⽤程序服务器的启动脚本进⾏任何修改,这些脚本需要添加javaagent:path/to/aspectjweaver.jar或(如本节后⾯所述)javaagent:path/to/spring-instrument.jar。开发⼈员配置应⽤ 程序上下⽂以启⽤加载时编织,⽽不是依赖通常负责部署配置(如启动脚本)的管理员。
4.1 第一个例子
假设你是一个应用开人员,被指派诊断一个系统的若干性能问题。与其拿出性能分析工具, 我们不如开启一个简单的分析切面,使我们能很快地得到一些性能指标,这样我们就能马上 针对特定区域使用一些较细粒度的分析工具。
这就是一个分析切面。没什么特别的,只是一个快餐式的基于时间的模拟分析器, 使用类@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 {
@Pointcut("execution(public * foo..*.*(..))")
public void methodsToBeProfiled(){}
@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());
}
}
}
我们还需要创建一个META-INF/aop.xml
文件,以告知AspectJ weaver 我们要把ProfilingAspect
织入到类中。这个文件惯例,即在Java classpath中 出现一个文件称作META-INF/aop.xml
是标准的AspectJ。以下示例显示了该aop.xml
文件:
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- a service object; we will be profiling its 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-instrument.jar foo.Main
-javaagent是用于指定和启用代理以对在JVM上运行的程序进行检测的标志。Spring框架附带有这样的代理工具InstrumentationSavingAgent,该代理文件打包在spring-instrument.jar中,在上一示例中作为-javaagent参数的值提供。
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支持都已介绍完了, 此节余下内容将对使用这些配置和用法背后的理由作详细解释。
4.2 切面
你在LTW中使用的切面必须是AspectJ切面。您可以使用AspectJ语言本身来编写它们,也可以使用@AspectJ风格来编写方面。这样,您的方面就是有效的AspectJ和Spring AOP切面。 此外,编译后的切面类需要在classpath可用。
4.3 META-INF/aop.xml
AspectJ LTW的基础设施是用一个或多个位于Java classpath上的(可以是直接的文件形式, 也可以是更典型的jar包形式)META-INF/aop.xml
文件配置起来的
该文件的结构和内容在AspectJ参考文档的LTW部分中进行了详细 说明。由于该aop.xml
文件是100%AspectJ,因此在此不再赘述
4.4 所需的库
至少,您需要以下库来使用Spring Framework对AspectJ LTW的支持:
spring-aop.jar
aspectjweaver.jar
如果使用Spring提供的代理来启用检测(),则还需要:
spring-instrument.jar
5. Spring 配置
Spring LTW功能的关键组件是LoadTimeWeaver
接口 (在org.springframework.instrument.classloading
包中), 以及Spring分发包中大量的实现。LoadTimeWeaver
的实现负责在运行时把一个或多个java.lang.instrument.ClassFileTransformers
类添加到 ClassLoader
中,这能产生各种各样有趣的应用,LTW切面恰好便是其中之一。
如果你对运行时类文件变换的思想还不熟悉,推荐你在继续之前阅读
java.lang.instrument
包的Javadoc API文档。 虽然该文档并不全面,但是至少您可以看到关键的接口和类(在您阅读本节时作为参考)。
为特定的ApplicationContext配置LoadTimeWeaver简单得只需要添加一行。(请注意几乎肯定你需要使用ApplicationContext
作为你的 Spring容器 - 一般来说只有BeanFactory
是不够的, 因为LTW功能需要用到BeanFactoryPostProcessors
。)
要启用Spring Framework的LTW支持,您需要配置LoadTimeWeaver
,通常通过使用@EnableLoadTimeWeaving
注解来完成,如下所示:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
另外,如果您更喜欢基于XML的配置,请使用 <context:load-time-weaver/>
元素。请注意,元素是在context
名称空间中定义的 。以下示例显示如何使用<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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:load-time-weaver/>
</beans>
前面的配置会自动为您定义并注册许多LTW特定的基础结构Bean,例如LoadTimeWeaver
和an AspectJWeavingEnabler
。默认LoadTimeWeaver
为DefaultContextLoadTimeWeaver
类,它尝试装饰自动检测到的LoadTimeWeaver
。“自动检测”的LoadTimeWeaver的确切类型取决于运行时环境。下表总结了各种LoadTimeWeaver实现:
Runtime Environment | LoadTimeWeaver implementation |
---|---|
Running in Apache Tomcat | TomcatLoadTimeWeaver |
Running in GlassFish (limited to EAR deployments) | GlassFishLoadTimeWeaver |
Running in Red Hat’s JBoss AS or WildFly | JBossLoadTimeWeaver |
Running in IBM’s WebSphere | WebSphereLoadTimeWeaver |
Running in Oracle’s WebLogic | WebLogicLoadTimeWeaver |
JVM started with Spring InstrumentationSavingAgent (java -javaagent:path/to/spring-instrument.jar ) |
InstrumentationLoadTimeWeaver |
Fallback, expecting the underlying ClassLoader to follow common conventions (namely addTransformer and optionally a getThrowawayClassLoader method) |
ReflectiveLoadTimeWeaver |
请注意,该表仅列出LoadTimeWeavers
使用时自动检测到的DefaultContextLoadTimeWeaver
。您可以确切指定LoadTimeWeaver
要使用的实现。
要使用Java配置指定特定的LoadTimeWeaver,请实现该 LoadTimeWeavingConfigurer
接口并覆盖该getLoadTimeWeaver()
方法。以下示例指定一个ReflectiveLoadTimeWeaver
:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {
@Override
public LoadTimeWeaver getLoadTimeWeaver() {
return new ReflectiveLoadTimeWeaver();
}
}
如果使用基于XML的配置,则可以将完全限定的类名指定为元素weaver-class
上属性的值<context:load-time-weaver/>
。同样,以下示例指定了ReflectiveLoadTimeWeaver
:
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:load-time-weaver
weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</beans>
稍后可以使用众所周知的名称loadTimeWeaver从Spring容器中检索由配置定义和注册的LoadTimeWeaver。请记住,LoadTimeWeaver仅作为Spring的LTW基础结构添加一个或多个ClassFileTransformers
的机制而存在。实际 ClassFileTransformer
执行LTW的是ClassPreProcessorAgentAdapter
(从org.aspectj.weaver.loadtime
包中)类。有关更多详细信息,请参见ClassPreProcessorAgentAdapter
类的类级javadoc,因为实际上如何实现编织的细节不在本文档的讨论范围之内。
剩下要讨论的配置的最后一个属性:AspectjWeaving属性(如果使用XML,则为Aspectj-weaving)。此属性控制是否启用LTW。它接受三个可能值之一,如果属性不存在,则默认值为自动检测。下表总结了三个可能的值:
Annotation Value | XML Value | Explanation |
---|---|---|
ENABLED |
on |
AspectJ织入功能开启,切面将会在加载时适当时机被织入。 |
DISABLED |
off |
LTW功能关闭,不会在加载时织入切面。 |
AUTODETECT |
autodetect |
如果Spring LTW基础设施能找到至少一个META-INF/aop.xml 文件,那么AspectJ织入将会开启,否则关闭。此为默认值。 |
6. 特定环境的配置
这最后一节包括所有你在诸如应用服务器和web容器中使用Spring的LTW功能时需要的额外设置和配置。
6.1 Tomcat, JBoss, WebSphere, WebLogic
Tomcat,JBoss / WildFly,IBM WebSphere Application Server和Oracle WebLogic Server均提供了ClassLoader
能够进行本地检测的通用应用程序。Spring原生的LTW可以利用这些ClassLoader实现来提供AspectJ编织。您可以简单地启用加载时编织,如前所述。具体而言,您无需修改JVM启动脚本即可添加 -javaagent:path/to/spring-instrument.jar
。
请注意,在JBoss上,您可能需要禁用应用程序服务器扫描,以防止它在应用程序实际启动之前加载类。一个快速的解决方法是将一个WEB-INF/jboss-scanning.xml
具有以下内容的文件添加到您的工件中:
<scanning xmlns="urn:jboss:scanning:1.0"/>
6.2 通用Java应用程序
在特定LoadTimeWeaver
实现不支持的环境中需要类检测时,JVM代理是通用解决方案。对于这种情况,Spring提供了InstrumentationLoadTimeWeaver
一个需要Spring特定(但非常通用)的JVM代理的程序,该JVM代理spring-instrument.jar
可以通过common@EnableLoadTimeWeaving
和<context:load-time-weaver/>
setups自动检测到。
要使用它,您必须通过提供以下JVM选项来使用Spring代理启动虚拟机:
-javaagent:/path/to/spring-instrument.jar
请注意,这需要修改JVM启动脚本,这可能会阻止您在应用程序服务器环境中使用它(取决于您的服务器和您的操作策略)。也就是说,对于每个JVM一个应用程序的部署(例如独立的Spring Boot应用程序),无论如何,您通常都可以控制整个JVM的设置。