前言
切面(Aspect)是类(Class)的一个补充,两者正交互补,让spring的Ioc容器功能得到大大的增强。
使用Spring进行面向方面的编程
面向方面编程(AOP)是对面向对象编程(OOP)的补充,它提供了考虑程序结构的另一种方式。在OOP中模块化的关键单元是类,而在AOP中模块化的单元是方面。方面支持对跨多种类型和对象的关注点(如事务管理)进行模块化。(在AOP文献中,这种关注通常被称为“横切”关注。)
Spring的一个关键组件是AOP框架。虽然Spring IoC容器不依赖于AOP(这意味着如果您不想使用AOP就不需要使用),但AOP补充了Spring IoC,提供了一个功能非常强大的中间件解决方案。
使用AspectJ切入点的Spring AOP
Spring通过使用基于模式的方法或@AspectJ注释样式,提供了编写自定义方面的简单而强大的方法。这两种样式都提供完整类型的通知和使用AspectJ切入点语言,同时仍然使用Spring AOP进行编织。
本章讨论基于模式和@Aspectj的AOP支持。底层AOP支持将在下一章讨论。
AOP在Spring框架中被用于:
- 提供声明性企业服务。这类服务中最重要的是声明式事务管理。
- 让用户实现自定义方面,用AOP补充他们对OOP的使用。
如果您只对通用声明式服务或其他预先打包的声明式中间件服务(如池)感兴趣,则不需要直接使用Spring AOP,可以跳过本章的大部分内容。
AOP概念
让我们从定义一些中心的AOP概念和术语开始。这些术语不是特定于Spring的。不幸的是,AOP术语不是特别直观。然而,如果Spring使用它自己的术语,这将更加令人困惑。
- 方面:跨多个类的关注点的模块化。事务管理是企业Java应用程序中横切关注点的一个很好的例子。在Spring AOP中,方面是通过使用常规类(基于模式的方法)或使用
@Aspect
注释(@AspectJ样式)注释的常规类来实现的。 - 连接点:程序执行期间的点,例如方法的执行或异常的处理。在Spring AOP中,连接点总是表示方法执行。
- 通知:方面在特定连接点上采取的操作。不同类型的建议包括“around”,“before”和“after”建议。(通知类型将在后面讨论。)许多AOP框架,包括Spring,将通知建模为拦截器,并维护围绕连接点的拦截器链。
- 切入点:匹配连接点的谓词。通知与切入点表达式相关联,并在切入点匹配的任何连接点上运行(例如,执行具有特定名称的方法)。切入点表达式匹配的连接点的概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。
- 引入:代表类型声明额外的方法或字段。Spring AOP允许向任何被通知的对象引入新接口(和相应的实现)。例如,您可以使用引入使bean实现
IsModified
接口,从而简化缓存。(在AspectJ社区中,引入称为类型间声明。) - 目标对象:被一个或多个方面通知的对象。也称为“被通知对象”。由于Spring AOP是通过使用运行时代理实现的,因此该对象始终是一个被代理对象。
- AOP代理:由AOP框架创建的对象,以便实现方面契约(通知方法执行等)。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。
- 编织:将方面与其他应用程序类型或对象链接起来,以创建一个被通知的对象。这可以在编译时(例如,使用AspectJ编译器)、加载时或运行时完成。与其他纯Java AOP框架一样,Spring AOP在运行时执行编织。
Spring AOP包括以下类型的通知:
- Before通知:在连接点之前运行的通知,但不能阻止执行流继续到连接点(除非它抛出异常)。
- After returning 通知:在连接点正常完成之后运行通知(例如,如果一个方法返回而没有抛出异常)。
- After throwing通知:在方法退出时通过抛出异常来运行通知。
- After (finally)通知:无论连接点以何种方式退出(正常或异常返回),都将运行通知。
- Around通知:围绕连接点(如方法调用)的通知。这是最有力的建议。Around通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续到连接点,还是通过返回自己的返回值或抛出异常来缩短被建议的方法执行。
Around通知是最普遍的通知。由于Spring AOP像AspectJ一样提供了各种各样的通知类型,所以我们建议您使用功能最弱的通知类型来实现所需的行为。例如,如果您只需要用方法的返回值更新缓存,那么最好实现after returning通知,而不是around通知,尽管around通知可以完成相同的任务。使用最具体的通知类型可以提供更简单的编程模型,出错的可能性更小。例如,您不需要在用于around通知的JoinPoint
上调用proceed()
方法,因此,您不会失败地调用它。
切入点匹配的连接点的概念是AOP的关键,它区别于只提供拦截的旧技术。切入点使通知能够独立于面向对象的层次结构进行定向。例如,您可以将提供声明式事务管理的around通知应用到跨多个对象(例如服务层中的所有业务操作)的一组方法。
Spring AOP的功能和目标
Spring AOP是用纯Java实现的。不需要特殊的编译过程。Spring AOP不需要控制类装入器层次结构,因此适合在servlet容器或应用程序服务器中使用。
Spring AOP目前只支持方法执行连接点(通知Spring bean上的方法执行)。没有实现字段拦截,尽管可以在不破坏Spring AOP核心api的情况下添加对字段拦截的支持。如果需要通知字段访问和更新连接点,可以考虑使用AspectJ之类的语言。
Spring AOP处理AOP的方法不同于大多数其他AOP框架。其目的不是提供最完整的AOP实现(尽管Spring AOP非常强大)。相反,其目标是在AOP实现和Spring IoC之间提供紧密的集成,以帮助解决企业应用程序中的常见问题。
因此,例如,Spring框架的AOP功能通常与Spring IoC容器一起使用。方面是通过使用普通的bean定义语法来配置的(尽管这允许强大的“自动代理”功能)。这是与其他AOP实现的一个重要区别。使用Spring AOP不能轻松或有效地做一些事情,比如通知非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ是最佳选择。然而,我们的经验是,Spring AOP为企业级Java应用程序中的大多数问题提供了一个优秀的解决方案,这些应用程序支持AOP。
Spring AOP从不努力与AspectJ竞争,以提供全面的AOP解决方案。我们相信基于代理的框架(如Spring AOP)和成熟的框架(如AspectJ)都是有价值的,它们是互补的,而不是相互竞争的。Spring无缝地将Spring AOP和IoC与AspectJ集成在一起,从而在一致的基于Spring的应用程序体系结构中支持AOP的所有使用。这种集成不会影响Spring AOP API或AOP Alliance API。Spring AOP仍然是向后兼容的。请参阅下一章关于Spring AOP api的讨论。
Spring框架的核心原则之一是非侵入性。这就是不应该强迫您在业务或领域模型中引入特定于框架的类和接口的思想。但是,在某些地方,Spring框架确实允许您将Spring框架特定的依赖项引入到代码库中。提供这些选项的原因是,在某些情况下,以这种方式阅读或编写某些特定的功能片段可能更容易。然而,Spring框架(几乎)总是为您提供选择:您可以自由地做出明智的决定,决定哪个选项最适合您的特定用例或场景。
与本章相关的一个选择就是选择哪种AOP框架(以及哪种AOP风格)。您可以选择AspectJ、Spring AOP或两者都有。您还可以选择@AspectJ注释风格的方法或Spring XML配置风格的方法。本章选择首先引入@AspectJ风格的方法,这并不意味着Spring团队更喜欢@AspectJ注释风格的方法而不是Spring XML配置风格的方法。
请参阅选择使用哪种AOP声明风格,以更完整地讨论每种风格的“为什么和为什么”。
AOP代理
Spring AOP默认为AOP代理使用标准JDK动态代理。这使得任何接口(或一组接口)都可以被代理。
Spring AOP还可以使用CGLIB代理。这对于代理类而不是接口来说是必要的。默认情况下,如果业务对象没有实现接口,则使用CGLIB。由于根据接口而不是类编程是一种很好的实践,所以业务类通常实现一个或多个业务接口。强制使用CGLIB是可能的,在那些(希望很少)的情况下,你需要通知一个没有在接口上声明的方法,或者你需要将一个代理对象作为一个具体类型传递给一个方法。
了解Spring AOP是基于代理的这一事实很重要。请参阅理解AOP代理,以彻底了解这个实现细节的确切含义。
@Aspectj的支持
@AspectJ指的是一种将方面声明为用注释注释的常规Java类的风格。@AspectJ风格是作为AspectJ 5发行版的一部分由AspectJ项目引入的。Spring使用AspectJ提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注释。然而,AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或weaver。
使用AspectJ编译器和编织器可以使用完整的AspectJ语言,在在Spring应用程序使用AspectJ中进行了讨论。
开启@AspectJ支持
要在Spring配置中使用@AspectJ方面,您需要启用Spring支持,以便基于@AspectJ方面配置Spring AOP,并根据这些方面是否建议自动代理bean。通过自动代理,我们的意思是,如果Spring确定一个bean是由一个或多个方面通知的,它将自动为该bean生成一个代理,以拦截方法调用,并确保通知按需要运行。
可以通过XML或java风格的配置启用@AspectJ支持。在这两种情况下,您还需要确保AspectJ的aspectjweaver.jar
库位于应用程序的类路径中(1.8或更高版本)。该库可在AspectJ发行版的lib目录或Maven中央存储库中获得。
通过Java配置启用@AspectJ支持
要使用Java @Configuration
启用@AspectJ支持,请添加@EnableAspectJAutoProxy
注释,示例如下:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
通过XML配置启用@AspectJ支持
要通过基于xml的配置启用@AspectJ支持,请使用aop:aspectj autoproxy
元素,如下面的示例所示:
<aop:aspectj-autoproxy/>
这假设您使用基于XML模式的配置中描述的模式支持。请参阅AOP模式了解如何在AOP名称空间中导入标记。
声明一个方面
启用了@AspectJ支持后,Spring将自动检测在应用程序上下文中定义的任何带有@AspectJ方面(带有@Aspect
注释)类的bean,并用于配置Spring AOP。下面两个例子展示了一个不太有用的方面所需的最小定义。
两个示例中的第一个展示了应用程序上下文中的常规bean定义,该定义指向具有@Aspect
注释的bean类:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
两个示例中的第二个显示了NotVeryUsefulAspect
类定义,该定义使用org.aspectj.lang.annotation.Aspect
注释进行了注释;
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
与任何其他类一样,方面(用@Aspect
注释的类)可以有方法和字段。它们还可以包含切入点、通知和引入(类型间)声明。
通过组件扫描自动检测方面
您可以将方面类注册为Spring XML配置中的常规bean,或者通过类路径扫描自动检测它们——与任何其他Spring管理bean一样。但是,请注意@Aspect
注释不足以实现类路径中的自动检测。为了达到这个目的,您需要添加一个单独的@Component
注释(或者,根据Spring的组件扫描程序的规则,一个自定义的构造型注释)。
用其他方面通知一个方面?
在Spring AOP中,方面本身不能成为来自其他方面的通知的目标。类上的@Aspect注释将其标记为方面,因此将其排除在自动代理之外。
声明一个切入点
切入点确定感兴趣的连接点,从而使我们能够控制通知何时运行。Spring AOP只支持Spring bean的方法执行连接点,因此可以将切入点看作匹配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编程指南(以及扩展,AspectJ 5开发人员的笔记本)或有关AspectJ的书籍之一(如Colyer等人编写的Eclipse AspectJ,或Ramnivas Laddad编写的AspectJ in Action)。
支持切入点指示器
结合切入点表达式
共享公共的切入点定义
编写好的切入点
在编译期间,AspectJ处理切入点,以优化匹配性能。检查代码并确定每个连接点是否(静态或动态地)匹配给定的切入点是一个代价高昂的过程。(动态匹配意味着不能从静态分析中完全确定匹配,并且在代码中放置一个测试,以确定在代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ将其重写为匹配流程的最佳形式。这是什么意思?基本上,切入点是用DNF(析取范式)重写的,切入点的组件被排序,以便首先检查那些计算成本较低的组件。这意味着您不必担心理解各种切入点指示符的性能,可以在切入点声明中以任何顺序提供它们。
然而,AspectJ只能使用它被告知的内容。为了优化匹配性能,您应该考虑他们试图实现什么,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然分为三类:种类、范围和上下文:
- 类指示符选择特定类型的连接点:
execution
、get
、set
、call
和handler
。 - 范围指示符选择一组感兴趣的连接点(可能是多种连接点):
within
和withincode
- 上下文指示符基于上下文匹配(也可以绑定):
this
、target
和@annotation
一个编写良好的切入点至少应该包括前两种类型(类型和范围)。您可以包含上下文指示符来基于连接点上下文进行匹配,或者绑定该上下文以便在通知中使用。仅提供类指示符或仅提供上下文指示符可以工作,但由于额外的处理和分析,可能会影响编织性能(使用的时间和内存)。范围指示符匹配起来非常快,使用它们意味着AspectJ可以非常快地消除不应该进一步处理的连接点组。如果可能的话,一个好的切入点应该总是包含一个。
声明通知
通知与切入点表达式相关联,并在切入点匹配的方法执行之前、之后或前后运行。切入点表达式可以是对命名的切入点的简单引用,也可以是在适当位置声明的切入点表达式。
前置通知
返回后通知
抛出后通知
后置(Finally)通知
环绕通知
通知参数
Spring提供完全类型的通知,这意味着您需要在通知签名中声明所需的参数(正如我们在前面的返回和抛出示例中看到的那样),而不是一直使用Object[]
数组。在本节的后面部分,我们将看到如何使参数和其他上下文值对通知主体可用。首先,我们来看一下如何编写通用通知,以便找出当前通知建议的方法。
访问当前JoinPoint
向通知传递参数
通知参数和泛型
确定参数的名字
带参数执行
通知排序
当多个通知都想在同一个连接点上运行时,会发生什么情况?Spring AOP遵循与AspectJ相同的优先规则来确定通知执行的顺序。最高优先级的建议在“进入时”先运行(因此,给定两个before建议,具有最高优先级的先运行)。在连接点上,优先级最高的建议最后运行(因此,给定两个after建议,优先级最高的将排在第二位)。
当在不同方面中定义的两条通知都需要在同一个连接点上运行时,除非您另有指定,否则执行顺序是未定义的。您可以通过指定优先级来控制执行的顺序。通过在aspect类中实现org.springframework.core.Ordered
接口或用@Order
注释对其进行注释,可以用正常的Spring方式完成这一操作。给定两个方面,从Ordered.getOrder()
返回较低值(或注释值)的方面具有较高的优先级。
引入
引入(在AspectJ中称为类型间声明)使方面能够声明被通知的对象实现给定的接口,并代表这些对象提供该接口的实现。
您可以使用@DeclareParents
注释进行介绍。这个注释用于声明匹配的类型有一个新的父类型(因此有了这个名称)。例如,给定一个名为UsageTracked
的接口和一个名为DefaultUsageTracked
的接口实现,下面的方面声明服务接口的所有实现者也实现了UsageTracked
接口(例如,通过JMX进行统计):
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
要实现的接口由带注释字段的类型决定。@DeclareParents
注释的value
属性是一个AspectJ类型模式。任何匹配类型的bean都实现UsageTracked
接口。请注意,在前面示例的before建议中,服务bean可以直接用作UsageTracked接口的实现。如果以编程方式访问一个bean,您应该编写以下代码:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
方面实例化模型
默认情况下,在应用程序上下文中每个方面都有一个单独的实例。AspectJ称之为单例实例化模型。可以用不同的生命周期来定义方面。Spring支持AspectJ的perthis
和pertarget
实例化模型;目前不支持perflow
、perflowbelow
和pertypewithin
。
可以通过在@Aspect
注释中指定perthis
子句来声明perthis
方面。考虑下面的例子:
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {
private int someState;
@Before("com.xyz.myapp.CommonPointcuts.businessService()")
public void recordServiceUsage() {
// ...
}
}
在前面的示例中,perthis
子句的效果是为执行业务服务的每个惟一的服务对象创建一个方面实例(每个唯一的对象在切入点表达式匹配的连接点上绑定到this
)。方面实例是在第一次在服务对象上调用方法时创建的。当服务对象超出范围时,方面也超出了范围。在创建方面实例之前,其中的任何通知都不会运行。一旦创建了方面实例,其中声明的通知就会在匹配的连接点上运行,但只有当服务对象是与此方面相关联的对象时才会如此。有关per
子句的更多信息,请参阅AspectJ编程指南。
pertarget
实例化模型的工作方式与perthis
完全相同,但是它在匹配的连接点上为每个惟一的目标对象创建一个方面实例。
AOP的例子
现在您已经了解了所有组成部分的工作方式,我们可以将它们放在一起做一些有用的事情。
业务服务的执行有时会由于并发性问题而失败(例如,死锁失败)。如果重试该操作,下一次尝试很可能会成功。对于适合在这样的条件下重试的业务服务(幂等操作不需要返回到用户以解决冲突),我们希望透明地重试操作,以避免客户机看到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
失败,我们再次尝试,除非我们用尽了所有的重试尝试。
对应的弹簧配置如下:
<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
为了改进方面,使它只重试幂等操作,我们可以定义以下幂等注释:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
然后,我们可以使用该注释来注释服务操作的实现。对方面的更改是只重试幂等操作,这涉及到细化切入点表达式,以便只有@Idempotent
操作匹配,如下所示:
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// ...
}
基于模式的AOP支持
声明一个方面
声明一个切入点
声明通知
引入
方面实例化模型
顾问
“顾问”的概念来自Spring中定义的AOP支持,在AspectJ中没有直接的对等物。顾问就像一个独立的小方面,只有一条建议。通知本身由bean表示,必须实现Spring中的通知类型中描述的通知接口之一。顾问可以利用AspectJ切入点表达式。
Spring通过<aop:advisor>
元素支持advisor概念。您最常看到它与事务通知一起使用,后者在Spring中也有自己的名称空间支持。下面的例子显示了一个advisor:
<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
属性内联定义一个切入点表达式。
要定义顾问的优先级,以便建议可以参与排序,请使用order
属性定义顾问的有序值。
一个AOP模式的例子
选择使用哪种AOP声明风格
Spring AOP还是Full AspectJ?
@AspectJ还是用于Spring AOP的XML?
混合方面类型
通过使用自动代理支持、模式定义的<aop:aspect>
方面、<aop:advisor>
声明的顾问,甚至在同一配置中使用其他风格的代理和拦截器,完全有可能混合使用@AspectJ风格的方面。所有这些都是通过使用相同的底层支持机制实现的,可以毫无困难地共存。
代理机制
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代理,请将<aop:aspectj autoproxy>
元素的proxy-target-class
属性设置为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代理。
理解AOP代理
Spring AOP是基于代理的。在编写自己的方面或使用Spring框架提供的任何基于Spring aop的方面之前,掌握最后一条语句的实际含义是非常重要的。
首先考虑这样一个场景,您有一个普通的、未代理的、没有任何特殊之处的、直接的对象引用,如下面的代码片段所示:
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();
在Spring应用程序中使用AspectJ
到目前为止,我们在这一章中所涉及的一切都是纯Spring AOP。在本节中,我们将了解如果您的需求超出了Spring AOP单独提供的功能,那么如何使用AspectJ编译器或weaver来代替Spring AOP,或者在Spring AOP之外使用。
Spring附带了一个小的AspectJ方面库,它可以在您的发行版中作为spring-aspects.jar
单独使用。您需要将其添加到类路径中,以便使用其中的方面。使用AspectJ向Spring依赖注入域对象以及AspectJ的其他Spring方面将讨论这个库的内容以及如何使用它。通过使用Spring IoC配置AspectJ方面讨论了如何依赖注入使用AspectJ编译器编织的AspectJ方面。最后,在Spring框架中使用AspectJ进行加载时编织介绍了使用AspectJ的Spring应用程序的加载时编织。
使用AspectJ向Spring依赖注入域对象
Spring容器实例化并配置在应用程序上下文中定义的bean。给定包含要应用的配置的bean定义的名称,还可以要求bean工厂配置已存在的对象。spring-aspects.jar
包含一个注释驱动的方面,它利用这个功能来允许任何对象的依赖项注入。该支持旨在用于在任何容器控制之外创建的对象。域对象通常属于这一类,因为它们通常是使用new
操作符以编程方式创建的,或者是通过ORM工具作为数据库查询的结果创建的。
@Configurable
注释将一个类标记为符合spring驱动配置的条件。在最简单的情况下,您可以纯粹地将它用作标记注释,如下面的示例所示:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable
public class Account {
// ...
}
当以这种方式作为标记接口使用时,Spring通过使用与完全限定类型名称(com.xyz.myapp.domain.Account)相同的bean定义(通常是原型作用域)来配置注释类型(在本例中是Account
)的新实例。由于bean的默认名称是其类型的完全限定名,声明原型定义的一种方便方法是省略id
属性,如下面的示例所示:
<bean class="com.xyz.myapp.domain.Account" scope="prototype">
<property name="fundsTransferService" ref="fundsTransferService"/>
</bean>
如果想显式地指定要使用的原型bean定义的名称,可以直接在注释中这样做,如下面的示例所示:
Spring现在寻找名为account
的bean定义,并使用它作为配置新Account
实例的定义。
您还可以使用自动装配来避免指定专用bean定义。要让Spring应用自动装配,请使用@Configurable
注释的autowire
属性。您可以指定@Configurable(autowire= autowire.BY_TYPE)
或@Configurable(autowire= autowire.BY_NAME)
用于分别根据类型或名称自动装配。作为一种替代方案,最好是在字段或方法级别通过@Autowired
或@Inject
为您的@Configurable
bean指定显式的、注释驱动的依赖注入(有关进一步细节,请参阅基于注释的容器配置)。
最后,您可以使用dependencyCheck
属性(例如,@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true)
)来为新创建和配置的对象引用启用Spring依赖项检查。如果这个属性被设置为true, Spring在配置之后会验证所有的属性(不是基本类型或集合)已经被设置。
注意,单独使用注释不会产生任何效果。是spring-aspects.jar
中的AnnotationBeanConfigurerAspect
对注释的存在起作用。本质上,方面表示,在初始化一个用@Configurable
注释的类型的新对象返回后,使用Spring根据注释的属性配置新创建的对象。在这个上下文中,“初始化”指的是新实例化的对象(例如,用new
操作符实例化的对象)以及正在进行反序列化的Serializable
对象(例如,通过readResolve())。
以上段落中的一个关键短语是“in essence”。在大多数情况下,“从新对象初始化返回后”的确切语义是正确的。在这个上下文中,“初始化后”意味着依赖关系是在对象构造完成之后注入的。这意味着依赖项不能在类的构造函数体中使用。如果你想要在构造函数体运行之前注入依赖项,从而可以在构造函数体中使用,你需要在
@Configurable
声明中定义它,如下所示:@Configurable(preConstruction = true)
您可以在AspectJ编程指南的这个附录中找到关于各种切入点类型的语言语义的更多信息。
要做到这一点,带注释的类型必须使用AspectJ编织器进行编织。您可以使用构建时Ant或Maven任务来完成这一任务(例如,请参阅AspectJ开发环境指南),也可以使用加载时编织(请参阅Spring框架中使用AspectJ进行加载时编织)。AnnotationBeanConfigurerAspect
本身需要由Spring进行配置(以便获得用于配置新对象的对bean工厂的引用)。如果您使用基于java的配置,您可以添加@EnableSpringConfigured
到任何@Configuration
类,如下所示:
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
如果您更喜欢基于XML的配置,那么Spring context
名称空间定义了一个方便的context:spring-configured
元素,您可以如下所示:
<context:spring-configured/>
在配置方面之前创建的@Configurable
对象实例会导致向调试日志发出一条消息,并且不会发生对象的配置。一个例子可能是Spring配置中的bean,它在被Spring初始化时创建域对象。在这种情况下,您可以使用depends-on
bean属性手动指定该bean依赖于配置方面。下面的示例演示如何使用depends-on
属性:
<bean id="myService"
class="com.xzy.myapp.service.MyService"
depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">
<!-- ... -->
</bean>
不要通过bean配置器方面激活
@Configurable
处理,除非您真的想在运行时依赖它的语义。特别是,确保不要在作为常规Spring bean注册到容器中的bean类上使用@Configurable
。这样做会导致双重初始化,一次通过容器,一次通过方面。
单元测试@Configurable
对象
@Configurable
支持的目标之一是支持域对象的独立单元测试,而不存在与硬编码查找相关的困难。如果@Configurable
类型没有被AspectJ编织,那么注释在单元测试期间没有任何影响。您可以在测试对象中设置模拟或存根属性引用,并照常进行。如果@Configurable
类型已经被AspectJ编织,您仍然可以正常地在容器外进行单元测试,但是每次您构造@Configurable
对象时都会看到一条警告消息,指示它没有被Spring配置。
使用多个应用程序上下文
用于实现@Configurable
支持的AnnotationBeanConfigurerAspect
是一个AspectJ单例方面。单例方面的作用域与static
成员的作用域相同:每个类加载器都有一个方面实例来定义类型。这意味着,如果在同一个类加载器层次结构中定义多个应用程序上下文,则需要考虑在何处定义@EnableSpringConfigured
bean,以及在类路径的何处放置spring-aspects.jar
。
考虑一个典型的Spring web应用程序配置,它有一个共享的父应用程序上下文,定义了公共业务服务、支持这些服务所需的一切,以及每个servlet(包含特定于该servlet的定义)的一个子应用程序上下文。所有这些上下文都共存于同一个类加载器层次结构中,因此AnnotationBeanConfigurerAspect
只能保存对其中一个上下文的引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义@EnableSpringConfigured
bean。这定义了您可能想要注入到域对象中的服务。其结果是,您无法通过使用@Configurable
机制(这可能不是您想要做的事情)来使用对子(特定于servlet)上下文中定义的bean的引用来配置域对象。
当在同一个容器中部署多个web应用程序时,确保每个web应用程序通过使用自己的类加载器(例如,通过将spring-aspects.jar
放在'WEB-INF/lib'
中)加载spring-aspect.jar
中的类型。如果spring-aspects.jar
只添加到容器范围的类路径中(因此由共享的父类加载器加载),那么所有web应用程序都共享相同的方面实例(这可能不是您想要的)。
AspectJ的其他Spring方面
除了@Configurable
方面之外,spring-aspects.jar
还包含一个AspectJ方面,您可以使用这个方面来驱动Spring对用@Transactional
注释注释的类型和方法的事务管理。这主要是为那些希望在Spring容器之外使用Spring框架事务支持的用户准备的。
解释@Transactional
注释的方面是AnnotationTransactionAspect
。当您使用这个方面时,您必须注释实现类(或该类中的方法或两者),而不是该类实现的接口(如果有的话)。AspectJ遵循Java的规则,即接口上的注释不继承。
类上的@Transactional
注释为类中任何公共操作的执行指定了默认事务语义。
类中方法上的@Transactional
注释会覆盖类注释给出的默认事务语义(如果存在的话)。任何可见性的方法都可以被注释,包括私有方法。直接注释非公共方法是为这些方法的执行获得事务界定的唯一方法。
从Spring Framework 4.2开始,
spring-aspects
提供了一个类似的方面,为标准javax.transaction.Transactional
注释提供了完全相同的特性。查看JtaAnnotationTransactionAspect
了解更多细节。
对于想要使用Spring配置和事务管理支持但不想(或不能)使用注释的AspectJ程序员,spring-aspects.jar
还包含了abstract
的方面,您可以扩展这些方面以提供自己的切入点定义。有关更多信息,请参阅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);
}
通过使用Spring IoC配置AspectJ方面
当您在Spring应用程序中使用AspectJ方面时,很自然地希望并期望能够使用Spring配置这些方面。AspectJ运行时本身负责方面的创建,并且通过Spring配置AspectJ创建的方面的方法依赖于方面使用的AspectJ实例化模型(per-xxx
子句)。
大多数AspectJ方面都是单例方面。这些方面的配置很容易。您可以创建一个照常引用方面类型的bean定义,并包含factory-method="aspectOf"
bean属性。这确保了Spring通过请求AspectJ而不是尝试自己创建一个实例来获得方面实例。下面的示例演示如何使用factory-method="aspectOf"
属性:
<bean id="profiler" class="com.xyz.profiler.Profiler"
factory-method="aspectOf">
<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
注意factory-method="aspectOf"
属性
非单例方面更难配置。但是,可以通过创建原型bean定义,并使用spring-aspects.jar
中的@Configurable
支持来配置aspect实例,一旦它们有了由AspectJ运行时创建的bean。
如果你有@AspectJ方面你想编织与AspectJ域模型(例如,使用装入时编织类型)和其他您想要使用Spring AOP @AspectJ方面,而这些方面都是配置在Spring,您需要告诉Spring AOP @AspectJ自动代理支持,配置中定义的@AspectJ方面的哪个确切子集应该用于自动代理。可以通过在<aop:aspectj-autoproxy/>
声明中使用一个或多个<include/>
元素来实现这一点。每个<include/>
元素指定一个名称模式,只有名称与至少一个模式匹配的bean才用于Spring AOP自动代理配置。下面的例子展示了如何使用<include/>
元素:
<aop:aspectj-autoproxy>
<aop:include name="thisBean"/>
<aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
不要被<aop:aspectj-autoproxy/>
元素的名称所误导。使用它会导致Spring AOP代理的创建。这里使用了@AspectJ风格的方面声明,但没有涉及AspectJ运行时。
在Spring框架中使用AspectJ进行加载时编织
加载时编织(LTW)是指在将AspectJ方面加载到Java虚拟机(JVM)时将它们编织到应用程序的类文件中的过程。本节的重点是在Spring框架的特定上下文中配置和使用LTW。本节不是对LTW的一般介绍。关于LTW和仅使用AspectJ配置LTW(完全不涉及Spring)的详细信息,请参阅AspectJ开发环境指南的LTW部分。
Spring框架给AspectJ LTW带来的价值在于支持对编织过程进行更细粒度的控制。“普通的”AspectJ LTW是通过使用Java(5+)代理来实现的,该代理在启动JVM时通过指定VM参数来打开。因此,这是一种jvm范围的设置,在某些情况下可能还不错,但通常有点太粗糙了。启用spring的LTW允许您在每个类加载器的基础上打开LTW,这更细粒度,在“单jvm -多应用程序”环境中更有意义(比如在典型的应用服务器环境中)。
此外,在某些环境中,这种支持开启加载时编织,而不需要对应用服务器的启动脚本进行任何修改,该脚本需要添加-javaagent:path/to/aspectjweaver.jar
或(我们将在本节后面描述)-javaagent:path/to/spring-tool.jar
。开发人员配置应用程序上下文以开启加载时编织,而不是依赖通常负责部署配置(如启动脚本)的管理员。
现在销售宣传已经结束,让我们首先浏览一个使用Spring的AspectJ LTW的快速示例,然后是示例中介绍的元素的详细细节。有关完整的示例,请参见Petclinic示例应用程序。
第一个例子
假设您是一名应用程序开发人员,其任务是诊断系统中某些性能问题的原因。我们将切换到一个简单的分析方面,让我们快速获得一些性能指标,而不是单独使用一个分析工具。然后,我们可以立即对该特定区域应用更细粒度的分析工具。
这里给出的示例使用XML配置。您还可以配置和使用@AspectJ与Java配置。具体来说,您可以使用
@EnableLoadTimeWeaving
注释作为<context:load-time-weaver/>
的替代方法(参见下面的详细信息)。
下面的示例显示了分析方面,这并不花哨。它是一个基于时间的分析器,使用@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编织器我们想要将ProfilingAspect
编织到我们的类中。这个文件约定,即在称为META-INF/aop.xml
的Java类路径上存在一个(或多个)文件,是标准的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在每个类加载器的基础上选择性地打开LTW,这是真的。然而,对于本例,我们使用Java代理(随Spring提供)来打开LTW。我们使用以下命令来运行前面所示的Main
类:
java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main
-javaagent
是一个标志,用于指定和启用代理来检测运行在JVM上的程序。Spring框架附带了这样一个代理,InstrumentationSavingAgent
,它被打包在spring-instrument.jar
中,在前面的示例中,这个jar是作为-javaagent
参数的值提供的。
执行主程序的输出与下一个示例类似。(我已经在calculateEntitlement()
实现中引入了Thread.sleep(..)
语句,以便分析器实际捕获的不是0毫秒(01234
毫秒不是AOP引入的开销)。下面的清单显示了我们运行分析器时得到的输出:
Calculating entitlement
StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms % Task name
------ ----- ----------------------------
01234 100% calculateEntitlement
由于这个LTW是通过使用成熟的AspectJ来实现的,所以我们不仅仅局限于建议Spring bean。下面对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支持的基本内容已经在前面的示例中介绍过了,本节的其余部分将详细解释每个配置和使用背后的“原因”。
本例中使用的
ProfilingAspect
可能是基本的,但它非常有用。这是开发时间方面的一个很好的例子,开发人员可以在开发期间使用它,然后轻松地将其排除在部署到UAT或生产中的应用程序的构建中。
Aspects
在LTW中使用的方面必须是AspectJ方面。您可以用AspectJ语言本身编写它们,也可以用@AspectJ风格编写您的方面。那么,你的方面就是有效的AspectJ和Spring AOP方面。此外,编译后的方面类需要在类路径上可用。
‘META-INF/aop.xml’
AspectJ LTW基础设施是通过使用Java类路径上的一个或多个META-INF/aop.xml
文件来配置的(直接或更典型地,在jar文件中)。
AspectJ参考文档的LTW部分详细介绍了该文件的结构和内容。因为aop.xml
文件是100%的AspectJ,所以我们在这里不再进一步描述它。
Required libraries (JARS)
至少,你需要以下库来使用Spring框架对AspectJ LTW的支持:
spring-aop.jar
aspectjweaver.jar
如果使用spring提供的代理来启用检测,还需要:
spring-instrument.jar
Spring配置
Spring的LTW支持中的关键组件是LoadTimeWeaver
接口(在org.springframework.instrument.classloading
包中),以及Spring发行版附带的许多实现。LoadTimeWeaver
负责在运行时向ClassLoader
添加一个或多个java.lang.instrument.ClassFileTransformers
,这为所有有趣的应用程序打开了大门,其中之一就是方面的LTW。
如果您不熟悉运行时类文件转换的概念,在继续之前,请参阅
java.lang.instrument
包的javadoc API文档。虽然该文档并不全面,但至少可以看到关键的接口和类(在阅读本节时可以参考)。
为特定的ApplicationContext
配置LoadTimeWeaver
只需添加一行即可。(请注意,您几乎肯定需要使用ApplicationContext
作为您的Spring容器——通常,一个BeanFactory
是不够的,因为LTW支持使用BeanFactoryPostProcessors
。)
要启用Spring框架的LTW支持,您需要配置LoadTimeWeaver
,这通常通过使用@EnableLoadTimeWeaving
注释来完成,如下所示:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
另外,如果您更喜欢基于xml的配置,可以使用<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
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
和AspectJWeaveEnabler
。默认的LoadTimeWeaver
是DefaultContextLoadTimeWeaver
类,它尝试装饰一个自动检测到的LoadTimeWeaver
。“自动检测”的确切LoadTimeWeaver
类型取决于您的运行时环境。下表总结了各种LoadTimeWeaver
实现:
运行时环境 | LoadTimeWeaver 实现 |
---|---|
在Apache Tomcat中运行 | TomcatLoadTimeWeaver |
在GlassFish中运行(仅限于EAR部署) | GlassFishLoadTimeWeaver |
JBoss AS或WildFly | JBossLoadTimeWeaver |
在IBM的WebSphere中运行 | WebSphereLoadTimeWeaver |
运行在Oracle的WebLogic中 | WebLogicLoadTimeWeaver |
JVM从Spring InstrumentationSavingAgent开始(java -javaagent:path/to/spring-instrument.jar ) | InstrumentationLoadTimeWeaver |
回退,期望底层的类加载器遵循通用约定(即addTransformer 和可选的getThrowawayClassLoader 方法) | ReflectiveLoadTimeWeaver |
请注意,该表只列出了当您使用DefaultContextLoadTimeWeaver
时自动检测到的LoadTimeWeavers
。您可以指定要使用哪个LoadTimeWeaver
实现。
要使用Java配置指定特定的LoadTimeWeaver
,请实现LoadTimeWeavingConfigurer
接口并覆盖getLoadTimeWeaver()
方法。以下示例指定了ReflectiveLoadTimeWeaver
:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {
@Override
public LoadTimeWeaver getLoadTimeWeaver() {
return new ReflectiveLoadTimeWeaver();
}
}
如果您使用基于xml的配置,您可以将完全限定的类名指定为<context:load-time-weaver/>
元素上的weaver-class
属性的值。同样,下面的示例指定了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
稍后可以使用众所周知的名称loadTimeWeaver
从Spring容器中检索。请记住,LoadTimeWeaver
仅作为Spring的LTW基础设施添加一个或多个ClassFileTransformers
的机制而存在。实际执行LTW的ClassFileTransformer
是ClassPreProcessorAgentAdapter
(来自org.aspectj.weaver.loadtime
包)类。请参阅ClassPreProcessorAgentAdapter
类的类级javadoc以获取更多细节,因为编织实际上是如何影响的细节超出了本文档的范围。
还有最后一个配置属性需要讨论:aspectjWeaving
属性(如果使用XML,则称为aspectj-weaving
)。此属性控制是否启用LTW。它接受三个可能值中的一个,如果属性不存在,则默认值为autodetect
。下表总结了三种可能的值:
注释的值 | XML值 | 说明 |
---|---|---|
ENABLED | on | AspectJ编织开始了,并且在加载时适当地编织方面。 |
DISABLED | off | LTW是关闭的。加载时没有任何方面被编织。 |
AUTODETECT | autodetect | 如果Spring LTW基础设施能够找到至少一个META-INF/aop.xml 文件,那么就开启了AspectJ编织。否则为off。这是默认值 |
特定于环境的配置
最后一节包含在应用服务器和web容器等环境中使用Spring的LTW支持时需要的任何额外设置和配置。
Tomcat, JBoss, WebSphere, WebLogic
Tomcat、JBoss/WildFly、IBM WebSphere Application Server和Oracle WebLogic Server都提供了一个通用的应用程序ClassLoader
,它能够进行本地检测。Spring的原生LTW可以利用这些类加载器实现来提供AspectJ编织。您可以简单地启用加载时编织,如前面所述。具体来说,您不需要修改JVM启动脚本来添加-javaagent:path/to/spring-instrument.jar
。
注意,在JBoss上,可能需要禁用应用服务器扫描,以防止它在应用程序实际启动之前加载类。一个快速的解决方法是在工件中添加一个名为WEB-INF/jboss-scans.xml
的文件,包含以下内容:
<scanning xmlns="urn:jboss:scanning:1.0"/>
泛型Java应用程序
当在特定LoadTimeWeaver
实现不支持的环境中需要类插装时,JVM代理是一般的解决方案。对于这种情况,Spring提供了InstrumentationLoadTimeWeaver
,它需要一个特定于Spring(但非常通用)的JVM代理spring-instrument.jar
,通过常见的@EnableLoadTimeWeaving
和<context:load-time-weaver/>
设置自动检测。
要使用它,必须通过提供以下JVM选项来使用Spring代理启动虚拟机:
-javaagent:/path/to/spring-instrument.jar
请注意,这需要修改JVM启动脚本,这可能会阻止您在应用程序服务器环境中使用它(取决于您的服务器和操作策略)。也就是说,对于“一个应用程序一个JVM”的部署,比如独立的Spring引导应用程序,通常在任何情况下都可以控制整个JVM的设置。
更多资源
关于AspectJ的更多信息可以在AspectJ网站上找到。
Spring AOP api
前一章描述了Spring使用@AspectJ和基于模式的方面定义对AOP的支持。在本章中,我们将讨论低级的Spring AOP api。对于常见的应用程序,我们建议像前一章所描述的那样使用带有AspectJ切入点的Spring AOP。
Spring中的切入点API
本节描述Spring如何处理关键的切入点概念。
概念
Spring的切入点模型支持独立于通知类型的切入点重用。可以用相同的切入点定位不同的通知。
org.springframework.aop.Pointcut
接口是中心接口,用于向特定类和方法发送通知。完整的接口如下:
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
将Pointcut
接口拆分为两部分允许重用类和方法匹配部分以及细粒度的组合操作(例如与另一个方法匹配器执行联合)。
ClassFilter
接口用于将切入点限制为一组给定的目标类。如果matches()
方法总是返回true,则匹配所有目标类。下面的清单显示了ClassFilter
接口定义:
public interface ClassFilter {
boolean matches(Class clazz);
}
MethodMatcher
接口通常更重要。完整的接口如下:
public interface MethodMatcher {
boolean matches(Method m, Class targetClass);
boolean isRuntime();
boolean matches(Method m, Class targetClass, Object[] args);
}
matches(Method, Class)
方法用于测试这个切点是否匹配目标类上的给定方法。这种评估可以在创建AOP代理时执行,以避免每次方法调用都需要测试。如果两个参数的matches
方法对于一个给定的方法返回true
,而MethodMatcher的isRuntime()
方法返回true
,那么在每次调用方法时都会调用三个参数匹配的方法。这让切入点在目标通知开始之前查看传递给方法调用的参数。
大多数MethodMatcher
实现都是静态的,这意味着它们的isRuntime()
方法返回false
。在本例中,永远不会调用三个参数的matches
方法。
如果可能的话,尝试将切入点设为静态的,以便在创建AOP代理时允许AOP框架缓存切入点计算的结果。
切入点的操作
Spring支持切入点上的操作(尤其是并和交集)。
并集表示两个切入点匹配的方法。交集意味着两个切入点匹配的方法。并集通常更有用。您可以通过使用org.springframework.aop.support.Pointcuts
类中的静态方法或同一包中的ComposablePointcut
类来组合切入点。然而,使用AspectJ切入点表达式通常是一种更简单的方法。
AspectJ切入点表达式
自2.0以来,Spring使用的最重要的切入点类型是org.springframework.aop.aspectj.AspectJExpressionPointcut
。这是一个使用AspectJ提供的库来解析AspectJ切入点表达式字符串的切入点。
有关受支持的AspectJ切入点原语的讨论,请参阅前一章。
方便的切入点实现
Spring提供了几个方便的切入点实现。你可以直接使用其中一些;其他的则打算在应用程序特定的切入点中子类化。
静态的切入点
静态切入点基于方法和目标类,不能考虑方法的参数。对于大多数用法,静态切入点就足够了——而且是最好的。在第一次调用方法时,Spring只能计算一次静态切入点。在此之后,不需要在每次方法调用时再次计算切入点。
本节的其余部分将描述Spring中包含的一些静态切入点实现。
正则表达式的切入点
指定静态切入点的一种明显方法是正则表达式。除了Spring之外,还有一些AOP框架使这成为可能。org.springframework.aop.support.JdkRegexpMethodPointcut
是一个通用的正则表达式切入点,它使用JDK中的正则表达式支持。
使用JdkRegexpMethodPointcut
类,您可以提供一个模式字符串列表。如果其中任何一个匹配,那么切入点的计算结果为true
。(因此,产生的切入点实际上是指定模式的并集。)
下面的例子展示了如何使用JdkRegexpMethodPointcut
:
<bean id="settersAndAbsquatulatePointcut"
class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
Spring提供了一个名为RegexpMethodPointcutAdvisor
的方便类,它允许我们也引用一个Advice
(记住,Advice
可以是一个拦截器,前置通知,抛出通知,等等)。在幕后,Spring使用JdkRegexpMethodPointcut
。使用RegexpMethodPointcutAdvisor
简化了连接,因为一个bean封装了切入点和通知,如下面的示例所示:
<bean id="settersAndAbsquatulateAdvisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref bean="beanNameOfAopAllianceInterceptor"/>
</property>
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
你可以对任何Advice
类型使用RegexpMethodPointcutAdvisor
。
属性驱动的切入点
静态切入点的一种重要类型是元数据驱动的切入点。这将使用元数据属性的值(通常是源级元数据)。
动态的切入点
动态切入点的评估成本比静态切入点高。它们考虑了方法参数和静态信息。这意味着它们必须在每次方法调用时进行计算,并且不能缓存结果,因为参数会有所不同。
主要的例子是control flow
切入点。
控制流的切入点
Spring控制流切入点在概念上类似于AspectJ cflow
切入点,尽管没有那么强大。(目前没有办法指定一个切入点在与另一个切入点匹配的连接点下面运行。)控制流切入点与当前调用堆栈匹配。例如,如果连接点被com.mycompany.web
包中的方法或SomeCaller
类调用,则可能触发。控制流切入点是通过使用org.springframework.aop.support.ControlFlowPointcut
类指定的。
控制流切入点在运行时的评估成本明显高于其他动态切入点。在Java 1.4中,成本大约是其他动态切入点的5倍。
切入点超类
Spring提供了有用的切入点超类来帮助您实现自己的切入点。
因为静态切入点最有用,所以您可能应该继承StaticMethodMatcherPointcut
。这只需要实现一个抽象方法(尽管您可以覆盖其他方法来定制行为)。下面的例子展示了如何子类化StaticMethodMatcherPointcut
:
class TestStaticPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method m, Class targetClass) {
// return true if custom criteria match
}
}
还有用于动态切入点的超类。可以对任何通知类型使用自定义切入点。
自定义切入点
因为Spring AOP中的切入点是Java类而不是语言特性(如AspectJ),所以可以声明定制的切入点,无论是静态的还是动态的。Spring中的自定义切入点可以任意复杂。但是,如果可以的话,我们建议使用AspectJ切入点表达式语言。
Spring的后期版本可能提供对JAC所提供的“语义切入点”的支持——例如,“更改目标对象中的实例变量的所有方法”。
Spring中的通知API
现在我们来看看Spring AOP是如何处理通知的。
通知的生命周期
每个建议都是Spring bean。通知实例可以跨所有被通知对象共享,也可以对每个被通知对象惟一。这对应于每个类或每个实例的通知。
每类通知是最常用的。它适用于一般的通知,比如事务顾问。它们不依赖于代理对象的状态或添加新状态。它们只是根据方法和参数来行动。
每个实例的通知适用于引入,以支持mixin。在本例中,通知将状态添加到代理对象。
可以在同一个AOP代理中混合使用共享通知和每个实例通知。
Spring中的通知类型
Spring提供了几种通知类型,并可扩展以支持任意通知类型。介绍通知的基本概念和标准通知类型。
拦截Around通知
Spring中最基本的通知类型是围绕通知的拦截。
Spring与AOP Alliance
接口兼容,用于围绕使用方法拦截的通知。实现MethodIntercepto
r和实现around advice的类也应该实现以下接口:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
invoke()
方法的MethodInvocation
参数公开了被调用的方法、目标连接点、AOP代理和方法的参数。invoke()
方法应该返回调用的结果:连接点的返回值。
下面的例子展示了一个简单的MethodInterceptor
实现:
public class DebugInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("Before: invocation=[" + invocation + "]");
Object rval = invocation.proceed();
System.out.println("Invocation returned");
return rval;
}
}
注意对MethodInvocation
的proceed()
方法的调用。这将沿着拦截器链向连接点前进。大多数拦截器调用这个方法并返回它的返回值。然而,与任何around通知一样,MethodInterceptor
可以返回不同的值或抛出异常,而不是调用proceed方法。但是,如果没有充分的理由,您不会想这样做。
MethodInterceptor
实现提供了与其他AOP联盟兼容的AOP实现的互操作性。本节其余部分讨论的其他通知类型以特定于spring的方式实现了常见的AOP概念。虽然使用最具体的通知类型有一个优势,但如果您想在另一个AOP框架中运行方面,请坚持使用围绕通知的MethodInterceptor
。注意,切入点目前还不能在框架之间互操作,而且AOP联盟目前也没有定义切入点接口。
前置通知
更简单的通知类型是前置通知。这并不需要MethodInvocation
对象,因为它只在进入方法之前被调用。
前置通知的主要优点是不需要调用proceed()
方法,因此,不存在无意中沿着拦截器链继续失败的可能性。
下面的清单显示了MethodBeforeAdvice
接口:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object[] args, Object target) throws Throwable;
}
(Spring的API设计允许字段前置通知,尽管通常的对象适用于字段拦截,而且Spring不太可能实现它。)
注意,返回类型是void
。前置通知可以在连接点运行之前插入自定义行为,但不能更改返回值。如果前置通知抛出异常,它将停止拦截器链的进一步执行。异常沿拦截器链反向传播。如果它未被检查或在被调用方法的签名上,则直接将它传递给客户端。否则,它将被包装在AOP代理未检查的异常中。
下面的例子展示了Spring中的前置通知,它计算了所有的方法调用:
public class CountingBeforeAdvice implements MethodBeforeAdvice {
private int count;
public void before(Method m, Object[] args, Object target) throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
前置通知可以用于任何切入点。
抛出通知
如果连接点抛出异常,则在连接点返回后调用Throws通知。Spring提供输入的抛出建议。注意,这意味着org.springframework.aop.ThrowsAdvice
接口不包含任何方法。它是一个标记接口,标识给定对象实现了一个或多个类型化的抛出通知方法。这些应该是以下形式:
afterThrowing([Method, args, target], subclassOfThrowable)
只需要最后一个参数。方法签名可能有一个或四个参数,这取决于通知方法是否对方法和参数感兴趣。下面两个清单显示了抛出通知的示例类。
如果抛出RemoteException
(包括子类),将调用以下通知:
public class RemoteThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
}
与前面的通知不同,下一个示例声明了四个参数,这样它就可以访问被调用的方法、方法参数和目标对象。如果抛出ServletException
,将调用以下通知:
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
最后一个示例说明了如何在一个同时处理RemoteException
和ServletException
的类中使用这两个方法。任何数量的抛出通知方法都可以组合在一个类中。下面的清单显示了最后一个示例:
public static class CombinedThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
如果throw-advice方法本身抛出异常,它将覆盖原始异常(也就是说,它更改抛出给用户的异常)。覆盖异常通常是一个RuntimeException,它与任何方法签名兼容。但是,如果throw-advice方法抛出一个检查异常,它必须与目标方法声明的异常相匹配,因此在某种程度上与特定的目标方法签名相耦合。不要抛出与目标方法签名不兼容的未声明的检查异常!
抛出通知可以与任何切入点一起使用。
后置通知
Spring中的后置通知必须实现org.springframework.aop.AfterReturningAdvice
接口,如下面的清单所示:
public interface AfterReturningAdvice extends Advice {
void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable;
}
后置通知可以访问返回值(它不能修改)、被调用的方法、方法的参数和目标。
下面后置通知将计算所有未抛出异常的成功方法调用:
public class CountingAfterReturningAdvice implements AfterReturningAdvice {
private int count;
public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
这个通知不会改变执行路径。如果抛出异常,抛出的是拦截器链,而不是返回值。
后置通知可以与任何切入点一起使用。
引入通知
Spring将引入通知视为一种特殊的拦截通知。
Introduction需要一个IntroductionAdvisor
和一个IntroductionInterceptor
,它们实现了以下接口:
public interface IntroductionInterceptor extends MethodInterceptor {
boolean implementsInterface(Class intf);
}
从AOP Alliance MethodInterceptor
接口继承的invoke()
方法必须实现引入部分。也就是说,如果被调用的方法在引入的接口上,则引入拦截器负责处理方法调用——它不能调用proceed()
。
引入通知不能与任何切入点一起使用,因为它只应用于类级别,而不是方法级别。您只能对IntroductionAdvisor
使用引入通知,它有以下方法:
public interface IntroductionAdvisor extends Advisor, IntroductionInfo {
ClassFilter getClassFilter();
void validateInterfaces() throws IllegalArgumentException;
}
public interface IntroductionInfo {
Class<?>[] getInterfaces();
}
没有MethodMatcher
,因此没有与引入通知相关的Pointcut
。只有类过滤是合乎逻辑的。
getInterfaces()
方法返回这个顾问引入的接口。
validateInterfaces()
方法用于内部查看引入的接口是否可以通过配置的IntroductionInterceptor
实现。
考虑Spring测试套件中的一个例子,假设我们想向一个或多个对象引入以下接口:
public interface Lockable {
void lock();
void unlock();
boolean locked();
}
这介绍了一个mixin。我们希望能够将被通知对象转换为Lockable
,不管它们的类型是什么,并调用lock和unlock方法。如果调用lock()
方法,我们希望所有setter方法都抛出LockedException
。因此,我们可以添加一个方面,它可以在对象没有任何知识的情况下使对象成为不可变的:这是AOP的一个很好的例子。
首先,我们需要一个IntroductionInterceptor
来完成繁重的工作。在本例中,我们扩展了org.springframework.aop.support.DelegatingIntroductionInterceptor
便利类。我们可以直接实现IntroductionInterceptor
,但是对于大多数情况,使用DelegatingIntroductionInterceptor
是最好的。
DelegatingIntroductionInterceptor
被设计为将引入接口的引入委托给引入接口的实际实现,从而隐藏了拦截的使用。可以使用构造函数参数将委托设置为任何对象。默认委托(使用无参数构造函数时)是this
。因此,在下一个例子中,委托是DelegatingIntroductionInterceptor
的LockMixin
子类。给定一个委托(默认情况下,它自己),DelegatingIntroductionInterceptor
实例会查找由委托实现的所有接口(除了IntroductionInterceptor
),并支持针对其中任何一个接口的引入。像LockMixin
这样的子类可以调用suppressInterface(Class intf)
方法来抑制不应该公开的接口。然而,无论一个IntroductionInterceptor准备支持多少个接口,IntroductionAdvisor
都会控制哪些接口实际上是公开的。引入的接口隐藏了目标对同一接口的任何实现。
因此,LockMixin
扩展了DelegatingIntroductionInterceptor
并实现了Lockable
本身。超类自动获取可引入的Lockable
,因此不需要指定它。我们可以通过这种方式引入任意数量的接口。
请注意locked
实例变量的使用。这有效地向目标对象中保存的状态添加了额外的状态。
下面的例子展示了LockMixin
类的示例:
public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {
private boolean locked;
public void lock() {
this.locked = true;
}
public void unlock() {
this.locked = false;
}
public boolean locked() {
return this.locked;
}
public Object invoke(MethodInvocation invocation) throws Throwable {
if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
throw new LockedException();
}
return super.invoke(invocation);
}
}
通常,您不需要重写invoke()
方法。DelegatingIntroductionInterceptor
实现(如果引入了这个方法,它就会调用这个delegate
方法,否则就会进入连接点)通常就足够了。在本例中,我们需要添加一个检查:如果处于锁定模式,就不能调用setter方法。
所需的引入只需要包含一个不同的LockMixin
实例并指定所引入的接口(在本例中,只需要Lockable
)。一个更复杂的例子可能会引用引入拦截器(它将被定义为原型)。在本例中,没有与LockMixin
相关的配置,因此我们使用new
来创建它。下面的例子展示了LockMixinAdvisor
类:
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
public LockMixinAdvisor() {
super(new LockMixin(), Lockable.class);
}
}
我们可以非常简单地应用这个顾问,因为它不需要配置。(然而,没有IntroductionAdvisor
就不可能使用IntroductionInterceptor
。)像通常的引入一样,顾问必须是每个实例的,因为它是有状态的。对于每个被建议的对象,我们需要LockMixinAdvisor
的不同实例,也就是LockMixin
。顾问包含被通知对象的部分状态。
我们可以通过使用Advised.addAdvisor()
方法或XML配置(推荐的方式)以编程方式应用这个顾问,就像其他任何顾问一样。下面讨论的所有代理创建选项,包括自动代理创建器,都能正确处理引入和有状态混合。
Spring中的Advisor API
在Spring中,Advisor是一个方面,它只包含一个与切入点表达式相关联的通知对象。
除了介绍的特殊情况,任何顾问都可以用于任何通知。org.springframework.aop.support.DefaultPointcutAdvisor
是最常用的顾问类。它可以与MethodInterceptor
、BeforeAdvice
或ThrowsAdvice
一起使用。
在同一个AOP代理中,可以混合使用Spring中的顾问和通知类型。例如,可以在一个代理配置中使用围绕通知、抛出通知和前置通知的拦截。Spring自动创建必要的拦截器链。
使用ProxyFactoryBean
创建AOP代理
如果您为您的业务对象使用Spring IoC容器(ApplicationContext
或BeanFactory
)(您应该这样做!),那么您需要使用Spring的AOP FactoryBean
实现之一。(请记住,工厂bean引入了一个间接层,让它创建不同类型的对象。)
Spring AOP支持也在幕后使用工厂bean。
在Spring中创建AOP代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean
。这样就可以完全控制切入点、应用的任何通知以及它们的顺序。但是,如果您不需要这样的控制,有一些更简单的选项是更好的。
基础知识
与其他Spring FactoryBean
实现一样,ProxyFactoryBean
引入了一个间接层。如果定义了一个名为foo
的ProxyFactoryBean
,引用foo
的对象不会看到ProxyFactoryBean
实例本身,而是一个由ProxyFactoryBean
中getObject()
方法的实现创建的对象。这个方法创建一个包装目标对象的AOP代理。
使用ProxyFactoryBean
或另一个IoC感知类来创建AOP代理的最重要好处之一是,通知和切入点也可以由IoC管理。这是一个强大的特性,支持某些用其他AOP框架很难实现的方法。例如,通知本身可以引用应用程序对象(除了目标之外,目标在任何AOP框架中都应该可用),从而受益于依赖项注入提供的所有可插拔性。
JavaBean属性
与Spring提供的大多数FactoryBean
实现一样,ProxyFactoryBean
类本身就是一个JavaBean。它的属性用于:
- 指定要代理的目标。
- 指定是否使用CGLIB(稍后描述并参见基于JDK和CGLIB的代理)。
一些关键属性继承自org.springframework.aop.framework.ProxyConfig
(Spring中所有AOP代理工厂的超类)。这些关键属性包括以下内容:
proxyTargetClass
:如果目标类要被代理,而不是目标类的接口,则为真。如果此属性值设置为true
,则创建CGLIB代理(但也请参阅基于JDK和CGLIB的代理)。optimize
:控制是否将主动优化应用于通过CGLIB创建的代理。除非完全理解相关的AOP代理如何处理优化,否则不应该轻率地使用这种设置。这目前仅用于CGLIB代理。它对JDK动态代理没有影响。frozen
:冻结代理配置后,不允许对该配置进行修改。当您不希望调用者能够在创建代理之后(通过Advised
的接口)操作代理时,这对于进行轻微的优化是很有用的。该属性的默认值为false
,因此允许进行更改(例如添加额外的通知)。exposeProxy
:确定当前代理是否应该在ThreadLocal
中公开,以便目标可以访问它。如果目标需要获取代理,并且exposeProxy
属性被设置为true
,那么目标可以使用AopContext.currentProxy()
方法。
ProxyFactoryBean
特有的其他属性包括:
proxyInterfaces
:String
接口名称的数组。如果没有提供,则使用目标类的CGLIB代理(但也请参阅基于JDK和CGLIB的代理)。interceptorNames
:一个由Advisor
、拦截器或其他要应用的通知名称组成的String
数组。顺序很重要,先进先服务。也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。
这些名称是当前工厂中的bean名称,包括来自祖先工厂的bean名称。这里不能提到bean引用,因为这样做会导致ProxyFactoryBean
忽略通知的单例设置。
可以在拦截器名称后面加上星号(*
)。这样做会导致所有顾问bean的应用程序名称都以要应用的星号之前的部分开头。您可以在使用“全局”顾问中找到使用该特性的示例。singleton
:不管getObject()
方法被调用多少次,工厂是否应该返回单个对象。有几个FactoryBean
实现提供了这样的方法。缺省值为true
。如果你想使用有状态的通知——例如,对于有状态的混合程序——使用原型通知和一个单例值false
。
基于JDK和cglib的代理
本节是关于ProxyFactoryBean
如何选择为特定的目标对象(将要被代理)创建基于jdk的代理或基于cglib的代理的决定性文档。
ProxyFactoryBean
在创建基于JDK或cglib的代理方面的行为在Spring的1.2.x版和2.0版之间发生了变化。在自动检测接口方面,ProxyFactoryBean
现在表现出与TransactionProxyFactoryBean
类的接口类似的语义。
如果要被代理的目标对象的类(以下简称为目标类)没有实现任何接口,那么将创建一个基于cglib的代理。这是最简单的场景,因为JDK代理是基于接口的,没有接口意味着JDK代理甚至是不可能的。您可以插入目标bean并通过设置interceptorNames
属性指定拦截器列表。注意,即使ProxyFactoryBean
的proxyTargetClass
属性被设置为false
,也会创建基于cglib的代理。(这样做毫无意义,最好从bean定义中删除,因为这样做往好了说是多余的,往坏了说是令人困惑的。)
如果目标类实现了一个(或多个)接口,则创建的代理类型取决于ProxyFactoryBean
的配置。
如果ProxyFactoryBean
的proxyTargetClass
属性被设置为true
,那么将创建一个基于cglib的代理。这是有道理的,也符合最少意外的原则。即使ProxyFactoryBean
的proxyInterfaces
属性被设置为一个或多个完全限定的接口名,proxyTargetClass
属性被设置为true
也会导致基于cglib的代理生效。
如果ProxyFactoryBean
的proxyInterfaces
属性被设置为一个或多个完全限定的接口名,那么将创建基于jdk的代理。所创建的代理实现了在proxyInterfaces
属性中指定的所有接口。如果目标类实现的接口比在proxyInterfaces
属性中指定的接口多得多,这当然很好,但返回的代理没有实现这些额外的接口。
如果没有设置ProxyFactoryBean
的proxyInterfaces
属性,但是目标类实现了一个(或多个)接口,那么ProxyFactoryBean
会自动检测目标类确实实现了至少一个接口的事实,并创建基于jdk的代理。实际被代理的接口是目标类实现的所有接口。实际上,这与向proxyInterfaces
属性提供目标类实现的每个接口的列表相同。然而,它的工作量明显较少,也不太容易出现印刷错误。
代理接口
考虑一个运行中的ProxyFactoryBean
的简单示例。这个例子包括:
- 被代理的目标bean。这是示例中的personTarget bean定义。
- 一个顾问和一个拦截器用来提供建议。
- 一个指定目标对象(personTarget bean)、代理接口和应用通知的AOP代理bean定义。
下面的清单显示了这个例子:
<bean id="personTarget" class="com.mycompany.PersonImpl">
<property name="name" value="Tony"/>
<property name="age" value="51"/>
</bean>
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>
<bean id="person"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<property name="target" ref="personTarget"/>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
请注意,interceptorNames
属性接受一个String
列表,该列表包含当前工厂中的拦截器或顾问的bean名称。您可以使用顾问、拦截器、前置、后置和抛出通知对象。顾问的顺序很重要。
您可能想知道为什么列表中不包含bean引用。这样做的原因是,如果
ProxyFactoryBean
的singleton属性设置为false
,那么它必须能够返回独立的代理实例。如果任何一个advisor本身就是一个原型,那么就需要返回一个独立的实例,因此必须能够从工厂获得原型的实例。只有引用是不够的。
前面显示的person
bean定义可以代替Person
实现,如下所示:
Person person = (Person) factory.getBean("person");
同一个IoC上下文中的其他bean可以表示对它的强类型依赖关系,就像普通Java对象一样。下面的例子展示了如何做到这一点:
<bean id="personUser" class="com.mycompany.PersonUser">
<property name="person"><ref bean="person"/></property>
</bean>
本例中的PersonUser
类公开了一个Person
类型的属性。就它而言,可以透明地使用AOP代理来代替“真实的”人员实现。然而,它的类将是一个动态代理类。可以将其转换为Advised
的接口(稍后讨论)。
您可以使用匿名内部bean来隐藏目标和代理之间的区别。只有ProxyFactoryBean
定义不同。包含该通知只是为了完整性。下面的示例展示了如何使用匿名内部bean:
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<!-- Use inner bean, not local reference to target -->
<property name="target">
<bean class="com.mycompany.PersonImpl">
<property name="name" value="Tony"/>
<property name="age" value="51"/>
</bean>
</property>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
使用匿名内部bean的优点是只有一个Person
类型的对象。如果我们想防止应用程序上下文的用户获得对未通知对象的引用,或者需要避免Spring IoC自动组装的任何歧义,这是很有用的。还有一个可以论证的优点是,ProxyFactoryBean
定义是自包含的。然而,有时候能够从工厂获得未通知的目标实际上可能是一种优势(例如,在某些测试场景中)。
代理类
如果您需要代理一个类,而不是一个或多个接口,该怎么办?
假设在我们前面的示例中,没有Person
接口。我们需要通知一个名为Person
的类,它没有实现任何业务接口。在这种情况下,您可以将Spring配置为使用CGLIB代理,而不是动态代理。为此,将前面显示的ProxyFactoryBean
上的proxyTargetClass
属性设置为true。虽然最好是根据接口而不是类进行编程,但在使用遗留代码时,通知没有实现接口的类的能力可能很有用。(一般来说,Spring不是规定性的。虽然它使应用良好的实践变得容易,但它避免了强制使用特定的方法。)
如果你愿意,你可以在任何情况下强制使用CGLIB,即使你有接口。
CGLIB代理是通过在运行时生成目标类的子类来工作的。Spring将这个生成的子类配置为将方法调用委托给原始目标。子类用于实现装饰器模式,并织入通知。
CGLIB代理通常对用户是透明的。然而,还有一些问题需要考虑:
- 不能通知
Final
方法,因为它们不能被重写。 - 没有必要将CGLIB添加到类路径中。从Spring 3.2开始,CGLIB被重新打包并包含在Spring -core JAR中。换句话说,基于cglib的AOP“开箱即用”工作,就像JDK动态代理一样。
CGLIB代理和动态代理之间的性能差别很小。在这种情况下,性能不应该是决定性的考虑因素。
使用“全局”顾问
通过在拦截器名称上附加星号,所有具有与星号之前的部分匹配的bean名称的顾问都将添加到顾问链中。如果您需要添加一组标准的“全局”顾问,那么这就会派上用场了。下面的示例定义了两个全局顾问:
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="service"/>
<property name="interceptorNames">
<list>
<value>global*</value>
</list>
</property>
</bean>
<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>
简洁的代理的定义
特别是在定义事务代理时,您可能最终会得到许多类似的代理定义。使用父bean和子bean定义以及内部bean定义可以产生更清晰和更简洁的代理定义。
首先,我们为代理创建一个父、模板、bean定义,如下所示:
<bean id="txProxyTemplate" abstract="true"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
它从来没有实例化过自己,所以它实际上可能是不完整的。然后,需要创建的每个代理都是子bean定义,它将代理的目标包装为内部bean定义,因为无论如何目标都不会单独使用。下面的示例显示了这样的子bean:
<bean id="myService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MyServiceImpl">
</bean>
</property>
</bean>
可以覆盖父模板中的属性。在下面的示例中,我们重写事务传播设置:
<bean id="mySpecialService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MySpecialServiceImpl">
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="store*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
可以覆盖父模板中的属性。在下面的示例中,我们重写事务传播设置:
<bean id="mySpecialService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MySpecialServiceImpl">
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="store*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
请注意,在父bean示例中,我们通过将abstract
属性设置为true
(如前所述)显式地将父bean定义标记为抽象,这样它实际上可能永远不会被实例化。默认情况下,应用程序上下文(但不是简单的bean工厂)预实例化所有的单例。因此,重要的是(至少对于单例bean),如果您有一个(父)bean定义,您打算仅作为模板使用,并且该定义指定了一个类,您必须确保将abstract
属性设置为true
。否则,应用程序上下文实际上会尝试预实例化它。
使用ProxyFactory
以编程方式创建AOP代理
使用Spring以编程方式创建AOP代理很容易。这使您可以不依赖于Spring IoC而使用Spring AOP。
由目标对象实现的接口将被自动代理。下面的清单显示了使用一个拦截器和一个顾问为目标对象创建代理的过程:
ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();
第一步是构造一个类型为org.springframework.aop.framework.ProxyFactory
的对象。您可以使用一个目标对象来创建它,如前面的示例所示,也可以在另一个构造函数中指定要代理的接口。
您可以添加通知(将拦截器作为一种专门的通知)、顾问,或者两者都添加,并在ProxyFactory
的生命周期中操纵它们。如果您添加了一个IntroductionInterceptionAroundAdvisor
,您可以使代理实现额外的接口。
ProxyFactory
上还有一些方便的方法(继承自AdvisedSupport
),允许您添加其他通知类型,如前置和抛出通知。AdvisedSupport
是ProxyFactory
和ProxyFactoryBean
的超类。
将AOP代理创建与IoC框架集成在一起是大多数应用程序中的最佳实践。我们建议使用AOP从Java代码中外部化配置,这在一般情况下是应该的。
操作被通知对象
无论如何创建AOP代理,您都可以通过使用org.springframework.aop.framework.Advised
接口来操作它们。任何AOP代理都可以转换到这个接口,不管它实现了哪些其他接口。该接口包括以下方法:
Advisor[] getAdvisors();
void addAdvice(Advice advice) throws AopConfigException;
void addAdvice(int pos, Advice advice) throws AopConfigException;
void addAdvisor(Advisor advisor) throws AopConfigException;
void addAdvisor(int pos, Advisor advisor) throws AopConfigException;
int indexOf(Advisor advisor);
boolean removeAdvisor(Advisor advisor) throws AopConfigException;
void removeAdvisor(int index) throws AopConfigException;
boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;
boolean isFrozen();
getAdvisors()
方法为每个已添加到工厂的顾问、拦截器或其他通知类型返回一个Advisor
。如果添加了Advisor
,则该索引处返回的Advisor
就是添加的对象。如果添加了拦截器或其他通知类型,Spring将其包装在一个带有总是返回true
的切入点的顾问中。因此,如果你添加了一个MethodInterceptor
,这个索引返回的顾问是一个DefaultPointcutAdvisor
,它返回你的MethodInterceptor
和一个匹配所有类和方法的切入点。
默认情况下,即使创建了代理,也可以添加或删除顾问或拦截器。唯一的限制是不可能添加或删除引入顾问,因为工厂中的现有代理不显示接口更改。(你可以向工厂申请新的代理来避免这个问题。)
下面的例子展示了将一个AOP代理转换为Advised
的接口,并检查和操作它的通知:
Advised advised = (Advised) myObject;
Advisor[] advisors = advised.getAdvisors();
int oldAdvisorCount = advisors.length;
System.out.println(oldAdvisorCount + " advisors");
// Add an advice like an interceptor without a pointcut
// Will match all proxied methods
// Can use for interceptors, before, after returning or throws advice
advised.addAdvice(new DebugInterceptor());
// Add selective advice using a pointcut
advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));
assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);
在生产环境中修改业务对象上的通知是否明智(没有双关语的意思)值得怀疑,尽管毫无疑问存在合法的使用案例。然而,它在开发中非常有用(例如,在测试中)。我们有时发现,能够以拦截器或其他通知的形式添加测试代码,进入我们想要测试的方法调用中,是非常有用的。(例如,通知可以进入为该方法创建的事务中,在将事务标记为回滚之前,可以运行SQL检查数据库是否被正确更新。)
根据创建代理的方式,通常可以设置frozen
标志。在这种情况下,Advised
的isFrozen()
方法返回true
,任何通过添加或删除修改通知的尝试都会导致AopConfigException
异常。在某些情况下,能够冻结被建议对象的状态是很有用的(例如,防止调用代码删除安全拦截器)。
使用“自动代理”工具
到目前为止,我们已经考虑了通过使用ProxyFactoryBean
或类似的工厂bean来显式地创建AOP代理。
Spring还允许我们使用“自动代理”bean定义,它可以自动代理选定的bean定义。这是建立在Spring的“bean post processor”基础设施上的,它允许在容器装载时修改任何bean定义。
在这个模型中,您在XML bean定义文件中设置一些特殊的bean定义,以配置自动代理基础设施。这允许您声明符合自动代理条件的目标。您不需要使用ProxyFactoryBean
。
有两种方法可以做到这一点:
- 通过使用引用当前上下文中特定bean的自动代理创建器。
- 自动代理创建的一个特殊情况值得单独考虑:由源级元数据属性驱动的自动代理创建。
Auto-proxy Bean定义
本节介绍由org.springframework.aop.framework.autoproxy
包提供的自动代理创建器
BeanNameAutoProxyCreator
BeanNameAutoProxyCreator
类是一个BeanPostProcessor
,它自动为具有匹配文字值或通配符的名称的bean创建AOP代理。下面的示例展示了如何创建一个BeanNameAutoProxyCreator
bean:
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames" value="jdk*,onlyJdk"/>
<property name="interceptorNames">
<list>
<value>myInterceptor</value>
</list>
</property>
</bean>
与ProxyFactoryBean
一样,有一个interceptorNames
属性,而不是一个拦截器列表,以允许原型顾问的正确行为。命名为“拦截器”的可以是顾问或任何通知类型。
与一般的自动代理一样,使用BeanNameAutoProxyCreator
的主要目的是将相同的配置一致地应用到多个对象,同时使用最小的配置量。对于将声明性事务应用到多个对象,它是一个流行的选择。
名称匹配的Bean定义(如前面示例中的jdkMyBean
和onlyJdk
)是带有目标类的普通旧Bean定义。AOP代理是由BeanNameAutoProxyCreator
自动创建的。同样的建议也适用于所有匹配的bean。请注意,如果使用了顾问(而不是前面示例中的拦截器),那么切入点可能会以不同的方式应用于不同的bean。
DefaultAdvisorAutoProxyCreator
一个更通用和非常强大的自动代理创建者是DefaultAdvisorAutoProxyCreator
。这将自动在当前上下文中应用合格的顾问,而不需要在自动代理顾问的bean定义中包含特定的bean名称。它提供了与BeanNameAutoProxyCreator
相同的优点,即一致配置和避免重复。
使用这种机制包括:
- 指定
DefaultAdvisorAutoProxyCreator
bean定义。 - 在相同或相关的上下文中指定任意数量的顾问。请注意,这些必须是顾问,而不是拦截器或其他建议。这是必要的,因为必须有一个要评估的切入点来检查每个通知对候选bean定义的资格。
DefaultAdvisorAutoProxyCreator
自动评估每个顾问中包含的切入点,以查看它应该应用于每个业务对象的(如示例中的businessObject1
和businessObject2
)建议。
这意味着可以将任意数量的顾问程序自动应用到每个业务对象。如果任何顾问中的切入点都不匹配业务对象中的任何方法,则该对象不被代理。当为新的业务对象添加bean定义时,如果有必要,将自动对它们进行代理。
通常,自动代理的优点是使调用者或依赖项不可能获得未被通知的对象。在这个ApplicationContext
上调用getBean(“businessObject1”)
将返回一个AOP代理,而不是目标业务对象。(前面展示的“inner bean”习语也提供了这一好处。)
下面的示例创建了DefaultAdvisorAutoProxyCreator
bean和本节中讨论的其他元素:
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
<property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>
<bean id="customAdvisor" class="com.mycompany.MyAdvisor"/>
<bean id="businessObject1" class="com.mycompany.BusinessObject1">
<!-- Properties omitted -->
</bean>
<bean id="businessObject2" class="com.mycompany.BusinessObject2"/>
如果您希望将相同的通知一致地应用到许多业务对象,DefaultAdvisorAutoProxyCreator
非常有用。一旦基础设施定义就绪,您就可以添加新的业务对象,而不需要包含特定的代理配置。您还可以轻松地添加其他方面(例如,跟踪或性能监视方面),只需对配置进行最小的更改。
DefaultAdvisorAutoProxyCreator
提供了对过滤和排序的支持(通过使用命名约定,以便只评估某些顾问,这允许在同一工厂中使用多个不同配置的AdvisorAutoProxyCreators
)。顾问可以实现org.springframework.core.Ordered
的接口,以确保正确的排序,如果这是一个问题。前面示例中使用的TransactionAttributeSourceAdvisor
具有可配置的order值。默认设置是无序的。
使用TargetSource
实现
Spring提供了TargetSource
的概念,用org.springframework.aop.TargetSource
接口表示。该接口负责返回实现连接点的目标对象。每当AOP代理处理方法调用时,都会向TargetSource
实现请求目标实例。
使用Spring AOP的开发人员通常不需要直接使用TargetSource
实现,但这提供了支持池、热切换和其他复杂目标的强大手段。例如,通过使用池来管理实例,池TargetSource
可以为每次调用返回不同的目标实例。
如果未指定TargetSource
,则使用默认实现来包装本地对象。每次调用都会返回相同的目标(如您所料)。
本节的其余部分将描述Spring提供的标准目标源以及如何使用它们。
当使用自定义目标源时,您的目标通常需要是原型,而不是单例bean定义。这允许Spring在需要时创建一个新的目标实例。
支热插拔目标源
org.springframework.aop.targe.HotSwappableTargetSource
的存在是为了让AOP代理的目标被切换,同时让调用者保留对它的引用。
改变目标源的目标立即生效。HotSwappableTargetSource
是线程安全的。
您可以通过使用HotSwappableTargetSource上的swap()
方法来更改目标,如下面的示例所示:
HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);
下面的示例显示了所需的XML定义:
<bean id="initialTarget" class="mycompany.OldTarget"/>
<bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource">
<constructor-arg ref="initialTarget"/>
</bean>
<bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource" ref="swapper"/>
</bean>
前面的swap()
调用更改可交换bean的目标。持有该bean引用的客户机不知道更改,但会立即开始击中新的目标。
尽管这个示例没有添加任何通知(使用TargetSource
并不需要添加通知),但任何TargetSource都可以与任意通知一起使用。
池目标来源
使用池目标源提供了与无状态会话ejb类似的编程模型,在该模型中,维护一个由相同实例组成的池,并将方法调用转移到池中的空闲对象。
Spring池和SLSB池的一个关键区别是,Spring池可以应用于任何POJO。一般来说,与Spring一样,可以以一种非侵入性的方式应用该服务。
Spring提供了对Commons Pool 2.2的支持,后者提供了一个相当高效的池实现。您需要应用程序的类路径上的common-pool
Jar来使用这个特性。你也可以继承org.springframework.aop.target.AbstractPoolingTargetSource
来支持任何其他池化API。
Commons Pool 1.5+也受支持,但在Spring Framework 4.2时已弃用。
下面的清单显示了一个配置示例:
<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject"
scope="prototype">
... properties omitted
</bean>
<bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPool2TargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
<property name="maxSize" value="25"/>
</bean>
<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource" ref="poolTargetSource"/>
<property name="interceptorNames" value="myInterceptor"/>
</bean>
注意,目标对象(上例中的businessobjectarget
)必须是一个原型。这允许PoolingTargetSource
实现创建目标的新实例,以便在必要时扩展池。请参阅AbstractPoolingTargetSource的javadoc和您希望用于获取有关其属性信息的具体子类。maxSize
是最基本的,并且总是保证存在。
在这种情况下,myInterceptor
是需要在同一个IoC上下文中定义的拦截器的名称。但是,您不需要指定拦截器来使用池。如果你只想要池而不想要其他的通知,那么根本就不要设置interceptorNames
属性。
您可以配置Spring,使其能够将任何池中的对象转换到org.springframework.aop.target.PoolingConfig
接口,它通过一个介绍公开关于配置和池的当前大小的信息。您需要像下面这样定义一个顾问:
<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="poolTargetSource"/>
<property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>
该顾问是通过调用AbstractPoolingTargetSource
类上的便利方法获得的,因此使用MethodInvokingFactoryBean
。这个顾问的名称(这里是poolConfigAdvisor
)必须位于公开池对象的ProxyFactoryBean
中的拦截器名称列表中。
cast的定义如下:
PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());
池化无状态服务对象通常不是必需的。我们不认为这应该是默认选择,因为大多数无状态对象自然是线程安全的,而且如果资源被缓存,实例池是有问题的。
通过使用自动代理可以使用更简单的池。您可以设置任何自动代理创建器使用的TargetSource
实现。
原型目标的来源
设置“原型”目标源类似于设置池TargetSource
。在这种情况下,每次方法调用都会创建目标的新实例。虽然在现代JVM中创建新对象的成本并不高,但连接新对象(满足其IoC依赖项)的成本可能更高。因此,如果没有很好的理由,您不应该使用这种方法。
为此,您可以修改前面所示的poolTargetSource
定义,如下所示(为了清晰起见,我们还更改了名称):
<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
<property name="targetBeanName" ref="businessObjectTarget"/>
</bean>
唯一的属性是目标bean的名称。在TargetSource
实现中使用继承来确保一致的命名。与池目标源一样,目标bean必须是原型bean定义。
ThreadLocal目标来源
如果需要为每个传入请求(即每个线程)创建对象,ThreadLocal
目标源非常有用。ThreadLocal
的概念提供了一个jdk范围的工具,可以透明地在线程中存储资源。设置ThreadLocalTargetSource
与其他类型的目标源基本相同,如下面的示例所示:
<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
</bean>
在多线程和多类加载器环境中不正确地使用
ThreadLocal
实例会带来严重的问题(可能导致内存泄漏)。您应该始终考虑将threadlocal包装在其他类中,并且永远不要直接使用ThreadLocal
本身(包装类除外)。另外,您应该始终记住正确地设置和取消设置线程的本地资源(后者只涉及对ThreadLocal.set(null)
的调用)。在任何情况下都应该取消设置,因为不取消它可能会导致有问题的行为。Spring的ThreadLocal
支持为您做到了这一点,并且应该始终考虑使用ThreadLocal
实例而不使用其他适当的处理代码。
定义新的通知类型
Spring AOP被设计成可扩展的。虽然目前在内部使用拦截实现策略,但除了环绕通知、前置通知、抛出通知、后置通知的拦截之外,还可以支持任意类型的通知。
org.springframework.aop.framework.adapter
包是一个SPI包,它允许在不改变核心框架的情况下添加对新的自定义通知类型的支持。对自定义通知类型的唯一约束是它必须实现org.aopalliance.aop.Advice
标记接口。