6.Spring AOP APIS
上一章介绍了Spring 通过@AspectJ和基于模式的切面定义对AOP的支持。在本章中,我们将讨论Spring AOP的底层API。对于普通应用,我们推荐使用前一章中介绍的Spring AOP与AspectJ电切分。
6.1 Pointcut API inSpring
本节介绍spring如何处理点切概念。
6.1.1 概念
Spring的点切方式模型实现与建议类型无关的点切重用。你可以使用相同的点切针对不同 的建议。 org.springframework.aop.Pointcut接口是中心接口,用于向特定类和方法提供建议。完整的接口如下:
将Pointcut接口拆分为两部分,可以重复实用类和方法匹配部分一级细粒度的组合操作(例如与另一个方法匹配器执行“联合”)。 ClassFilter接口用于将切点限制在一组给定的目标雷伤。如果matches()方法总是返回true,则所有目标类都会被匹配。下面的列表显示了ClassFilter接口定义:
MethodMatcher界面通常更为重要。完整的接口如下:
matchers(Method,Class) 方法用于测试改切点方式是否与目标类上的给定方法相匹配。可以在创建Aop代理时执行此评估,以避免在每次调用方法时都进行测试。如果给定方法的双参数mathces方法返回true,而MethodMatcher的isRuntime() 方法返回true,则每次方法调用时都会调用三参数匹配方法。这样,pointcut就可以在目标建议开始只谬立即查看传递给方法调用的参数。 大多数MethodMatcher实现是静态的,这意味着它们的isRuntime()方法返回false。在这种情况下,三个参数的matches方法永远不会被调用。
6.1.2 Operations on Pointcuts
Spring 支持对电切分进行操作(尤其是联合和相交)。 联合指任意pointcut方式匹配的方法。交集指两个pointcut方式都匹配的方法。联合通常更有用。你可以通过使用org.springframework.aop.support.Pointcuts类中的静态方法或使用同以软件包中的ComposavlePointcut类来组成pointcut方式。不过,使用AspectJ的pointcut方式表达式通常是一种更简单的方法。
6.1.3 AspectJ Expression Pointcuts
自2.0起,Spring使用的最重要的点切类型是org.springframework.aop.aspectj.AspectJExpressionPointcut.这是一种使用AspectJ提供的库来解析AspectJ电切分表达式字符串的电切分。 有关AspectJ支持的点切基元的讨论,请参见前一章。
6.1.4Convenience Pointcut Implementations
Spring提供了几种方便的点切实现。你可以直接使用其中的一些;其他的则是为了在特定于应用程序的电切分中进行子类化。
Static Pointcut
静态pointcut方式基于方法和目标类,不能考虑方法的参数。静态pointcut方式足以满足大多数使用需要求,而且是最佳选择。Spring只能在首次调用方法时评估一次静态pointcut方式。伺候,每次调用方法时都无需再次评估该pointcut方式。 本节其余部分将介绍Spring附带的一些静态切点实现。
正则表达式pointcut方式
正则表达式是指定静态插入点的一些显而易见的方法。出了Spring之外,还有多个AOP框架可以做到这一点。org.springframework.aop.support.JdkRegexpMethodPointcut是一种通用的正则表达式pointcut方式,它使用JDK中的正则表达式支持。 通过JdkRegexpMethodPointcut类,你可以提供一系列模式字符串。如果其中任何一个是匹配字符串,则点切线的评估结果为true(因此,产生的点切线实际上是指定模式的联合)。 下面的示例展示了如何使用JdkRegexpMethodPointcut:
Spring提供了一个名为RegexpMethodPointcutAdvisor的pointcut类,它允许我们引用Advice(请记住,Advice可以是拦截器、before advice、throws advice等)。在幕后,Spring使用JdkRegexpMethodPointcut。使用RegexpMethodPointcutAdvisor可以简化布线,因为一个Bean即封装了点切有封装了建议,如下例所示:
你可以将RegexpMethodPointcutAdvisor与任何Advice类型一起使用。
Attribute-driven Pointcuts
静态pointcut方式的一个重要类型是元数据驱动Pointcuts方式。它使用元数据数星星(通常是源代码级元数据)的值。
Dynamic pointcuts 动态切点
与静态pointcut方式相比,动态pointcut方式的评估成本更高。他们会考虑方法参数和静态信息。这意味着每次调用方法时都必须对其进行评估,而且由于参数会发生变化,因此无法缓存评估结果。 主要的例子就是control flow pointcut
control Flow Pointcut
Spring 控制流pointcut在概念上类似于AspectJ flow pointcut,但功能没那么强大。(目前还无法指定一个pointcut在另一个pointcut匹配的连接点下方运行)。控制流pointcut与当前调用堆栈相匹配。例如,如果连接点被com.mycompany.web保重的方法或SomeCaller类调用,它就会触发。使用org.springframework.aop.support.ControlFlowPointcut类可指定控制流切点。
6.1.5 pointcut 超类
Spring提供了有用的pointcut超类,帮助你实现自己的pointcut。 由于静态快捷方式最为有用,你可能应该子类化StaticMethodMatchPointcut。这只需要实现一个抽象方法(尽管你可以覆盖其他车方法来定制行为)。下面的示例展示了如何子类化StaticMethodMatcherPoincut:
此外,还有用于动态pointcut的超类。你可以在任何建议类型中使用自定义pointcut。
6.1.6 自定义pointcut
由于SpringAop中的切点是java类而不是语言特性(如AspectJ),因此你可以声明自定义切点,无论是静态的还是动态切点。Spring中的自定义指向可以任意复杂。不过,如果可以的话,我们建议使用AspectJ的点切分表达式语言。
6.2 Advice API in Spring
现在我们来看看Spring Aop如何处理建议。
6.2.1 Advice Lifecycles
每个建议都是一个Spring Bean。建议实例可以在所有建议对象中共享,叶可以对每个建议对象唯一。这相当于按类或按实例建议。 最常用的是按类咨询。它适用于通用建议,如事务建议。这些建议不依赖于代理对象的状态,也不添加新的状态。它们只是作用于方法和参数。 每实例建议适用于引入,以支持混合体。在这种情况下,建议会为代理对象添加状态。 你可以在同一个AOP代理中混合使用共享建议和按实例建议。
6.2.2 Spring中的建议类型
Spring提供多种建议类型,并可扩展以支持任何建议类型。本节将介绍基本概念和标准建议类型。
Interception Around
Spring中最基本的建议类型是环绕建议的链接。 Spring符合使用方法链接的AOP Alliance接口的环绕建议。实现MethodInterceptor并实现环绕建议的类还应该实现一下接口:
invoke()方法的MethodInvocation参数会显示调用的方法、目标连接点、AOP代理和方法的参数。invoke()方法应返回调用的结果:连接点的返回值。 下面的实例展示了一个简单的MethodInterceptor实现:
请注意对MethodInvocation的Proceed() 方法的调用。这将沿用拦截器链向连接点移动。大多数拦截器都会调用该方法并返回其返回值。但是,MethodInterceptor和其他建议一样,可以返回不同的值或抛出异常,而不是调用proceed方法。但是,如果没有充分的理由,你不希望这样做。
MethodInterceptor实现提供了与其他符合AOP联盟的AOP实现的互操作性。本节其余部分讨论的其他建议类型以Spring特有的方式实现了常见的AOP概念。虽然使用最具体的建议类型具有优势,但如果你可能希望在其他AOP框架中运行该方面,请坚持使用MethodInterceptor建议。请注意,目前各框架之间还不能互操作,而且AOP联盟目前也没有定义点切接口。
Before Advice
更简单的建议类型是before建议,它不需要MethodInterceptor对象,因为它只在进入方法之前被调用。 before建议的主要有点是不需要调用proceed()方法,因此也就不会因疏忽而无法继续拦截器链。 下面的列表显示了MethodBeforeAdvice界面:
Spring 的API设计允许先字段后建议,尽管字段拦截适用于通常的对象,但Spring不大可能实现它。 请注意,返回类型是void。Before advice可以在连接点的运行前插入自定义行为,但不能更改返回值。如果before建议抛出异常,它会停止拦截器链的进一步执行。异常会沿着拦截器链向上传播。如果它未被选中或在被调用方法的签名上,则会直接传递给客户端。否则,AOP代理会将其封装在未选中的异常中。 下面的实力展示了Spring中的before advice,它会统计所有的方法的调用次数。
Throws Advice
如果连接点抛出异常,则在连接点返回后调用Throws建议。Spring提供类型化的抛出建议。请注意,这意味着org.springframework.aop.ThrowsAdvice接口不包含任何方法。它是一个标签接口,用于标识给定对象实现一个或多个类型化抛出提示方法。这些方法应采用以下形式:
只有最后一个参数是必需的。方法签名可能有一个或四个参数,这取决于advice方法是否对方法和参数感兴趣。下面列出的两个类是抛出建议的示例。 如果抛出RemoteException(包括来自子类的RemoteException),将调用以下建议:
与前面的建议不同,下一个实例声明了四个参数,因此可以访问调用的方法、方法参数和目标对象。如果抛出ServletException,将调用一下建议:
最后一个示例说明了如何在一个同事处理RemoteException和ServletException的类中使用这两种方法。任何数量的抛出建议方法都可以在一个类中组合使用。下面的列表显示了最终实例:
如何throws-advice方法自己抛出异常,则会覆盖原始异常(即更改抛给用户的异常)。覆盖的异常通常是RuntimeException,它与任何方法签名都兼容。但是,如果抛出提示方法抛出的已检查异常,则必须与目标的已声明异常想匹配,因此在某种程度上与特定的目标方法签名想关联。请勿抛出和目标方法签名不兼容的未声明检查异常。
After Returning Advice
Spring 中的后返回值建议必须实现org.springframework.aop.AfterReturningAdvice接口,如下所示:
返回后建议可以访问返回值(不能修改)、调用的方法、方法的参数和目标。 下面的返回建议会统计所有未抛出异常的成功方法调用次数:
该建议不会改变执行路径。如果出现异常,则会在拦截器链上抛出,而不是返回值。
Introduction Advice
Spring将引入建议视为一种特殊的拦截建议。 导言要求IntroductionAdvisor和IntroductionInterceptor实现一下接口:
从AOP联盟MethodInterceptor接口集成invoke() 方法必须实现引入。也就是说,如果调用的方法位于引入的接口上,则引入拦截器负责处理方法调用--它不能调用proceed()。 引入建议不能与任何点切分一起使用,因为它只适用于类而不是方法级别。你只能在具有一下方法的IntroductionAdvisor中使用引入建议:
没有MethodMatcher,因此也就没有介绍建议相关的Pointcut。只有类筛选才符合逻辑。 getInterfaces()方法返回该顾问引入的接口。 内部使用查立达特Interfaces()方法来查看所引入的接口是否可由配置的IntroductionInterceptor实现。 请看Spring测试套件中的一个示例,假设我们要为一个或多个对象引入一下接口:
这说明了一个mixin。我们希望能够将建议对象转换为Lockable,无论其类型如何,并调用锁定和解锁方法。如果我们调用lock() 方法,我们希望所有设置器方法都抛出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类的示例:
通常情况下,你无需重载invoke() 方法。DelegatingIntroductionInterceptor方法的视线(如果引入了delegate方法,则调用方法,否则调用连接点)通常就足够了。在本例中,我们需要添加一项检查:如果处于锁定模式,则不能调用setter方法。 所需的引入只需持有一个不同的LockMixin实例,并指定引入的接口(在本例中,只需Lockable)。更复杂的实例可能需要引入拦截器(被定义为原型)。在这种情况下,没有与LockMixin相关的配置,因此我们使用new来创建它。下面的示例展示了我们的LockMixinAdvisor类:
我们可以非常简单地应用这个顾问,因为它不需要任何配置。(不过,在没有IntroductionAdvisor的情况下,我们不能够使用IntroductionInterceptor。)与通常的介绍一样,顾问必须是按实例的,因为它是有状态的。我们需要为每个建议对象提供LockMixinAdvisor的不同实例,因此也需要LockMixin的不同实例。顾问包含被建议对象的部分状态。 我们可以通过使用Advised.addAdvisor()方法或(推荐方法)XML配置以编程方式应用该顾问,就像应用其他顾问一样。下面讨论的所有代理创建选择,包括“自动代理创建器”,都能正确处理引入和有状态的混合体。
6.3 The Advisor API in Spring
在Spring中,顾问是一个切面,它包含了一个与指向表达式相关联的单个建议对象。 出了介绍这种特殊情况外,任何顾问都可以与任何建议一起使用。org.springframework.aop.support.DefaultPointcutAdvisor是最常用的顾问类。它可以与MethodInterceptor、BeforeAdvice或ThrowsAdvice一起使用。 在同一个AOP代理中,可以混合使用Spring中的顾问和建议类型。例如,你可以在一个代理配置中使用围绕建议、抛出建议和在建议之前的拦截。Spring会自动创建必要的拦截器链。
6.4 Using the ProxyFactoryBean to Create AOP Proxies
如果你为业务对象使用Spring IoC容器(APplicationContext或BeanFactory)(你应该这样做!),你需要使用Spring的AOP FactoryBean实现之一。(请记住,工厂Bean引入了一个间接层,允许它创建不同类型的对象)。
在Spring中创建AOP代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean。这样就可以完全控制指向、任何使用的建议及其排序。不过,如果你不需要这样的控制,还有更简单的选择。
6.4.1 基础知识
与其他Spring FactoryBean实现一样,ProxyFactoryBean引入了一个间接层次。如果你定义了名为foo的ProxyFactoryBean,那么引入foo的对象看到的不是ProxyFactoryBean实例本身,而是由ProxyFactoryBean中的getObject() 方法的始实现创建的对象。该分发创建了一个封装目标对象的AOP代理。 使用ProxyFactoryBean或其他IOC感知类型创建AOP代理的最重要有时之一是,建议和指向也可以由IOC管理。这是一个功能强大的特性,可以实现其他AOP框架难以实现的某些分发。例如,建议本身可以引入应用程序对象(除目标对象外,任何AOP框架都应提供目标对象),从而受益依赖注入(Dependency Injection)提供的所有可插拔性。
6.4.2 JavaBean
与Spring提供的大多数FactoryBean实现一样,ProxyFactoryBean类本身也是一个JavaBean。其属性用于
指定要代理的目标
指定是否使用CGLIB(稍后描述,另请参阅基于JDK和CGLIB的代理)。
一些关键属性继承自org.springframework.aop.framework.ProxyConfig(Spring中所有AOP代理工厂的超类)。这些关键属性包括以下内容:
proxyTargetClass:true,如果要代理ide目标类,而不是目标类的接口。如果该属性值设置为true,则会创建CGLIB代理(另请参阅基于JDK和CGLIB的代理)。
optimize:控制是否对通常CGLIB创建的代理进行积极优化。除非你完全了解相关AOP代理如何处理优化,否则不应轻易使用此设置。此设置目前仅用于CGLIB代理。它对JDK动态代理没有影响。
frozen:如果代理设置为frozen,则不再允许更改配置。这不仅可以稍作优化,还可以再创建代理后不希望调用者(通过Advised接口)操作代理时使用。该属性的默认值为false,因此允许更改(如添加附加建议)。
exposeProxy:确定是否应在ThreaLocal中公开当前代理,以便目标可以访问它。如果目标需要获取代理,且exposeProxy属性被设置为true,则目标可以使用AopContext.currentProxy() 方法。
ProxyFactoryBean特有的其他属性包括以下内容:
proxyInterfaces:String接口名称数组。如果没有提供,则使用目标类的CGLIB代理(另请参阅基于JDK和CGLIB的代理)。
interceptorNames:String 数组,包含Advisor、拦截器或其他要应用的建议名称。排序很重要,以先来后到为原则。也就是说,列表中的第一个拦截器将首先拦截调用。
名称是当前工厂中的Bean名称,包括来自祖先工厂的Bean名称。你不能再次提及Bean引用,因为这样做会导致ProxyFactoryBean忽略建议的单利设置。 你可以在拦截器名称后添加星号。这样做的结果是,所有名称以星号前部分开头的顾问都会被应用。你可以使用“全局”顾问中找到使用此功能的示例。
单利:工厂是否应返回单一对象,无论getObject() 方法被调用多少次。一些FactoryBean实现提供了这种方法。默认值是true。如果你想使用有状态的建议(例如,有状态的混合体),请使用原型建议以及false的单例值。
6.4.3 JDK-and CGLIB-based proxies
本节是关于ProxyFactoryBean如何选择为特定目标对象(需要代理的对象)创建基于JDK的代理或基于CGLIB的代理的权威文档。
在Spring1.2.x和2.0版本之间,ProxyFactoryBean在创建基于JDK或CGLIB的代理方面的行为发生了变化。现在,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属性提供的目标类实现的每个接口的列表是一样的。不过,这种方法大大减少了工作量,也不容易出现排版错误。
6.4.4 Proxying Interfaces
请看 ProxyFactoryBean的一个简单示例。这个例子涉及
代理的目标Bean。这就是示例中的personTarget Bean定义。
一个Advisor和一个Interceptor用来提供建议。
AOP代理Bean定义用于指定目标对象(personTarget Bean)、代理接口和应用建议。
下面列出了实例:
请注意,interceptorNames属性接受一个String列表,其中包含当前工厂中拦截器或顾问的Bean名称。你可以使用顾问、拦截器、返回前、返回后和抛出建议对象。顾问的排序非常重要。
你可能向知道为什么列表中不包含Bean引用。原因在于,如果将ProxyFactoryBean的单利属性设置为false,它必须能够返回独立的代理实例。如果任何顾问本身就是原型,那么就需要返回一个独立的实例,因此必须能够从工厂获得唤醒的实例。仅持有引用是不够的。
前面显示的person Bean 定义可以用来替代Person实现,如下所示:
同一IOC上下文中其他Bean可以表达对它的强类型依赖,就像对普通Java对象一样。下面的实例展示了如何做到这一点:
本利中的PersonUser类暴露了一个Person类型的属性。其本身而言,AOP代理可以透明地替代“真实”的实现。不过,它的类将是一个动态代理类。可以将其转换为Advised接口(稍后讨论)。
你可以通过使用匿名内部类来隐藏目标和代理之间的区别。只有ProxyFactoryBean定义有所不同。此处包含的建议仅为完整起见。下面的实例展示了如何使用匿名内部Bean:
使用匿名内部Bean的好处是只有一个Peron类型的对象。如果我们想防止应用程序上下文的用户获得对未建议对象的引用,或者需要避免与Spring IOC自动注入产生任何歧义,这一点就非常有用。可以说,ProxyFactoryBean定义还具有自包含的优势。不过,有时从工厂中获取未建议的目标实际上也是一种优势(例如,在某些测试场景中)。
6.4.5 Proxying Classes
如果需要代理一个类,而不是一个或多个接口,该怎么办? 设想在我们前面的实例中,没有Person接口。我们需要建议一个名为Person的类,该类没有实现任何业务接口。在这种情况下,你可以将Spring配置为使用CGLIB代理而不是动态代理。为此,请将前面显示的ProxyFactoryBean上的proxyTargetClass属性设置为true。虽然编程时最后使用接口而不是类,但在处理遗留代码时,建议使用未实现接口的类的功能还是非常有用的。(总的来说,Spring并非规定性。虽然它可以让你轻松应用良好的实践,但也避免了强制使用特定的方法)。 如果你愿意,可以在任何情况下强制使用CGLIB,及时你的接口。 CGLIB代理的工作原理是在运行时生成目标类的子类。Spring会对生成的子类进行配置,以便将方法调用委派给原始目标类。子类用于实现装饰器模式,编织建议。 CGLIB代理一般来说对用户是透明的,不过,也有一些问题需要考虑:
Final 方法无法重载,因此无法提供建议。
无需在类路径中添加CGLIB。从Spring3.2开始,CGLIB被重新打包并包含在spring-coreJar中。换句话说,基于CGLIB的AOP和JDK动态代理一样,“开箱即用”。 CGLIB代理和动态代理在性能上差别不大。在这种情况下,性能不应成为决定性的考虑因素。
6.4.6 Using “Global” Advisors
通过在拦截器名称后添加星号,所有Bean名称与星号前部分匹配的顾问都会被添加到顾问链中。如果你需要添加一组标准的“全局”顾问,这将非常有用。下面的示例定义了两个全局顾问:
6.5 Concise Proxy Definitions
特别是在定义事件事务代理时,可能会出现许多类似的代理定义。使用父类和子类bean定义以及内部bean定义,可以使代理定义更加简洁了。 首先,我们为代理创建一个父模版bean定义,如下所示:
它本身从未实例化,因此实际上可能是不完整的。然后,需要创建的每个代理都是一个子bean定义,它将代理的目标封装为一个内部bean定义,因为目标本身永远不会被使用。下面的示例展示了这样一个子Bean:
你可以覆盖父模版的属性。在下面的示例中,我们将覆盖事务传播设置:
请注意,在父Bean的示例中,我们通过将abstract属性设置为true显式地将父Bean定义标记为抽象的,如前所述,这样它实际上就不会被实例化。应用程序上下文(但不是简单的Bean工厂)默认情况下会预先实例化所有单子。因此,重要的是(至少对单利Bean而言),如果你有一个只打算用作模版的(父)bean定义,并且该定义制定了一个类,你必须确保将abstract属性设置为true。否则,应用程序上下文实际上会尝试对其预实例化。
6.6 使用ProxyFactory以编程方式创建AOP代理
使用Spring可以轻松地以变成方式创建AOP代理。这样,你就可以在不依赖Spring IoC的情况下使用Spring AOP。 目标对象实现的接口会被自动代理。下面的列表显示了为目标对象创建代理的过程,其中包含一个拦截器和一个顾问:
第一步是构建一个org.springframework.aop.framework.ProxyFactory类型的对象。你可以使用目标对象创建该对象(如前面的示例),也可以在另一个构造函数中指定要代理的接口。 你可以添加建议(拦截器是建议的一种特殊类型)、顾问或两者,并在ProxyFactory的生命周期内对其进行操作。如果添加IntroductionInterceptionAroundAdvisor,则可以使代理实现其他接口。 ProxyFactory上还有一些方便的方法(继承自AdvisedSupport),可以让你添加其他建议类型,如before和throws建议。AdvisedSupport是ProxyFactory和ProxyFactoryBean的超类。
6.7 Manipulating Advised Objects
无论你如何创建AOP代理,都可以通过使用org.springframework.aop.framework.Advised接口来操作他们。任何AOP代理都可以被转换为该接口,无论它实现了那些其他接口。该接口包括以下方法:
getAdvisors() 方法会为已添加到工厂的每个顾问、拦截器或其他建议类型返回一个Advisor。如果你添加了一个Advisor,在此索引处返回的顾问就是你添加的对象。如果你添加了拦截器或其他建议类型,Spring会将其封装在一个顾问中,并使用一个总是返回true的点切口。因此,如果你添加了MethodInterceptor,则此索引返回的顾问是DefaultPointcutAdvisor,它将返回你的MethodInterceptor和一个与所有类和方法匹配的点切。 addAdvisor()方法可用于添加任何Advisor方法。通常,持有pointcut和建议的顾问是通用的DefaultPointcutAdvisor,你可以将其用于任何建议或pointcut(但不适用于介绍)。 默认情况下,即使代理已创建,也可以添加或删除顾问或拦截器。唯一的限制是无法添加或删除导入顾问,因为工厂中的现有代理不会显示接口更改。(你可以从工厂获取一个新的代理来避免这个问题)。 下面的示例展示了将AOP代理移植到Advised接口,并检查和操作其建议:
在生产中修改业务对象的建议是否可取(不是双关语(值得商榷,尽管毫无疑问存在合理的使用情况。不过,它在开发过程中(例如在测试中)可能非常有用。我们有时会发现,以拦截器或其他建议的形式添加测试代码,进入我们要测试的方法调用内部,是非常有用的。(例如,建议可以进入为该方法创建的事务内部,也许可以在标记事务回滚之前运行SQL来检查数据库是否已正确更新。
根据你创建代理的方式,你通常可以设置一个frozen标识。在这种情况下,Advised isFrozen() 方法返回true,而任何通过添加或删除来修改建议的尝试都会导致AopConfigException。冻结建议对象状态的功能在某些情况下非常有用(例如,防止调用代码删除安全拦截器)。
6.8 Using the “auto-proxy” facility
到目前为止,我们已经考虑通过使用ProxyFactoryBean或类似工厂bean来显式创建AOP代理。 Spring还允许我们使用“自动代理”bean定义,它可以自动代理选定的bean定义。这是建立在Spring的“Bean后置处理器”基础架构之上的,它可以在容器加载时修改任何Bean定义。 在这种模式下,你需要在XMLBean定义文件中设置一些特殊的Bean定义,以配置自动代理基础架构。这样,你就可以声明符合自动代理条件的目标。你无需使用ProxyFactoryBean。 有两种方法可以做到这一点:
通过使用自动代理创建器,在当前上下文中引用特定的bean。
自动代理创建的一种特殊情况值得单独考虑:由源级元数据属性驱动的自动代理创建。
6.8.1 Auto-proxy Bean Definitions
本节介绍org.springframework.aop.framework.autoproxy软件包提供的自动代理创建器。
BeanNameAutoProxyCreator
BeanNameAutoProxyCreator类是一个BeanPostProcessor,可自动为名称符合字面值或通配符的Bean创建AOP代理。下面的示例展示了如何创建BeanNameAutoProxyCreator Bean:
与ProxyFactoryBean一样,为了使圆形顾问的行为正确,也有一个interceptorNames属性,而不是拦截器列表。已命名的”拦截器“可以是顾问或任何建议类型。 与一般的自动代理一样,使用BeanNameAutoProxyCreator的主要目的是将相同采耳配置一致地用于多个对象,并将配置量降到最低。在对多个对象应用声明式事务时,它是一种常用的选择。 名称相匹配的Bean定义,如前面示例中jdkMyBean和onlyJdk是目标类的普通旧Bean定义。BeanNameAutoProxyCreator会自动创建一个AOP代理。相同的建议将应用于所有匹配的Bean。请注意,如果使用的是建议(而不是上例中的拦截器),那么对不同的Bean应用的指向性能可能不同。
DefaultAdvisorAutoProxyCreator
DefaultAdvisorAutoProxyCreator是一种更通用的,功能更强大的自动代理创建器。它会自动应用单签上下文中符合条件的顾问,而无需在自动代理顾问的bean定义中包含特定的bean名称。它与BeanNameAutoProxyCreator具有相同的优点,即一致的配置和避免重复。 使用这一机制包括
指定DefaultAdvisorAutoProxyCreator Bean定义
在相同或相关上下文中指定任意数量的顾问。请注意,这些必须是顾问,而不是拦截器或其他建议。这是必要的,因为必须有一个评估点切入,以检查候选bean定义的每个建议是否合格。
DefaultAdvisorAutoProxyCreator会自动评估每个顾问中包含的点切,以确认它应该对每个业务对象(例如示例中的businessObject1和businessObject2)应用那些建议(如果有的话)。 这意味着每个业务对象可以自动应用任意数量的顾问。如果顾问中没有任何点切与业务对象中的任何方法相匹配,则不会代理该对象。在为新业务对象添加bean定义时,如有必要,会自动带独立这些bean的定义。 一般而言,自动代理的优点使调用者或依赖关系无法获得未经检疫的对象。再次APplicationContext上调用getBean("businessObject1") 将返回一个AOP代理,而不是目标业务晒专项。(前面展示的”内部bean“习语也具有这个优点)。 下面的示例创建一个DefaultAdvisorAutoProxyCreator Bean和本节讨论的其他元素:
如果你相对许多业务对象一致应用相同的建议,DefaultAdvisorAutoProxyCreator将非常有用。一旦基础架构定义到位,你就可以添加新的业务对象,而无需包含特定的代理配置。你还可以轻松添加其他切面(例如,跟踪或性能监控方面),而只需对配置进行最小的更改。 DefaultAdvisorAutoProxyCreator提供了对过滤(通过使用命名约定,只对某些顾问进行评估,从而允许在同一工厂中使用多个不同的配置的AdvisorAutoProxyCreator) 和排序的支持。如果存在问题,顾问可以实现org.springframework.core.Ordered接口,以确保排序正确。前面实例中使用的TransactionAttibuteSourceAdvisor具有可配置的顺序值。默认设置为无序。
6.9 Using TargetSource Implementtations
Spring 提供了TargetSource的概念,用org.springframework.aop.TargetSource接口表示。该接口负责返回实现连接点的“目标对象”。每次AOP代理处理方法调用时,都会要求TargetSource实现提供目标实例。 如果你没有指定TargetSource,将使用默认实现来封装本地对象。每次调用都会返回相同的目标(正如你所期望的)。 本节其余部分将介绍Spring提供的标准目标源,以及如何使用它们。
6.9.1 热拔插目标源
被切换,同时让调用者保留对它的引用。 更改目标源的目标会立即生效。HostSwappableTargetSource是线程安全的。 你可以使用HostSwappableTargetSource上的swap()方法更改目标,如下示例所示:
下面的示例显示了所需的XML定义:
前面的swap()调用会更改可交换Bean目标。持有该Bean引用的客户端不会察觉到这一变化,但会立即开始访问新的目标。 虽然这个示例没有添加任何建议(使用TargetSource不一定要添加建议),单任何TargetSource都可以与任意建议结合使用。
6.9.2 Pooling TargetSources
使用池目标源可提供与无状态会话EJB类似的变成模型,即维护一个由相同实例组成的池,方法调用将指向池中的空闲对象 Spring池与SLSB池的一个重要区别是,Spring池可以应用于任何POJO。与一般的Spring一样,这种服务可以以非入侵的方式应用。 Spirng 支持Commons Pool2.3,它提供了相当高效的池化实现。要使用此功能,你需要在应用程序的类路径中添加commons-pool.Jar。你还可以子类化org.springframework.aop.target.AbstractPoolTargetSource以支持任何其他池API
下面列出了一个配置示例:
请注意,目标对象(上例中的businessObjectTarget)必须是一个原型。这样PoolingTargetSource的视线就可以创建目标对象的新实例,从而在必要时扩大池的规模。有关于其属性的信息,请参阅AbstractPoolingTargetSource的javadoc和你希望使用的具体子类。maxSize是最基本的,并且始终保证存在。 在这种情况下,myInterceptor是需要同一IoC上下文中定义的拦截器的名称。但是,使用池化需要指定拦截器。如果你只想使用池处理而不需要其他建议,请不要设置interceptorNames属性。 你可以对Spring进行配置,使其能够将任何池对象投向org.springframework.aop.target.PoolingConfig接口,该接口会通过介绍公开有关池的配置和当前大小的信息。你需要定义一个类似于下面的顾问:
该顾问是通过调用AbstractPoolingTargetSource类的方便方法获得的,因此使用MethodInvokingFactoryBean。该顾问的名称(poolConfigAdvisor,此处)必须位于暴露池对想的ProxyFactoryBean中的拦截器名称列表中。 演员的定义如下:
使用自动代理可以实现更简单的池化。你可以设置任何自动代理创建者使用的TargetSource实现。
6.9.3 Prototype TargetSource
设置“原型”目标源与设置池TargetSource类似。在这种情况下,每次方法调用都会创建一个新的目标实例。虽然在现在JVM中创建一个新对象成本并不高,单链接新对象(满足其IOC依赖关系)的成本可能会更高。因此,如果没有充足的理由,你不应该使用这种方法。 为此,你可以对前面显示的poolTargetSource定义进行如下修改(为了清晰可见,我们还更改了名称):
唯一的属性是目标Bean的名称。在TargetSource视线中使用了继承,以确保命名的一致性。与池化目标源一样,目标Bean必须是一个原型Bean定义。
6.9.2 ThreadLocal TargetSource
如果你需要为每个传入请求(即每个线程)创建一个对象,ThreadLocal目标源将非常有用。ThreadLocal的概念提供了一种JDK范围内的工具,可以在线程旁边透明地存储资源。如下例所示,设置ThreadLocalTargetSource的方法与解释其他类型目标源的方法基本相同:
在多线程和多类加载器环境中错误使用ThreadLocal实例会带来严重问题(可能导致内存泄露)。你应始终考虑在其他类中封装线程本地,而不要直接使用ThreadLocal本身(封装类除外)。此外,你应始终牢记正确设置和取消设置(后者只涉及调用ThreadLocal.set( null)) 线程本地资源。在任何情况下都应取消设置,因为不取消设置可能会导致有问题的行为。Spring的ThreadLocal支持可以帮你做到这一点,因此在使用ThreadLocal实例时,如果没有其他适当的处理代码,则应考虑使用ThreadLocal实例。
6.10 Defining New Advice Types
Spring AOP的设计具有可扩展性。虽然目前内部使用是拦截实现策略,但出了环绕建议、之前、抛出建议和之后返回建议的拦截之外,还可以支持任意建议类型。 org.springframewrok.aop.framework.adapter包是一个SPI包,它允许在不改变核心框架的情况下添加对新的自定义建议类型的支持。对自定义Advice类型的唯一限制是它必须实现org.aopalliance.aop.Advice标记接口。 有关详细信息,请参阅org.springframework.aop.framework.adapter javadoc。