Spring对AOP的支持2

如果你需要代理的是类,而不是一个或多个接口,又该怎么办呢?

想象一下我们上面的例子,如果没有Person接口, 我们需要通知一个叫Person的类, 而且该类没有实现任何业务接口。在这种情况下,你可以配置Spring使用CGLIB代理, 而不是动态代理。你只要在上面的ProxyFactoryBean定义中把 它的proxyTargetClass属性改成true就行了。

只要你愿意,即使在有接口的情况下,你也可以强迫Spring使用CGLIB代理。

CGLIB代理是通过在运行期产生目标类的子类来进行工作的。 Spring可以配置这个生成的子类,来代理原始目标类的方法调用。这个子类是用 Decorator设计模式置入到advice中的。

CGLIB代理对于用户来说应该是透明的。然而,还有以下一些因素需要考虑:

  • Final方法不能被通知,因为不能被重写。
  • 你需要在你的classpath中包括CGLIB的二进制代码,而动态代理对任何JDK都是可用的.

CGLIB和动态代理在性能上有微小的区别,对Spring 1.0来说,后者稍快。 另外,以后可能会有变化。在这种情况下性能不是决定性因素

5.6.便利的代理创建方式

通常,我们不需要ProxyFactoryBean的全部功能,因为我们常常只对一个方面感兴趣: 例如,事务管理。

当我们仅仅对一个特定的方面干兴趣时,我们可以使用许多便利的工厂来创建AOP代理。这些在其他 章节讨论,所以这里我们快速浏览一下它们。

5.6.1.TransactionProxyFactoryBean

用Spring提供的jPetStore的示例应用 演示了TransactionProxyFactoryBean的使用方式。

TransactionProxyFactoryBean是ProxyConfig的子类, 因此基本配置信息是和ProxyFactoryBean共享的。 (见上面ProxyConfig的属性列表。)

下面的代码来自于JPetStore application,演示了ProxyFactoryBean是如何工作的。跟ProxyFactoryBean一样,存在一个目标bean的定义。类的依赖关系定义在代理工厂bean定义中(petStore),而不是普通Java对象(petStoreTarget)。

TransactionProxyFactoryBean需要设置一个target属性, 还需要设置“transactionAttributes”, “transactionAttributes”用来指定需要事务化 处理的方法,还有要求的传播方式和其他设置:

<bean id="petStoreTarget" class="org.springframework.samples.jpetstore.domain.logic.PetStoreImpl">

<property name="accountDao"><ref bean="accountDao"/></property>

<!-- Other dependencies omitted -->

</bean>

<bean id="petStore"

class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">

<property name="transactionManager"><ref bean="transactionManager"/></property>

<property name="target"><ref local="petStoreTarget"/></property>

<property name="transactionAttributes">

<props>

<prop key="insert*">PROPAGATION_REQUIRED</prop>

<prop key="update*">PROPAGATION_REQUIRED</prop>

<prop key="*">PROPAGATION_REQUIRED,readOnly</prop>

</props>

</property>

</bean>

TransactionProxyFactoryBean自动创建一个事务advisor, 该advisor包括一个基于事务属性的切入点。因此只有事务性的方法被通知。

TransactionProxyFactoryBean使用preInterceptors和 postInterceptors属性指定“pre”和“post”通知。它们将拦截器,通知和Advisor数组放置 在事务拦截器前后的拦截器链中。使用XML格式的bean定义中的<list>元素定义,就 象下面一样:

<property name="preInterceptors">

<list>

<ref local="authorizationInterceptor"/>

<ref local="notificationBeforeAdvice"/>

</list>

</property>

<property name="postInterceptors">

<list>

<ref local="myAdvisor"/>

</list>

</property>

这些属性可以加到上面的“petStore”的bean定义里。一个通用用法是将事务和声明式 安全组合在一起使用:一个和EJB提供的类似的方法。

因为使用前拦截器和后拦截器时,用的是真正的实例引用,而不象在 ProxyFactoryBean中用的bean的名字,因此它们只能用于共享实例的通知。因此它们不能用在有状态的通知中:例如,在mixin中。这和TransactionProxyFactoryBean的要求是一致的。如果你需要更复杂的,可以定制的AOP,你可以考虑使用普通的ProxyFactoryBean,或者是自动代理生成器(参考下面)。

尤其是如果我们将Spring的AOP在许多情况下看成是EJB的替代品,我们会发现大多数通知是很普通的, 可以使用共享实例。声明式的事务管理和安全检查是一个典型的例子。

TransactionProxyFactoryBean依赖于由它的transactionManager 属性指定的TransactionManager。这种事务管理方式是可插拔的,基于JTA,JDBC或者其他事务管理策略皆可。这与Spring的事务抽象层有关,而不在于AOP本身。我们将在下一章中讨论事务机制。

如果你只对声明性事务管理感兴趣,TransactionProxyFactoryBean是一个不错的解决办法,并且比直接使用ProxyFactoryBean来得简单.

5.6.2.EJB 代理

其它有一些专门的代理用于创建EJB代理,使得EJB的“业务方法”的接口可以被调用代码直接使用。 调用代码并不需要进行JNDI查找或使用EJB的创建方法:这是在可读性和架构灵活性方面的重大提高。

进一步请参考本手册内的Spring的EJB业务。

5.7.使用ProxyFactory以编程的方式创建AOP代理

使用Spring以编程的方式创建AOP代理也很简单。 这使得你不需要Spring的IoC就能够使用Spring的AOP。

下面的代码显示的用拦截器和advisor为目标对象创建代理。目标对象实现的接口将自动被代理:

ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);

factory.addInterceptor(myMethodInterceptor);

factory.addAdvisor(myAdvisor);

MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();

第一步是创建类型为org.springframework.aop.framework.ProxyFactory 的对象。你可以和上面的例子一样用目标对象创建,或者在另一个构造函数中指定要被代理的接口。

你可以添加拦截器或advisor,在整个ProxyFactory的生命周期内操作它们。如果你添加 IntroductionInterceptionAroundAdvisor,你可以使代理实现附加接口。

ProxyFactory(它是从AdvisedSupport继承而来)也提供了一些实用方法,使你可以添加 其它通知类型,比如before通知和throws通知。AdvisedSupport是ProxyFactory和ProxyFactoryBean 的父类。

将AOP代理的创建和IoC框架结合起来在大多数应用中都是最好的实现方式。我们推荐你和一般情况一样,不要将AOP配置信息放在Java代码里。

5.8.操作被通知对象

无论你怎么创建AOP代理,你都可以使用org.springframework.aop.framework.Advised 接口来操作它们。任何AOP代理无论实现其它什么接口,都可以类型转换为这个接口。这个接口包括下列方法:

void addInterceptor(Interceptor interceptor) throws AopConfigException;

void addInterceptor(int pos, Interceptor interceptor)

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,使用当前索引返回的advisor就是你添加的对象。如果你添加拦截器或其它通知类型, Spring将当前对象和一个满足要求的切入点封装在一个advisor里。因此,如果你添加 MethodInterceptor, 使用当前索引返回的advisor是一个DefaultPointcutAdvisor,这个advisor返回 MethodInterceptor和满足所有类和方法的切入点。

addAdvisor()被用来添加Advisor。通常会是一个普通的 DefaultPointcutAdvisor,它可以和任何通知或切入点(除了引用)一起使用。

缺省情况下,在每次代理被创建的时候添加或删除advisor或拦截器。唯一的限制是不能添加或删除 引入advisor,因为工厂提供的已存在的代理不反映接口的变化。(你可以从工厂得到一个新的代理来避免这个问题)

是否建议在产品中修改业务对象的通知还值得怀疑,虽然毫无疑问存在合理的使用情况。但是, 在开发中这是非常有用的:例如,在测试中。我有时候发现以拦截器或其它通知的形式来添加测试代码非常有用,这样就可以进入我想要测试的方法调用。(例如,通知可以进入为这个方法创建的事务中: 在为回滚事务作标记前,运行SQL检查数据库是否被正确更新。)

根据你创建代理的方式,你通常可以设置frozen标记,这样Advised 的isFrozen()就返回true,任何添加或删除通知都将导致 AopConfigException。这种冻结被通知对象状态的方法在一些情况下是非常有用的:例如,为了阻止调用代码删除一个安全拦截器。如果已知运行时修改通知不被允许,这还可以被Spring 1.1用来 作优化。

5.9.使用“autoproxy”功能

目前为止,我们已经讨论了使用ProxyFactoryBean或类似的工厂bean来显式创建AOP代理。

Spring也允许我们使用“autoproxy”的bean定义,它可以自动代理所选择的bean定义。这是建立在Spring的 “bean后处理器”机制上的,它能够在容器载入bean定义的时候修改任何bean定义。

在这个模型中,你可以在你的XML bean定义文件中建立特殊的bean定义,来配置自动代理机制。这允许你声明目标对象以使用自动代理功能:你就可以不需要使用ProxyFactoryBean。

有两种方法来实现自动代理:

  • 使用一个自动代理生成器,它引用当前上下文中的那些特殊bean
  • 有一个特殊的自动代理创建的情况值得单独考虑:由源代码级元数据驱动的自动代理创建

5.9.1.自动代理的bean定义

org.springframework.aop.framework.autoproxy包提供了下列标准自动代理生成器。

5.9.1.1.BeanNameAutoProxyCreator

BeanNameAutoProxyCreator为名字符合某个值或统配符的bean自动创建AOP代理。

<bean id="jdkBeanNameProxyCreator"

class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">

<property name="beanNames"><value>jdk*,onlyJdk</value></property>

<property name="interceptorNames">

<list>

<value>myInterceptor</value>

</list>

</property>

</bean>

就和ProxyFactoryBean一样,有一个interceptorNames 属性,而不是一个拦截器列表,这个属性允许为prototype的advisor提供正确的行为。虽然名字叫 “拦截器”,但是也可以是advisor或任何通知类型。

就象一般的自动代理创建一样,使用BeanNameAutoProxyCreator的主要目的 是对多个对象使用相同的配置信息,并且减少配置的工作量。这在为多个对象使用声明式事务时是一个很流行的选择。

在上面的例子中,名字匹配的bean定义,如“jdkMyBean”和“onlyJdk”,是包含目标类的普通bean定义。 BeanNameAutoProxyCreator将自动创建AOP代理。相同的通知会被因用到所有匹配的bean。 注意,如果使用了advisor(而不是上面例子中的拦截器),切入点可能对不同的bean会不同。

5.9.1.2.DefaultAdvisorAutoProxyCreator

DefaultAdvisorAutoProxyCreator是一个更通用,更强大的自动代理生成器。它将 自动应用于当前上下文的符合条件的advisor,而不需要在自动代理advisor的bean定义中包含特定的bean名字。它有助于配置的一致性,并避免象BeanNameAutoProxyCreator一样重复配置。

使用这个机制包括:

  • 指定一个DefaultAdvisorAutoProxyCreator的bean定义
  • 在相同或相关上下文中指定任何数目的Advisor。注意这些必须是Advisor, 而不仅仅是拦截器或其它通知。这是很必要的,因为必须有一个切入点来检查每个通知是否符合候选bean定义。

DefaultAdvisorAutoProxyCreator会自动计算每个advisor包含的的切入点,看看 是否有什么通知应该被引用到每个业务对象(比如例子中的“businessObject1”和“businessObject2”)。

这意味着任何数目的advisor都可以自动应用到每个业务对象。如果advisor中没有任何切入点符合业务对象的方法,这个对象就不会被代理。因为会为新的业务对象添加bean定义,如果必要,它们会自动被代理。

一般来说,自动代理可以保证调用者或依赖无法接触未被通知的对象。在这个ApplicationContext上 调用getBean("businessObject1")返回一个AOP代理,而不是目标业务对象。

<bean id="autoProxyCreator"

class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">

</bean>

<bean id="txAdvisor"

autowire="constructor"

class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">

<property name="order"><value>1</value></property>

</bean>

<bean id="customAdvisor"

class="com.mycompany.MyAdvisor">

</bean>

<bean id="businessObject1"

class="com.mycompany.BusinessObject1">

<!-- Properties omitted -->

</bean>

<bean id="businessObject2"

class="com.mycompany.BusinessObject2">

</bean>

如果你想在几个业务对象上应用相同的通知,DefaultAdvisorAutoProxyCreator 就非常有用。一旦定义恰当,你可以简单地添加业务对象而不需要包括特定的代理配置。你也可以非常容易地删除所附加的方面--例如,跟踪或性能监控的方面--可以尽可能减少配置修改。

DefaultAdvisorAutoProxyCreator支持过滤(使用命名规则以便只计算某一些 advisor,允许在一个工厂中使用多个,被不同配置的AdvisorAutoProxyCreator)和排序。Advisor可以实现 org.springframework.core.Ordered接口以保证正确的排序,如果排序确实需要。在 上面的例子中,TransactionAttributeSourceAdvisor有一个可配置的顺序值,缺损是不排序。

5.9.1.3.AbstractAdvisorAutoProxyCreator

这是DefaultAdvisorAutoProxyCreator的父类。你可以继承它实现你自己的自动代理生成器,这种情况不太常见,一般是advisor定义不能给DefaultAdvisorAutoProxyCreator框架的行为提供足够的定制。

5.9.2.使用元数据驱动的自动代理

一种特别重要的自动代理类型是由元数据驱动的。这和.NET的ServicedComponents编程框架 非常类似。它没有象EJB那样使用XML部署描述,事务管理和其它企业级业务的配置都是定义在源代码级的属性上。

在这种情况下,你可以使用DefaultAdvisorAutoProxyCreator,以及可以读取元数据属性的 Advisor。元数据细节定义在候选advisor的切入点部分,而不是自动代理创建类本身。

这是DefaultAdvisorAutoProxyCreator的一种特殊情况,但是它本身而言是值得考虑的。 (可以读取元数据的代码处于advisor的切入点中,而不是AOP框架本身。)

jPetStore示例应用的/attributes目录演示了属性驱动的自动代理的使用。在这个例子中, 没有必要使用TransactionProxyFactoryBean。仅仅在业务对象上定义业务属性就足够了,因为使用了可知元数据的切入点。bean定义在/WEB-INF/declarativeServices.xml中,包括下面的代码。注意这是通用的,可以在jPetStore以外的地方使用:

<bean id="autoproxy"

class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">

</bean>

<bean id="transactionAttributeSource"

class="org.springframework.transaction.interceptor.AttributesTransactionAttributeSource"

autowire="constructor">

</bean>

<bean id="transactionInterceptor"

class="org.springframework.transaction.interceptor.TransactionInterceptor"

autowire="byType">

</bean>

<bean id="transactionAdvisor"

class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor"

autowire="constructor" >

</bean>

<bean id="attributes"

class="org.springframework.metadata.commons.CommonsAttributes"

/>

DefaultAdvisorAutoProxyCreator bean定义--在这种情况下称作“advisor”,但是名字 无关紧要--会在当前的应用上午中选择所有符合的切入点。在这个例子中,类型为 TransactionAttributeSourceAdvisor的“transactionAdvisor”bean定义将会应用于包含事务属性的类或方法。TransactionAttributeSourceAdvisor通过构造函数依赖于TransactionInterceptor。这个例子通过自动 装配来解析它。AttributesTransactionAttributeSource依赖于 org.springframework.metadata.Attributes接口的一个实现。在这段代码中,“attributes” bean使用Jakarta Commons Attributes API来获取属性信息。(应用代码必须使用Commons Attributes编译任务编译。)

这里定义的TransactionInterceptor依赖于一个 PlatformTransactionManager定义,它并没有被包括在这个通用的文件中(虽然应该是这样), 这是因为它是和应用的事务需求相关的(一般地,是想这个例子中的JTA,或者Hibernate,JDO 或JDBC):

<bean id="transactionManager"

class="org.springframework.transaction.jta.JtaTransactionManager"/>

如果你只要求声明式事务管理,使用这些通用的XML定义就可以使得Spring自动代理含有事务属性的所有类和方法。 你不需要直接和AOP打交道,并且编程模型和.NET的ServicedComponents非常相似。

这个机制具有可扩展性。它可以基于定制的属性来使用自动代理。你需要:

  • 定义你的定制属性。
  • 指定的Advisor包含必要的通知和由方法或类的定制属性所触发的切入点。你可以使用已经存在的通知,仅仅实 现用来选择定制属性的切入点。

这些advisor可能对每个被通知类都是唯一的(例如,maxin)。它们仅仅需要被定义成 prototype bean,而不是singleton bean。例如,Spring的测试套件中的LockMixin 引入拦截器可以和一个属性驱动切入点一起来定位一个maxin,就象这里演示的。我们使用JavaBean配置的 普通的DefaultPointcutAdvisor:

<bean id="lockMixin"

class="org.springframework.aop.LockMixin"

singleton="false"

/>

<bean id="lockableAdvisor"

class="org.springframework.aop.support.DefaultPointcutAdvisor"

singleton="false"

>

<property name="pointcut">

<ref local="myAttributeAwarePointcut"/>

</property>

<property name="advice">

<ref local="lockMixin"/>

</property>

</bean>

<bean id="anyBean" class="anyclass" ...

如果知道属性的切入点符合anyBean或者其它bean定义中的任何方法,这个maxin 将被应用。注意,lockMixin和lockableAdvisor定义都是 prototype的。myAttributeAwarePointcut切入点可以被定义成singleton,因为它不为不同的被通知对象保存状态。

5.10.使用TargetSources

Spring提供了TargetSource的概念,由 org.springframework.aop.TargetSource接口定义。这个接口负责返回实现切入点的 “目标对象”。每次AOP代理处理方法调用时,目标实例都会用到TargetSource实现。

使用Spring AOP的开发者一般不需要直接使用TargetSources,但是这提供了一种强大的方法来支持池,热交换,和其它复杂目标。例如,一个支持池的TargetSource可以在每次调用时返回不同的目标对象实例,使用池来管理实例。

如果你没有指定TargetSource,就使用缺省的实现,它封装了一个本地对象。每次调用会返回相同的目标对象 (和你期望的一样)。

让我们来看一下Spring提供的标准目标源,以及如何使用它们。

当使用定制目标源时,你的目标通常需要定义为prototype bean,而不是singleton bean。这使得 Spring在需要的时候创建一个新的目标实例。

5.10.1.可热交换的目标源

org.springframework.aop.target.HotSwappableTargetSource 允许切换一个AOP代理的目标,而调用者维持对它的引用。

修改目标源的目标会立即起作用。并且HotSwappableTargetSource是线程安全的。

你可以通过HotSwappableTargetSource的swap()方法 来改变目标,就象下面一样:

HotSwappableTargetSource swapper =

(HotSwappableTargetSource) beanFactory.getBean("swapper");

Object oldTarget = swapper.swap(newTarget);

所需的XML定义如下:

<bean id="initialTarget" class="mycompany.OldTarget">

</bean>

<bean id="swapper"

class="org.springframework.aop.target.HotSwappableTargetSource">

<constructor-arg><ref local="initialTarget"/></constructor-arg>

</bean>

<bean id="swappable"

class="org.springframework.aop.framework.ProxyFactoryBean"

>

<property name="targetSource">

<ref local="swapper"/>

</property>

</bean>

上面的swap()调用会修改swappable这个bean的目标。持有对这个bean应用的客户端 将不会知道这个变化,但会立刻转为使用新的目标对象。

虽然这个例子没有添加任何通知--使用TargetSource也没必要添加通知-- 当然任何TargetSource都可以和任何一种通知一起使用。

5.10.2.支持池的目标源

使用支持池的目标源提供了一种和无状态的session EJB类似的编程模式,在无状态的session EJB中,维护了一个相同实例的池,提供从池中获取可用对象的方法。

Spring的池和SLSB的池之间的重要区别在于Spring的池可以被应用到任何普通Java对象。就象Spring的通用 的做法,这个业务也可以以非侵入的方式被应用。

Spring直接支持Jakarta Commons Pool 1.1,它是一种非常高效的池实现。使用这个功能,你需要在你的应用的 classpath中添加commons-pool的Jar文件。也可以直接继承 org.springframework.aop.target.AbstractPoolingTargetSource来支持其它池API。

下面是一个配置的例子:

<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject"

singleton="false">

... properties omitted

</bean>

<bean id="poolTargetSource"

class="org.springframework.aop.target.CommonsPoolTargetSource">

<property name="targetBeanName"><value>businessObject</value></property>

<property name="maxSize"><value>25</value></property>

</bean>

<bean id="businessObject"

class="org.springframework.aop.framework.ProxyFactoryBean"

>

<property name="targetSource"><ref local="poolTargetSource"/></property>

<property name="interceptorNames"><value>myInterceptor</value></property>

</bean>

注意例子中的目标对象“businessObjectTarget”必须是prototype。这样在 PoolingTargetSource的实现在扩大池容量的时候可以创建目标的新实例。关于这些属性的 信息可以参考AbstractPoolingTargetSource和子类的Javadoc。maxSize是最基本的属性, 被保证总是存在。

在这种情况下,名字为“myInterceptor”的拦截器需要定义在同一个IoC上下文中。但是,并不一定需要指定拦截器也用池。如果你仅需要池,并且没有其它通知,可以根本不设置属性interceptorNames。

也可以配置Spring以便可以将任何池化的对象转换类型为 org.springframework.aop.target.PoolingConfig接口。通过这个接口的一个引入,可以得到 配置信息和池的当前大小。你需要这样定义一个advisor:

<bean id="poolConfigAdvisor"

class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">

<property name="target"><ref local="poolTargetSource" /></property>

<property name="targetMethod"><value>getPoolingConfigMixin</value></property>

</bean>

通过调用AbstractPoolingTargetSource类上的方法,可以得到这个advisor,因此使用MethodInvokingFactoryBean。这个advisor的名字(“poolConfigAdvisor”)必须在暴露池化对象的 This advisor is obtained by calling a convenience method on the ProxyFactoryBean中的拦截器名字列表中。

这个类型转换就象下面:

PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");

System.out.println("Max pool size is " + conf.getMaxSize());

池化无状态业务对象并不是总是必要的。我们不认为这是缺省选择,因为大多数无状态对象自然就是线程 安全的,如果资源被缓存,实例池化会有问题。

简单的池也可以使用自动代理。任何自动代理生成器都可以设置TargetSources。

5.10.3.Prototype目标源

设置“prototype”目标源和支持池的目标源类似。在每次方法调用的时候都会创建一个新的目标实例。 虽然在现代JVM中创建对象的代价不是很高,但是装配新对象的代价可能更高(为了maz满足它的IoC依赖关系)。 因此没有好的理由不应该使用这个方法。

为了这么做,你可以修改上面的的poolTargetSource定义,就向下面一样。 (为了清晰起见,我修改了名字。)

<bean id="prototypeTargetSource"

class="org.springframework.aop.target.PrototypeTargetSource">

<property name="targetBeanName"><value>businessObject</value></property>

</bean>

只有一个属性:目标bean的名字。在TargetSource实现中使用继承是为了保证命名的一致性。就象支持池的目标源一样,目标bean必须是一个prototype的bean定义。

5.11.定义新的通知类型

Spring AOP设计能够很容易地扩展。虽然拦截实现的策略目前只在内部使用,但还是有可能支持拦截around通知, before通知,throws通知和after returning通知以外的任何通知类型。

org.springframework.aop.framework.adapter 包是一个支持添加新的定制通知类型而不修改核心框架的SPI(译:可能是API)包。定制通知类型的唯一限制是它必须实现 org.aopalliance.aop.Advice标记接口。

更多信息请参考org.springframework.aop.framework.adapter包的Javadoc。

5.12.进一步的资料和资源

对于AOP的介绍,我推荐Ramnivas Laddad (Manning, 2003)写的AspectJ in Action

进一步的Spring AOP的例子请参考Spring的示例应用:

  • JPetStore的缺省配置演示了使用TransactionProxyFactoryBean来定义声明式事务管理。
  • JPetStore的/attributes目录演示了属性驱动的声明式事务管理。

如果你对Spring AOP更多高级功能感兴趣,可以看一下测试套件。测试覆盖率超过90%,并且演示了本文档没有提到的许多高级功能。

5.13.路标

Spring AOP,就象Spring的其它部分,是开发非常活跃的部分。核心API已经稳定了。象Spring的其它部分一样, AOP框架是非常模块化的,在保留基础设计的同时提供扩展。在Spring 1.1到1.2阶段有很多地方可能会有所提高,但是这 些地方也保留了向后兼容性。它们是:

  • 性能的提高:AOP代理的创建由工厂通过策略接口处理。因此我们能够支持额外的AOP 代理类型而不影响用户代码或核心实现。对于Spring 1.1,我们正在检查AOP代理实现的所有字节码,万一不需要 运行时通知改变。这应该大大减少AOP框架的额外操作。但是注意,AOP框架的额外操作不是在普通使用中需要考虑 的内容。
  • 更具表达力的切入点:Spring目前提供了一个具有表达力的切入点接口,但是我们 添加更多的切入点实现。我们正在考虑提供一个简单但具有强大表达式语言的实现。如果你希望贡献一个有用的 切入点实现,我们将非常欢迎。
  • 引入方面这个高层概念,它包含多个advisor。

此前对于AOP的使用仅限于声明式事务,除此之外在实际开发中也没有遇到过与之相关的问题。最近项目中遇到了以下几点需求,仔细思考之后,觉得采用AOP 来解决。一方面是为了以更加灵活的方式来解决问题,另一方面是借此机会深入学习Spring AOP相关的内容。本文是权当本人的自己AOP学习笔记,以下需求不用AOP肯定也能解决,至于是否牵强附会,仁者见仁智者见智。

  1. 对部分函数的调用进行日志记录,用于观察特定问题在运行过程中的函数调用情况
  2. 监控部分重要函数,若抛出指定的异常,需要以短信或邮件方式通知相关人员
  3. 金控部分重要函数的执行时间

事实上,以上需求没有AOP也能搞定,只是在实现过程中比较郁闷摆了。

  1. 需要打印日志的函数分散在各个包中,只能找到所有的函数体,手动添加日志。然而这些日志都是临时的,待问题解决之后应该需要清除打印日志的代码,只能再次手动清除^_^!
  2. 类似1的情况,需要捕获异常的地方太多,如果手动添加时想到很可能明天又要手动清除,只能再汗。OK,该需求相对比较固定,属于长期监控的范畴,并不需求临时添加后再清除。然而,客户某天要求,把其中20%的异常改为短信提醒,剩下的80%改用邮件提醒。改之,两天后,客户抱怨短信太多,全部改成邮件提醒...
  3. 该需求通常用于监控某些函数的执行时间,用以判断系统执行慢的瓶颈所在。瓶颈被解决之后,烦恼同情况1


终于下定决心,采用AOP来解决!代码如下:

切面类TestAspect

Java代码

  1. packagecom.spring.aop;
  2. /**
  3. *切面
  4. *
  5. */
  6. publicclassTestAspect{
  7. publicvoiddoAfter(JoinPointjp){
  8. System.out.println("logEndingmethod:"
  9. +jp.getTarget().getClass().getName()+"."
  10. +jp.getSignature().getName());
  11. }
  12. publicObjectdoAround(ProceedingJoinPointpjp)throwsThrowable{
  13. longtime=System.currentTimeMillis();
  14. ObjectretVal=pjp.proceed();
  15. time=System.currentTimeMillis()-time;
  16. System.out.println("processtime:"+time+"ms");
  17. returnretVal;
  18. }
  19. publicvoiddoBefore(JoinPointjp){
  20. System.out.println("logBeginingmethod:"
  21. +jp.getTarget().getClass().getName()+"."
  22. +jp.getSignature().getName());
  23. }
  24. publicvoiddoThrowing(JoinPointjp,Throwableex){
  25. System.out.println("method"+jp.getTarget().getClass().getName()
  26. +"."+jp.getSignature().getName()+"throwexception");
  27. System.out.println(ex.getMessage());
  28. }
  29. privatevoidsendEx(Stringex){
  30. //TODO发送短信或邮件提醒
  31. }
  32. }

package com.spring.aop;

/**

* 切面

*

*/

public class TestAspect {

public void doAfter(JoinPoint jp) {

System.out.println("log Ending method: "

+ jp.getTarget().getClass().getName() + "."

+ jp.getSignature().getName());

}

public Object doAround(ProceedingJoinPoint pjp) throws Throwable {

long time = System.currentTimeMillis();

Object retVal = pjp.proceed();

time = System.currentTimeMillis() - time;

System.out.println("process time: " + time + " ms");

return retVal;

}

public void doBefore(JoinPoint jp) {

System.out.println("log Begining method: "

+ jp.getTarget().getClass().getName() + "."

+ jp.getSignature().getName());

}

public void doThrowing(JoinPoint jp, Throwable ex) {

System.out.println("method " + jp.getTarget().getClass().getName()

+ "." + jp.getSignature().getName() + " throw exception");

System.out.println(ex.getMessage());

}

private void sendEx(String ex) {

//TODO 发送短信或邮件提醒

}

}

Java代码

  1. packagecom.spring.service;
  2. /**
  3. *接口A
  4. */
  5. publicinterfaceAService{
  6. publicvoidfooA(String_msg);
  7. publicvoidbarA();
  8. }

package com.spring.service;

/**

* 接口A

*/

public interface AService {

public void fooA(String _msg);

public void barA();

}

Java代码

  1. packagecom.spring.service;
  2. /**
  3. *接口A的实现类
  4. */
  5. publicclassAServiceImplimplementsAService{
  6. publicvoidbarA(){
  7. System.out.println("AServiceImpl.barA()");
  8. }
  9. publicvoidfooA(String_msg){
  10. System.out.println("AServiceImpl.fooA(msg:"+_msg+")");
  11. }
  12. }

package com.spring.service;

/**

*接口A的实现类

*/

public class AServiceImpl implements AService {

public void barA() {

System.out.println("AServiceImpl.barA()");

}

public void fooA(String _msg) {

System.out.println("AServiceImpl.fooA(msg:"+_msg+")");

}

}

Java代码

  1. packagecom.spring.service;
  2. /**
  3. *Service类B
  4. */
  5. publicclassBServiceImpl{
  6. publicvoidbarB(String_msg,int_type){
  7. System.out.println("BServiceImpl.barB(msg:"+_msg+"type:"+_type+")");
  8. if(_type==1)
  9. thrownewIllegalArgumentException("测试异常");
  10. }
  11. publicvoidfooB(){
  12. System.out.println("BServiceImpl.fooB()");
  13. }
  14. }

package com.spring.service;

/**

* Service类B

*/

public class BServiceImpl {

public void barB(String _msg, int _type) {

System.out.println("BServiceImpl.barB(msg:"+_msg+" type:"+_type+")");

if(_type == 1)

throw new IllegalArgumentException("测试异常");

}

public void fooB() {

System.out.println("BServiceImpl.fooB()");

}

}

ApplicationContext

Java代码

  1. <?xmlversion="1.0"encoding="UTF-8"?>
  2. <beansxmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:aop="http://www.springframework.org/schema/aop"
  5. xsi:schemaLocation="
  6. http://www.springframework.org/schema/beans
  7. http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
  8. http://www.springframework.org/schema/aop
  9. http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"
  10. default-autowire="autodetect">
  11. <aop:config>
  12. <aop:aspectid="TestAspect"ref="aspectBean">
  13. <!--配置com.spring.service包下所有类或接口的所有方法-->
  14. <aop:pointcutid="businessService"
  15. expression="execution(*com.spring.service.*.*(..))"/>
  16. <aop:beforepointcut-ref="businessService"method="doBefore"/>
  17. <aop:afterpointcut-ref="businessService"method="doAfter"/>
  18. <aop:aroundpointcut-ref="businessService"method="doAround"/>
  19. <aop:after-throwingpointcut-ref="businessService"method="doThrowing"throwing="ex"/>
  20. </aop:aspect>
  21. </aop:config>
  22. <beanid="aspectBean"class="com.spring.aop.TestAspect"/>
  23. <beanid="aService"class="com.spring.service.AServiceImpl"></bean>
  24. <beanid="bService"class="com.spring.service.BServiceImpl"></bean>
  25. </beans>

<?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:aop="http://www.springframework.org/schema/aop"

xsi:schemaLocation="

http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"

default-autowire="autodetect">

<aop:config>

<aop:aspect id="TestAspect" ref="aspectBean">

<!--配置com.spring.service包下所有类或接口的所有方法-->

<aop:pointcut id="businessService"

expression="execution(* com.spring.service.*.*(..))" />

<aop:before pointcut-ref="businessService" method="doBefore"/>

<aop:after pointcut-ref="businessService" method="doAfter"/>

<aop:around pointcut-ref="businessService" method="doAround"/>

<aop:after-throwing pointcut-ref="businessService" method="doThrowing" throwing="ex"/>

</aop:aspect>

</aop:config>

<bean id="aspectBean" class="com.spring.aop.TestAspect" />

<bean id="aService" class="com.spring.service.AServiceImpl"></bean>

<bean id="bService" class="com.spring.service.BServiceImpl"></bean>

</beans>

测试类AOPTest

Java代码

  1. publicclassAOPTestextendsAbstractDependencyInjectionSpringContextTests{
  2. privateAServiceaService;
  3. privateBServiceImplbService;
  4. protectedString[]getConfigLocations(){
  5. String[]configs=newString[]{"/applicationContext.xml"};
  6. returnconfigs;
  7. }
  8. /**
  9. *测试正常调用
  10. */
  11. publicvoidtestCall()
  12. {
  13. System.out.println("SpringTestJUnittest");
  14. aService.fooA("JUnittestfooA");
  15. aService.barA();
  16. bService.fooB();
  17. bService.barB("JUnittestbarB",0);
  18. }
  19. /**
  20. *测试After-Throwing
  21. */
  22. publicvoidtestThrow()
  23. {
  24. try{
  25. bService.barB("JUnitcallbarB",1);
  26. }catch(IllegalArgumentExceptione){
  27. }
  28. }
  29. publicvoidsetAService(AServiceservice){
  30. aService=service;
  31. }
  32. publicvoidsetBService(BServiceImplservice){
  33. bService=service;
  34. }
  35. }

public class AOPTest extends AbstractDependencyInjectionSpringContextTests {

private AService aService;

private BServiceImpl bService;

protected String[] getConfigLocations() {

String[] configs = new String[] { "/applicationContext.xml"};

return configs;

}

/**

* 测试正常调用

*/

public void testCall()

{

System.out.println("SpringTest JUnit test");

aService.fooA("JUnit test fooA");

aService.barA();

bService.fooB();

bService.barB("JUnit test barB",0);

}

/**

* 测试After-Throwing

*/

public void testThrow()

{

try {

bService.barB("JUnit call barB",1);

} catch (IllegalArgumentException e) {

}

}

public void setAService(AService service) {

aService = service;

}

public void setBService(BServiceImpl service) {

bService = service;

}

}

运行结果如下:

Java代码

  1. logBeginingmethod:com.spring.service.AServiceImpl.fooA
  2. AServiceImpl.fooA(msg:JUnittestfooA)
  3. logEndingmethod:com.spring.service.AServiceImpl.fooA
  4. processtime:0ms
  5. logBeginingmethod:com.spring.service.AServiceImpl.barA
  6. AServiceImpl.barA()
  7. logEndingmethod:com.spring.service.AServiceImpl.barA
  8. processtime:0ms
  9. logBeginingmethod:com.spring.service.BServiceImpl.fooB
  10. BServiceImpl.fooB()
  11. logEndingmethod:com.spring.service.BServiceImpl.fooB
  12. processtime:0ms
  13. logBeginingmethod:com.spring.service.BServiceImpl.barB
  14. BServiceImpl.barB(msg:JUnittestbarBtype:0)
  15. logEndingmethod:com.spring.service.BServiceImpl.barB
  16. processtime:0ms
  17. logBeginingmethod:com.spring.service.BServiceImpl.barB
  18. BServiceImpl.barB(msg:JUnitcallbarBtype:1)
  19. logEndingmethod:com.spring.service.BServiceImpl.barB
  20. methodcom.spring.service.BServiceImpl.barBthrowexception
  21. 测试异常

log Begining method: com.spring.service.AServiceImpl.fooA

AServiceImpl.fooA(msg:JUnit test fooA)

log Ending method: com.spring.service.AServiceImpl.fooA

process time: 0 ms

log Begining method: com.spring.service.AServiceImpl.barA

AServiceImpl.barA()

log Ending method: com.spring.service.AServiceImpl.barA

process time: 0 ms

log Begining method: com.spring.service.BServiceImpl.fooB

BServiceImpl.fooB()

log Ending method: com.spring.service.BServiceImpl.fooB

process time: 0 ms

log Begining method: com.spring.service.BServiceImpl.barB

BServiceImpl.barB(msg:JUnit test barB type:0)

log Ending method: com.spring.service.BServiceImpl.barB

process time: 0 ms

log Begining method: com.spring.service.BServiceImpl.barB

BServiceImpl.barB(msg:JUnit call barB type:1)

log Ending method: com.spring.service.BServiceImpl.barB

method com.spring.service.BServiceImpl.barB throw exception

测试异常

《Spring参考手册》中定义了以下几个AOP的重要概念,结合以上代码分析如下:

  • 切面(Aspect) :官方的抽象定义为“一个关注点的模块化,这个关注点可能会横切多个对象”,在本例中,“切面”就是类TestAspect所关注的具体行为,例如,AServiceImpl.barA()的调用就是切面TestAspect所关注的行为之一。“切面”在ApplicationContext中<aop:aspect>来配置。
  • 连接点(Joinpoint) :程序执行过程中的某一行为,例如,AServiceImpl.barA()的调用或者BServiceImpl.barB(String _msg, int _type)抛出异常等行为。
  • 通知(Advice) :“切面”对于某个“连接点”所产生的动作,例如,TestAspect中对com.spring.service包下所有类的方法进行日志记录的动作就是一个Advice。其中,一个“切面”可以包含多个“Advice”,例如TestAspect
  • 切入点(Pointcut) :匹配连接点的断言,在AOP中通知和一个切入点表达式关联。例如,TestAspect中的所有通知所关注的连接点,都由切入点表达式execution(* com.spring.service.*.*(..))来决定
  • 目标对象(Target Object) :被一个或者多个切面所通知的对象。例如,AServcieImpl和BServiceImpl,当然在实际运行时,Spring AOP采用代理实现,实际AOP操作的是TargetObject的代理对象。
  • AOP代理(AOP Proxy) 在Spring AOP中有两种代理方式,JDK动态代理和CGLIB代理。默认情况下,TargetObject实现了接口时,则采用JDK动态代理,例如,AServiceImpl;反之,采用CGLIB代理,例如,BServiceImpl。强制使用CGLIB代理需要将 <aop:config> 的 proxy-target-class 属性设为true

通知(Advice)类型

  • 前置通知(Before advice) :在某连接点(JoinPoint)之前执行的通知,但这个通知不能阻止连接点前的执行。ApplicationContext中在<aop:aspect>里面使用<aop:before>元素进行声明。例如,TestAspect中的doBefore方法
  • 后通知(After advice) :当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。ApplicationContext中在<aop:aspect>里面使用<aop:after>元素进行声明。例如,TestAspect中的doAfter方法,所以AOPTest中调用BServiceImpl.barB抛出异常时,doAfter方法仍然执行
  • 返回后通知(After return advice) :在某连接点正常完成后执行的通知,不包括抛出异常的情况。ApplicationContext中在<aop:aspect>里面使用<after-returning>元素进行声明。
  • 环绕通知(Around advice) :包围一个连接点的通知,类似Web中Servlet规范中的Filter的doFilter方法。可以在方法的调用前后完成自定义的行为,也可以选择不执行。ApplicationContext中在<aop:aspect>里面使用<aop:around>元素进行声明。例如,TestAspect中的doAround方法。
  • 抛出异常后通知(After throwing advice) : 在方法抛出异常退出时执行的通知。 ApplicationContext中在<aop:aspect>里面使用<aop:after-throwing>元素进行声明。例如,TestAspect中的doThrowing方法。

切入点表达式

  • 通常情况下,表达式中使用”execution“就可以满足大部分的要求。表达式格式如下:

Java代码

  1. execution(modifiers-pattern?ret-type-patterndeclaring-type-pattern?name-pattern(param-pattern)throws-pattern?)

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

modifiers-pattern:方法的操作权限

ret-type-pattern:返回值

declaring-type-pattern:方法所在的包

name-pattern:方法名

parm-pattern:参数名

throws-pattern:异常

其中,除ret-type-pattern和name-pattern之外,其他都是可选的。上例中,execution(* com.spring.service.*.*(..))表示com.spring.service包下,返回值为任意类型;方法名任意;参数不作限制的所有方法。

  • 通知参数

可以通过args来绑定参数,这样就可以在通知(Advice)中访问具体参数了。例如,<aop:aspect>配置如下

Java代码

  1. <aop:config>
  2. <aop:aspectid="TestAspect"ref="aspectBean">
  3. <aop:pointcutid="businessService"
  4. expression="execution(*com.spring.service.*.*(String,..))andargs(msg,..)"/>
  5. <aop:afterpointcut-ref="businessService"method="doAfter"/>
  6. </aop:aspect>
  7. </aop:config>

<aop:config>

<aop:aspect id="TestAspect" ref="aspectBean">

<aop:pointcut id="businessService"

expression="execution(* com.spring.service.*.*(String,..)) and args(msg,..)" />

<aop:after pointcut-ref="businessService" method="doAfter"/>

</aop:aspect>

</aop:config>

TestAspect的doAfter方法中就可以访问msg参数,但这样以来AService中的barA()和BServiceImpl中的barB()就不再是连接点,因为execution(* com.spring.service.*.*(String,..))只配置第一个参数为String类型的方法。其中,doAfter方法定义如下:

Java代码

  1. publicvoiddoAfter(JoinPointjp,Stringmsg)

public void doAfter(JoinPoint jp,String msg)

  • 访问当前的连接点

任何通知(Advice)方法可以将第一个参数定义为 org.aspectj.lang.JoinPoint 类型。JoinPoint 接口提供了一系列有用的方法,比如 getArgs() (返回方法参数)、getThis() (返回代理对象)、getTarget() (返回目标)、getSignature() (返回正在被通知的方法相关信息)和 toString() (打印出正在被通知

好长时间没有用过Spring了. 突然拿起书.我都发现自己对AOP都不熟悉了.
其实AOP的意思就是面向切面编程.
OO注重的是我们解决问题的方法(封装成Method),而AOP注重的是许多解决解决问题的方法中的共同点,是对OO思想的一种补充!
还是拿人家经常举的一个例子讲解一下吧:
比如说,我们现在要开发的一个应用里面有很多的业务方法,但是,我们现在要对这个方法的执行做全面监控,或部分监控.也许我们就会在要一些方法前去加上一条日志记录,
我们写个例子看看我们最简单的解决方案
我们先写一个接口IHello.java代码如下:

1 packagesinosoft.dj.aop.staticaop;
2
3 publicinterfaceIHello {
4 /***//**
5 *假设这是一个业务方法
6 *@paramname
7 */
8 voidsayHello(Stringname);
9 }
10


里面有个方法,用于输入"Hello" 加传进来的姓名;我们去写个类实现IHello接口

packagesinosoft.dj.aop.staticaop;

publicclassHelloimplementsIHello {

publicvoidsayHello(Stringname) {
System.out.println("Hello"+name);
}

}


现在我们要为这个业务方法加上日志记录的业务,我们在不改变原代码的情况下,我们会去怎么做呢?也许,你会去写一个类去实现IHello接口,并依赖Hello这个类.代码如下:

1 packagesinosoft.dj.aop.staticaop;
2
3 publicclassHelloProxyimplementsIHello {
4 privateIHellohello;
5
6 publicHelloProxy(IHellohello) {
7 this.hello=hello;
8 }
9
10 publicvoidsayHello(Stringname) {
11 Logger.logging(Level.DEBUGE,"sayHellomethodstart .");
12 hello.sayHello(name);
13 Logger.logging(Level.INFO,"sayHellomethodend!");
14
15 }
16
17 }
18


其中.Logger类和Level枚举代码如下:
Logger.java

1 packagesinosoft.dj.aop.staticaop;
2
3 importjava.util.Date;
4
5 publicclassLogger {
6 /***//**
7 *根据等级记录日志
8 *@paramlevel
9 *@paramcontext
10 */
11 publicstaticvoidlogging(Levellevel,Stringcontext) {
12 if(level.equals(Level.INFO)) {
13 System.out.println(newDate().toLocaleString()+""+context);
14 }
15 if(level.equals(Level.DEBUGE)) {
16 System.err.println(newDate()+""+context);
17 }
18 }
19
20 }
21

Level.java

1 packagesinosoft.dj.aop.staticaop;
2
3 publicenumLevel {
4 INFO,DEBUGE;
5 }
6

那我们去写个测试类看看,代码如下:
Test.java

1 packagesinosoft.dj.aop.staticaop;
2
3 publicclassTest {
4 publicstaticvoidmain(String[]args) {
5 IHellohello=newHelloProxy(newHello());
6 hello.sayHello("Doublej");
7 }
8 }
9

运行以上代码我们可以得到下面结果:

TueMar0420:57:12CST2008sayHellomethodstart .
HelloDoublej
2008-3-420:57:12sayHellomethodend!


从上面的代码我们可以看出,hello对象是被HelloProxy这个所谓的代理态所创建的.这样,如果我们以后要把日志记录的功能去掉.那我们只要把得到hello对象的代码改成以下:

1 packagesinosoft.dj.aop.staticaop;
2
3 publicclassTest {
4 publicstaticvoidmain(String[]args) {
5 IHellohello=newHello();
6 hello.sayHello("Doublej");
7 }
8 }
9


上面代码,可以说是AOP最简单的实现!
但是我们会发现一个问题,如果我们像Hello这样的类很多,那么,我们是不是要去写很多个HelloProxy这样的类呢.没错,是的.其实也是一种很麻烦的事.在jdk1.3以后.jdk跟我们提供了一个API java.lang.reflect.InvocationHandler的类. 这个类可以让我们在JVM调用某个类的方法时动态的为些方法做些什么事.让我们把以上的代码改一下来看看效果.
同样,我们写一个IHello的接口和一个Hello的实现类.在接口中.我们定义两个方法;代码如下 :

IHello.java

1 packagesinosoft.dj.aop.proxyaop;
2
3 publicinterfaceIHello {
4 /***//**
5 *业务处理A方法
6 *@paramname
7 */
8 voidsayHello(Stringname);
9 /***//**
10 *业务处理B方法
11 *@paramname
12 */
13 voidsayGoogBye(Stringname);
14 }
15



Hello.java

1 packagesinosoft.dj.aop.proxyaop;
2
3 publicclassHelloimplementsIHello {
4
5 publicvoidsayHello(Stringname) {
6 System.out.println("Hello"+name);
7 }
8 publicvoidsayGoogBye(Stringname) {
9 System.out.println(name+"GoodBye!");
10 }
11 }
12


我们一样的去写一个代理类.只不过.让这个类去实现java.lang.reflect.InvocationHandler接口,代码如下:

1 packagesinosoft.dj.aop.proxyaop;
2
3 importjava.lang.reflect.InvocationHandler;
4 importjava.lang.reflect.Method;
5 importjava.lang.reflect.Proxy;
6
7 publicclassDynaProxyHelloimplementsInvocationHandler {
8
9 /***//**
10 *要处理的对象(也就是我们要在方法的前后加上业务逻辑的对象,如例子中的Hello)
11 */
12 privateObjectdelegate;
13
14 /***//**
15 *动态生成方法被处理过后的对象(写法固定)
16 *
17 *@paramdelegate
18 *@paramproxy
19 *@return
20 */
21 publicObjectbind(Objectdelegate) {
22 this.delegate=delegate;
23 returnProxy.newProxyInstance(
24 this.delegate.getClass().getClassLoader(),this.delegate
25 .getClass().getInterfaces(),this);
26 }
27 /***//**
28 *要处理的对象中的每个方法会被此方法送去JVM调用,也就是说,要处理的对象的方法只能通过此方法调用
29 *此方法是动态的,不是手动调用的
30 */
31 publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)
32 throwsThrowable {
33 Objectresult=null;
34 try {
35 //执行原来的方法之前记录日志
36 Logger.logging(Level.DEBUGE,method.getName()+"Methodend .");
37
38 //JVM通过这条语句执行原来的方法(反射机制)
39 result=method.invoke(this.delegate,args);
40 //执行原来的方法之后记录日志
41 Logger.logging(Level.INFO,method.getName()+"MethodStart!");
42 }catch(Exceptione) {
43 e.printStackTrace();
44 }
45 //返回方法返回值给调用者
46 returnresult;
47 }
48
49 }
50


上面类中出现的Logger类和Level枚举还是和上一上例子的实现是一样的.这里就不贴出代码了.

让我们写一个Test类去测试一下.代码如下:
Test.java

1 packagesinosoft.dj.aop.proxyaop;
2
3 publicclassTest {
4 publicstaticvoidmain(String[]args) {
5 IHellohello=(IHello)newDynaProxyHello().bind(newHello());
6 hello.sayGoogBye("DoubleJ");
7 hello.sayHello("DoubleJ");
8
9 }
10 }
11


运行输出的结果如下:

TueMar0421:24:03CST2008sayGoogByeMethodend .
DoubleJGoodBye!
2008-3-421:24:03sayGoogByeMethodStart!
TueMar0421:24:03CST2008sayHelloMethodend .
HelloDoubleJ
2008-3-421:24:03sayHelloMethodStart!


由于线程的关系,第二个方法的开始出现在第一个方法的结束之前.这不是我们所关注的!
从上面的例子我们看出.只要你是采用面向接口编程,那么,你的任何对象的方法执行之前要加上记录日志的操作都是可以的.他(DynaPoxyHello)自动去代理执行被代理对象(Hello)中的每一个方法,一个java.lang.reflect.InvocationHandler接口就把我们的代理对象和被代理对象解藕了.但是,我们又发现还有一个问题,这个DynaPoxyHello对象只能跟我们去在方法前后加上日志记录的操作.我们能不能把DynaPoxyHello对象和日志操作对象(Logger)解藕呢?
结果是肯定的.让我们来分析一下我们的需求.
我们要在被代理对象的方法前面或者后面去加上日志操作代码(或者是其它操作的代码),
那么,我们可以抽象出一个接口,这个接口里就只有两个方法,一个是在被代理对象要执行方法之前执行的方法,我们取名为start,第二个方法就是在被代理对象执行方法之后执行的方法,我们取名为end .接口定义如下 :

1 packagesinosoft.dj.aop.proxyaop;
2
3 importjava.lang.reflect.Method;
4
5 publicinterfaceIOperation {
6 /***//**
7 *方法执行之前的操作
8 *@parammethod
9 */
10 voidstart(Methodmethod);
11 /***//**
12 *方法执行之后的操作
13 *@parammethod
14 */
15 voidend(Methodmethod);
16 }
17


我们去写一个实现上面接口的类.我们把作他真正的操作者,如下面是日志操作者的一个类:
LoggerOperation.java

packagesinosoft.dj.aop.proxyaop;

importjava.lang.reflect.Method;

publicclassLoggerOperationimplementsIOperation {

publicvoidend(Methodmethod) {
Logger.logging(Level.DEBUGE,method.getName()+"Methodend .");
}

publicvoidstart(Methodmethod) {
Logger.logging(Level.INFO,method.getName()+"MethodStart!");
}

}


然后我们要改一下代理对象DynaProxyHello中的代码.如下:

1 packagesinosoft.dj.aop.proxyaop;
2
3 importjava.lang.reflect.InvocationHandler;
4 importjava.lang.reflect.Method;
5 importjava.lang.reflect.Proxy;
6
7 publicclassDynaProxyHelloimplementsInvocationHandler {
8 /***//**
9 *操作者
10 */
11 privateObjectproxy;
12 /***//**
13 *要处理的对象(也就是我们要在方法的前后加上业务逻辑的对象,如例子中的Hello)
14 */
15 privateObjectdelegate;
16
17 /***//**
18 *动态生成方法被处理过后的对象(写法固定)
19 *
20 *@paramdelegate
21 *@paramproxy
22 *@return
23 */
24 publicObjectbind(Objectdelegate,Objectproxy) {
25
26 this.proxy=proxy;
27 this.delegate=delegate;
28 returnProxy.newProxyInstance(
29 this.delegate.getClass().getClassLoader(),this.delegate
30 .getClass().getInterfaces(),this);
31 }
32 /***//**
33 *要处理的对象中的每个方法会被此方法送去JVM调用,也就是说,要处理的对象的方法只能通过此方法调用
34 *此方法是动态的,不是手动调用的
35 */
36 publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)
37 throwsThrowable {
38 Objectresult=null;
39 try {
40 //反射得到操作者的实例
41 Classclazz=this.proxy.getClass();
42 //反射得到操作者的Start方法
43 Methodstart=clazz.getDeclaredMethod("start",
44 newClass[] {Method.class});
45 //反射执行start方法
46 start.invoke(this.proxy,newObject[] {method});
47 //执行要处理对象的原本方法
48 result=method.invoke(this.delegate,args);
49 //反射得到操作者的end方法
50 Methodend=clazz.getDeclaredMethod("end",
51 newClass[] {Method.class});
52 //反射执行end方法
53 end.invoke(this.proxy,newObject[] {method});
54
55 }catch(Exceptione) {
56 e.printStackTrace();
57 }
58 returnresult;
59 }
60
61 }
62


然后我们把Test.java中的代码改一下.测试一下:

packagesinosoft.dj.aop.proxyaop;

publicclassTest {
publicstaticvoidmain(String[]args) {
IHellohello=(IHello)newDynaProxyHello().bind(newHello(),newLoggerOperation());
hello.sayGoogBye("DoubleJ");
hello.sayHello("DoubleJ");

}
}

结果还是一样的吧.

如果你想在每个方法之前加上日志记录,而不在方法后加上日志记录.你就把LoggerOperation类改成如下:

1 packagesinosoft.dj.aop.proxyaop;
2
3 importjava.lang.reflect.Method;
4
5 publicclassLoggerOperationimplementsIOperation {
6
7 publicvoidend(Methodmethod) {
8 //Logger.logging(Level.DEBUGE,method.getName()+"Methodend .");
9 }
10
11 publicvoidstart(Methodmethod) {
12 Logger.logging(Level.INFO,method.getName()+"MethodStart!");
13 }
14
15 }
16


运行一下.你就会发现,每个方法之后没有记录日志了. 这样,我们就把代理者和操作者解藕了!

下面留一个问题给大家,如果我们不想让所有方法都被日志记录,我们应该怎么去解藕呢.?
我的想法是在代理对象的public Object invoke(Object proxy, Method method, Object[] args)方法里面加上个if(),对传进来的method的名字进行判断,判断的条件存在XML里面.这样我们就可以配置文件时行解藕了.如果有兴趣的朋友可以把操作者,被代理者,都通过配置文件进行配置 ,那么就可以写一个简单的SpringAOP框架了.

面向切面编程AOP)提供另外一种角度来思考程序结构,通过这种方式弥补了面向对象编程(OOP)的不足。 除了类(classes)以外,AOP提供了 切面。切面对关注点进行模块化,例如横切多个类型和对象的事务管理。 (这些关注点术语通常称作 横切(crosscutting) 关注点。)

Spring的一个关键的组件就是 AOP框架。 尽管如此,Spring IoC容器并不依赖于AOP,这意味着你可以自由选择是否使用AOP,AOP提供强大的中间件解决方案,这使得Spring IoC容器更加完善。

Spring 2.0 AOP

Spring 2.0 引入了一种更加简单并且更强大的方式来自定义切面,用户可以选择使用基于模式(schema-based)的方式或者使用@AspectJ注解。 这两种风格都完全支持通知(Advice)类型和AspectJ的切入点语言,虽然实际上仍然使用Spring AOP进行织入(Weaving)。

本章主要讨论Spring 2.0对基于模式和基于@AspectJ的AOP支持。请查阅"AOP声明风格的选择"一节获取为你的应用选择适当的声明风格的建议。Spring 2.0完全保留了对Spring 1.2的向下兼容性,下一章 将讨论 Spring 1.2 API所提供的底层的AOP支持。

Spring中所使用的AOP:

· 提供声明式企业服务,特别是为了替代EJB声明式服务。 最重要的服务是 声明性事务管理(declarative transaction management) ,这个服务建立在Spring的抽象事务管理(transaction abstraction)之上。

· 允许用户实现自定义的切面,用AOP来完善OOP的使用。

这样你可以把Spring AOP看作是对Spring的一种增强,它使得Spring可以不需要EJB就能提供声明式事务管理;或者也可以使用Spring AOP框架的全部功能来实现自定义的切面。

本章首先 介绍了AOP的概念,无论你打算采用哪种风格的切面声明,这个部分都值得你一读。本章剩下的部分将着重于Spring 2.0对AOP的支持; 下一章 提供了关于Spring 1.2风格的AOP概述,也许你已经在其他书本,文章以及已有的应用程序中碰到过这种AOP风格。

如果你只打算使用通用的声明式服务或者预先打包的声明式中间件服务,例如缓冲池(pooling),那么你不必直接使用Spring AOP,而本章的大部分内容也可以直接跳过。

6.1.1.AOP概念

首先让我们从定义一些重要的AOP概念开始。这些术语不是Spring特有的。 不幸的是,AOP术语并不是特别的直观;如果Spring使用自己的术语,将会变得更加令人困惑。

· 切面(Aspect): 一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。 在Spring AOP中,切面可以使用通用类(基于模式的风格)或者在普通类中以 @Aspect 注解(@AspectJ风格)来实现。

· 连接点(Joinpoint): 在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点 总是 代表一个方法的执行。 通过声明一个org.aspectj.lang.JoinPoint类型的参数可以使通知(Advice)的主体部分获得连接点信息。

· 通知(Advice): 在切面的某个特定的连接点(Joinpoint)上执行的动作。通知有各种类型,其中包括“around”、“before”和“after”等通知。 通知的类型将在后面部分进行讨论。许多AOP框架,包括Spring,都是以拦截器做通知模型, 并维护一个以连接点为中心的拦截器链。

· 切入点(Pointcut): 匹配连接点(Joinpoint)的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。 切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。

· 引入(Introduction): (也被称为内部类型声明(inter-type declaration))。声明额外的方法或者某个类型的字段。 Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用一个引入来使bean实现 IsModified 接口,以便简化缓存机制。

· 目标对象(Target Object): 被一个或者多个切面(aspect)所通知(advise)的对象。也有人把它叫做 被通知(advised) 对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个 被代理(proxied) 对象。

· AOP代理(AOP Proxy): AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能)。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。 注意:Spring 2.0最新引入的基于模式(schema-based)风格和@AspectJ注解风格的切面声明,对于使用这些风格的用户来说,代理的创建是透明的。

· 织入(Weaving): 把切面(aspect)连接到其它的应用程序类型或者对象上,并创建一个被通知(advised)的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。 Spring和其他纯Java AOP框架一样,在运行时完成织入。

通知的类型:

· 前置通知(Before advice): 在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。

· 返回后通知(After returning advice): 在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。

· 抛出异常后通知(After throwing advice): 在方法抛出异常退出时执行的通知。

· 后通知(After (finally) advice): 当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。

· 环绕通知(Around Advice): 包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。

环绕通知是最常用的一种通知类型。大部分基于拦截的AOP框架,例如Nanning和JBoss4,都只提供环绕通知。

跟AspectJ一样,Spring提供所有类型的通知,我们推荐你使用尽量简单的通知类型来实现需要的功能。例如,如果你只是需要用一个方法的返回值来更新缓存,虽然使用环绕通知也能完成同样的事情, 但是你最好使用After returning通知而不是环绕通知。 用最合适的通知类型可以使得编程模型变得简单,并且能够避免很多潜在的错误。 比如,你不需要调用 JoinPoint(用于Around Advice)的 proceed() 方法,就不会有调用的问题。

在Spring 2.0中,所有的通知参数都是静态类型,因此你可以使用合适的类型(例如一个方法执行后的返回值类型)作为通知的参数而不是使用一个对象数组。

切入点(pointcut)和连接点(join point)匹配的概念是AOP的关键,这使得AOP不同于其它仅仅提供拦截功能的旧技术。切入点使得定位通知(advice)可独立于OO层次。 例如,一个提供声明式事务管理的around通知可以被应用到一组横跨多个对象中的方法上(例如服务层的所有业务操作)。

6.1.2.Spring AOP的功能和目标

Spring AOP用纯Java实现。它不需要专门的编译过程。Spring AOP不需要控制类装载器层次,因此它适用于J2EE web容器或应用服务器。

Spring目前仅支持使用方法调用作为连接点(join point)(在Spring bean上通知方法的执行)。 虽然可以在不影响到Spring AOP核心API的情况下加入对成员变量拦截器支持,但Spring并没有实现成员变量拦截器。 如果你需要把对成员变量的访问和更新也作为通知的连接点,可以考虑其它语法的Java语言,例如AspectJ。

Spring实现AOP的方法跟其他的框架不同。Spring并不是要尝试提供最完整的AOP实现(尽管Spring AOP有这个能力), 相反的,它其实侧重于提供一种AOP实现和Spring IoC容器的整合,用于帮助解决在企业级开发中的常见问题。

因此,Spring AOP通常都和Spring IoC容器一起使用。 Aspect使用普通的bean定义语法(尽管Spring提供了强大的“自动代理(autoproxying)”功能): 与其他AOP实现相比这是一个显著的区别。有些事使用Spring AOP是无法轻松或者高效的完成的,比如说通知一个细粒度的对象。 这种时候,使用AspectJ是最好的选择。不过经验告诉我们:于大多数在J2EE应用中遇到的问题,只要适合AOP来解决的,Spring AOP都没有问题,Spring AOP提供了一个非常好的解决方案。

Spring AOP从来没有打算通过提供一种全面的AOP解决方案来取代AspectJ。我们相信无论是基于代理(proxy-based )的框架比如说Spring亦或是full-blown的框架比如说是AspectJ都是很有价值的,他们之间的关系应该是互补而不是竞争的关系。 Spring 2.0可以无缝的整合Spring AOP,IoC 和AspectJ,使得所有的AOP应用完全融入基于Spring的应用体系。 这样的集成不会影响Spring AOP API或者AOP Alliance API;Spring AOP保留了向下兼容性。接下来的一章会详细讨论Spring AOP API。

6.1.3.Spring的AOP代理

Spring缺省使用J2SE 动态代理(dynamic proxies)来作为AOP的代理。这样任何接口都可以被代理。

Spring也支持使用CGLIB代理. 对于需要代理类而不是代理接口的时候CGLIB代理是很有必要的。如果一个业务对象并没有实现一个接口,默认就会使用CGLIB。 作为面向接口编程的最佳实践,业务对象通常都会实现一个或多个接口。但也有可能会 强制使用CGLIB, 在这种情况(希望不常有)下,你可能需要通知一个没有在接口中声明的方法,或者需要传入一个代理对象给方法作为具体类型

在Spring 2.0之后,Spring可能会提供多种其他类型的AOP代理,包括了完整的生成类。这不会影响到编程模型。

6.2.@AspectJ支持

"@AspectJ"使用了Java 5的注解,可以将切面声明为普通的Java类。 AspectJ 5发布的 AspectJ project 中引入了这种@AspectJ风格。 Spring 2.0 使用了和AspectJ 5一样的注解,使用了AspectJ 提供的一个库来做切点(pointcut)解析和匹配。但是,AOP在运行时仍旧是纯的Spring AOP,并不依赖于AspectJ 的编译器或者织入器(weaver)。

使用AspectJ的编译器或者织入器(weaver)的话就可以使用完整的AspectJ 语言,我们将在 Section6.8, “在Spring应用中使用AspectJ” 中讨论这个问题。

6.2.1.启用@AspectJ支持

为了在Spring配置中使用@AspectJ aspects,你必须首先启用Spring对基于@AspectJ aspects的配置支持,自动代理(autoproxying)基于通知是否来自这些切面。自动代理是指Spring会判断一个bean是否使用了一个或多个切面通知,并据此自动生成相应的代理以拦截其方法调用,并且确认通知是否如期进行。

通过在你的Spring的配置中引入下列元素来启用Spring对@AspectJ的支持:

<aop:aspectj-autoproxy/>

我们假使你正在使用 AppendixA, XML Schema-based configuration 所描述的schema支持。 关于如何在aop的命名空间中引入这些标签,请参见 SectionA.2.6, “The aop schema”

如果你正在使用DTD,你仍旧可以通过在你的application context中添加如下定义来启用@AspectJ支持:

<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" />

你需要在你的应用程序的classpath中引入两个AspectJ库:aspectjweaver.jar 和 aspectjrt.jar。 这些库可以在AspectJ的安装包(1.5.1或者之后的版本)中的 lib 目录里找到,或者也可以在Spring依赖库的 lib/aspectj 目录下找到。

6.2.2.声明一个切面

在启用@AspectJ支持的情况下,在application context中定义的任意带有一个@Aspect切面(拥有@Aspect注解)的bean都将被Spring自动识别并用于配置在Spring AOP。 以下例子展示了为了完成一个不是非常有用的切面所需要的最小定义:

下面是在application context中的一个常见的bean定义,这个bean指向一个使用了 @Aspect 注解的bean类:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
 <!-- configure properties of aspect here as normal -->
</bean>

下面是 NotVeryUsefulAspect 类定义,使用了 org.aspectj.lang.annotation.Aspect 注解。

package org.xyz;
import org.aspectj.lang.annotation.Aspect;
 
@Aspect
public class NotVeryUsefulAspect {
 
}

切面(用 @Aspect 注解的类)和其他类一样有方法和字段定义。他们也可能包括切入点,通知和引入(inter-type)声明。

6.2.3.声明一个切入点(pointcut)

回想一下,切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行。 Spring AOP 只支持 Spring bean 方法执行连接点。所以你可以把切入点看做是匹配 Spring bean 上方法的执行。 一个切入点声明有两个部分:一个包含名字和任意参数的签名,还有一个切入点表达式,该表达式决定了我们关注那个方法的执行。在 @AspectJ 注解风格的 AOP 中,一个切入点签名通过一个普通的方法定义来提供,并且切入点表达式使用 @Pointcut 注解来表示(作为切入点签名的方法必须返回 void 类型)。

用一个例子会帮助我们区分切入点签名和切入点表达式之间的差别,下面的例子定义了一个切入点'anyOldTransfer', 这个切入点将匹配任何名为 "transfer" 的方法的执行:

@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

切入点表达式,也就是 @Pointcut 注解的值,是正规的AspectJ 5切入点表达式。 如果你想要更多了解AspectJ的 切入点语言,请参见 AspectJ 编程指南(如果要了解基于Java 5的扩展请参阅 AspectJ 5 开发手册)或者其他人写的关于AspectJ的书,例如Colyer et. al.著的《Eclipse AspectJ》或者Ramnivas Laddad著的《AspectJ in Action》。

6.2.3.1.切入点指定者的支持

Spring AOP 支持在切入点表达式中使用如下的AspectJ切入点指定者:

其他的切入点类型

完整的AspectJ切入点语言支持额外的切入点指定者,但是Spring不支持这个功能。 他们分别是call, initialization, preinitialization, staticinitialization, get, set, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this 和 @withincode。 在Spring AOP中使用这些指定者将会导致抛出IllegalArgumentException异常。

Spring AOP支持的切入点指定者可能在将来的版本中得到扩展,不但支持更多的AspectJ 切入点指定者(例如"if"),还会支持某些Spring特有的切入点指定者,比如"bean"(用于匹配bean的名字)。

· execution - 匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指定者。

· within - 限定匹配特定类型的连接点(在使用Spring AOP的时候,在匹配的类型中定义的方法的执行)。

· this - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。

· target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中目标对象(被代理的appolication object)是指定类型的实例。

· args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。

· @target- 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中执行的对象的类已经有指定类型的注解。

· @args- 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中实际传入参数的运行时类型有指定类型的注解。

· @within- 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。

· @annotation - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中连接点的主题有某种给定的注解。

因为Spring AOP限制了连接点必须是方法执行级别的,pointcut designators的讨论也给出了一个定义,这个定义和AspectJ的编程指南中的定义相比显得更加狭窄。除此之外,AspectJ它本身有基于类型的语义,在执行的连接点'this'和'target'都是指同一个对象,也就是执行方法的对象。 Spring AOP是一个基于代理的系统,并且严格区分代理对象本身(对应于'this')和背后的目标对象(对应于'target')

6.2.3.2.合并切入点表达式

切入点表达式可以使用using '&', '||' 和 '!'来合并.还可以通过名字来指向切入点表达式。 以下的例子展示了三种切入点表达式: anyPublicOperation(在一个方法执行连接点代表了任意public方法的执行时匹配); inTrading(在一个代表了在交易模块中的任意的方法执行时匹配) 和 tradingOperation(在一个代表了在交易模块中的任意的公共方法执行时匹配)。

 @Pointcut("execution(public * *(..))")
 private void anyPublicOperation() {}
 
 @Pointcut("within(com.xyz.someapp.trading..*")
 private void inTrading() {}
 
 @Pointcut("anyPublicOperation() && inTrading()")
 private void tradingOperation() {}

就上所示的,从更小的命名组件来构建更加复杂的切入点表达式是一种最佳实践。 当用名字来指定切入点时使用的是常见的Java成员可视性访问规则。 (比如说,你可以在同一类型中访问私有的切入点,在继承关系中访问受保护的切入点,可以在任意地方访问公共切入点。成员可视性访问规则不影响到切入点的 匹配

6.2.3.3.共享常见的切入点(pointcut)定义

当开发企业级应用的时候,你通常会想要从几个切面来参考模块化的应用和特定操作的集合。 我们推荐定义一个“SystemArchitecture”切面来捕捉常见的切入点表达式。一个典型的切面可能看起来像下面这样:

package com.xyz.someapp;
 
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
 
@Aspect
public class SystemArchitecture {
 
 /**
 * A join point is in the web layer if the method is defined
 * in a type in the com.xyz.someapp.web package or any sub-package
 * under that.
 */
 @Pointcut("within(com.xyz.someapp.web..*)")
 public void inWebLayer() {}
 
 /**
 * A join point is in the service layer if the method is defined
 * in a type in the com.xyz.someapp.service package or any sub-package
 * under that.
 */
 @Pointcut("within(com.xyz.someapp.service..*)")
 public void inServiceLayer() {}
 
 /**
 * A join point is in the data access layer if the method is defined
 * in a type in the com.xyz.someapp.dao package or any sub-package
 * under that.
 */
 @Pointcut("within(com.xyz.someapp.dao..*)")
 public void inDataAccessLayer() {}
 
 /**
 * A business service is the execution of any method defined on a service
 * interface. This definition assumes that interfaces are placed in the
 * "service" package, and that implementation types are in sub-packages.
 * 
* If you group service interfaces by functional area (for example, 
* in packages com.xyz.someapp.abc.service and com.xyz.def.service) then
 * the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
 * could be used instead.
 */
 @Pointcut("execution(* com.xyz.someapp.service.*.*(..))")
 public void businessService() {}
 
/**
 * A data access operation is the execution of any method defined on a 
* dao interface. This definition assumes that interfaces are placed in the
 * "dao" package, and that implementation types are in sub-packages.
 */
 @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
 public void dataAccessOperation() {}
 
}

示例中的切入点定义了一个你可以在任何需要切入点表达式的地方可引用的切面。比如,为了使service层事务化,你可以写成:

<aop:config>
 <aop:advisor
  pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
  advice-ref="tx-advice"/>
</aop:config>
 
<tx:advice id="tx-advice">
<tx:attributes>
 <tx:method name="*" propagation="REQUIRED"/>
 </tx:attributes>
</tx:advice>

Section6.3, “Schema-based AOP support” 中讨论 <aop:config> 和 <aop:advisor>标签。 在 Chapter9, 事务管理 中讨论事务标签。

6.2.3.4.示例

Spring AOP 用户可能会经常使用 execution pointcut designator。执行表达式的格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

除了返回类型模式(上面代码片断中的ret-type-pattern),名字模式和参数模式以外,所有的部分都是可选的。返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 你会使用的最频繁的返回类型模式是 *,它代表了匹配任意的返回类型。 一个全称限定的类型名将只会匹配返回给定类型的方法。名字模式匹配的是方法名。你可以使用 * 通配符作为所有或者部分命名模式。 参数模式稍微有点复杂:() 匹配了一个不接受任何参数的方法,而 (..) 匹配了一个接受任意数量参数的方法(零或者更多)。 模式 (*) 匹配了一个接受一个任何类型的参数的方法。 模式 (*,String) 匹配了一个接受两个参数的方法,第一个可以是任意类型,第二个则必须是String类型。 请参见AspectJ编程指南的 Language Semantics 部分。

下面给出一些常见切入点表达式的例子。

· 任意公共方法的执行:

execution(public * *(..))

· 任何一个以“set”开始的方法的执行:

execution(* set*(..))

· AccountService 接口的任意方法的执行:

execution(* com.xyz.service.AccountService.*(..))

· 定义在service包里的任意方法的执行:

execution(* com.xyz.service.*.*(..))

· 定义在service包或者子包里的任意方法的执行:

execution(* com.xyz.service..*.*(..))

· 在service包里的任意连接点(在Spring AOP中只是方法执行) :

within(com.xyz.service.*)

· 在service包或者子包里的任意连接点(在Spring AOP中只是方法执行) :

within(com.xyz.service..*)

· 实现了 AccountService 接口的代理对象的任意连接点(在Spring AOP中只是方法执行) :

this(com.xyz.service.AccountService)

'this'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得代理对象可以在通知体内访问到的部分。

· 实现了 AccountService 接口的目标对象的任意连接点(在Spring AOP中只是方法执行) :

target(com.xyz.service.AccountService)

'target'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得目标对象可以在通知体内访问到的部分。

· 任何一个只接受一个参数,且在运行时传入的参数实现了 Serializable 接口的连接点 (在Spring AOP中只是方法执行)

args(java.io.Serializable)

'args'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得方法参数可以在通知体内访问到的部分。

请注意在例子中给出的切入点不同于 execution(* *(java.io.Serializable)): args只有在动态运行时候传入参数是可序列化的(Serializable)才匹配,而execution 在传入参数的签名声明的类型实现了 Serializable 接口时候匹配。

· 有一个 @Transactional 注解的目标对象中的任意连接点(在Spring AOP中只是方法执行)

@target(org.springframework.transaction.annotation.Transactional)

'@target' 也可以在binding form中使用:请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

· 任何一个目标对象声明的类型有一个 @Transactional 注解的连接点(在Spring AOP中只是方法执行)

@within(org.springframework.transaction.annotation.Transactional)

'@within'也可以在binding form中使用:- 请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

· 任何一个执行的方法有一个 @Transactional annotation的连接点(在Spring AOP中只是方法执行)

@annotation(org.springframework.transaction.annotation.Transactional)

'@annotation' 也可以在binding form中使用:- 请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

· 任何一个接受一个参数,并且传入的参数在运行时的类型实现了 @Classified annotation的连接点(在Spring AOP中只是方法执行)

@args(com.xyz.security.Classified)

'@args'也可以在binding form中使用:- 请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

6.2.4.声明通知

通知是跟一个切入点表达式关联起来的,并且在切入点匹配的方法执行之前或者之后或者之前和之后运行。 切入点表达式可能是指向已命名的切入点的简单引用或者是一个已经声明过的切入点表达式。

6.2.4.1.前置通知(Before advice)

一个切面里使用 @Before 注解声明前置通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
 
@Aspect
public class BeforeExample {
 
 @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
 public void doAccessCheck() {
 // ...
 }
 
}

如果使用一个in-place 的切入点表达式,我们可以把上面的例子换个写法:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
 
@Aspect
public class BeforeExample {
 
 @Before("execution(* com.xyz.myapp.dao.*.*(..))")
 public void doAccessCheck() {
 // ...
 }
 
}
6.2.4.2.返回后通知(After returning advice)

返回后通知通常在一个匹配的方法返回的时候执行。使用 @AfterReturning 注解来声明:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
 
@Aspect
public class AfterReturningExample {
 
 @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
 public void doAccessCheck() {
 // ...
 }
 
}

说明:你可以在同一个切面里定义多个通知,或者其他成员。我们只是在展示如何定义一个简单的通知。这些例子主要的侧重点是正在讨论的问题。

有时候你需要在通知体内得到返回的值。你可以使用以 @AfterReturning 接口的形式来绑定返回值:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
 
@Aspect
public class AfterReturningExample {
 
 @AfterReturning(
 pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
 returning="retVal")
 public void doAccessCheck(Object retVal) {
 // ...
 }
 
}

在 returning 属性中使用的名字必须对应于通知方法内的一个参数名。 当一个方法执行返回后,返回值作为相应的参数值传入通知方法。一个 returning 子句也限制了只能匹配到返回指定类型值的方法。 (在本例子中,返回值是 Object 类,也就是说返回任意类型都会匹配)

6.2.4.3.抛出后通知(After throwing advice)

抛出后通知在一个方法抛出异常后执行。使用 @AfterThrowing 注解来声明:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
 
@Aspect
public class AfterThrowingExample {
 
 @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
 public void doRecoveryActions() {
 // ...
 }
 
}

你通常会想要限制通知只在某种特殊的异常被抛出的时候匹配,你还希望可以在通知体内得到被抛出的异常。 使用 throwing 属性不光可以限制匹配的异常类型(如果你不想限制,请使用 Throwable 作为异常类型),还可以将抛出的异常绑定到通知的一个参数上。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
 
@Aspect
public class AfterThrowingExample {
 
 @AfterThrowing(
 pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
 throwing="ex")
 public void doRecoveryActions(DataAccessException ex) {
 // ...
 }
 
}

在 throwing 属性中使用的名字必须与通知方法内的一个参数对应。 当一个方法因抛出一个异常而中止后,这个异常将会作为那个对应的参数送至通知方法。 throwing 子句也限制了只能匹配到抛出指定异常类型的方法(上面的示例为 DataAccessException)。

6.2.4.4.后通知(After (finally) advice)

不论一个方法是如何结束的,在它结束后(finally)后通知(After (finally) advice)都会运行。 使用 @After 注解来声明。这个通知必须做好处理正常返回和异常返回两种情况。通常用来释放资源。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
 
@Aspect
public class AfterFinallyExample {
 
 @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
 public void doReleaseLock() {
 // ...
 }
 
}
6.2.4.5.环绕通知(Around Advice)

最后一种通知是环绕通知。环绕通知在一个方法执行之前和之后执行。 它使得通知有机会既在一个方法执行之前又在执行之后运行。并且,它可以决定这个方法在什么时候执行,如何执行,甚至是否执行。环绕通知经常在在某线程安全的环境下,你需要在一个方法执行之前和之后共享某种状态的时候使用。 请尽量使用最简单的满足你需求的通知。(比如如果前置通知(before advice)也可以适用的情况下不要使用环绕通知)。

环绕通知使用 @Around 注解来声明。通知的第一个参数必须是 ProceedingJoinPoint 类型。 在通知体内,调用 ProceedingJoinPoint 的 proceed() 方法将会导致潜在的连接点方法执行。 proceed 方法也可能会被调用并且传入一个 Object[] 对象-该数组将作为方法执行时候的参数。

当传入一个Object[]对象的时候,处理的方法与通过AspectJ编译器处理环绕通知略有不同。对于使用传统AspectJ语言写的环绕通知来说,传入参数的数量必须和传递给环绕通知的参数数量匹配(不是后台的连接点接受的参数数量),并且特定顺序的传入参数代替了将要绑定给连接点的原始值(如果你看不懂不用担心)。 Spring采用的方法更加简单并且更好得和他的基于代理(proxy-based),只匹配执行的语法相适用。如果你适用AspectJ的编译器和编织器来编译为Spring而写的@AspectJ切面和处理参数,你只需要了解这一区别即可。有一种方法可以让你写出100%兼容Spring AOP和AspectJ的,我们将会在后续的通知参数(advice parameters)的章节中讨论它。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
 
@Aspect
public class AroundExample {
 
 @Around("com.xyz.myapp.SystemArchitecture.businessService()")
 public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
 // start stopwatch
 Object retVal = pjp.proceed();
 // stop stopwatch
 return retVal;
 }
 
}

方法的调用者得到的返回值就是环绕通知返回的值。 例如:一个简单的缓存切面,如果缓存中有值,就返回该值,否则调用proceed()方法。 请注意proceed可能在通知体内部被调用一次,许多次,或者根本不被调用。

6.2.4.6.通知参数(Advice parameters)

Spring 2.0 提供了完整的通知类型 - 这意味着你可以在通知签名中声明所需的参数,(就像在以前的例子中我们看到的返回值和抛出异常一样)而不总是使用Object[]。 我们将会看到如何在通知体内访问参数和其他上下文相关的值。首先让我们看以下如何编写普通的通知以找出正在被通知的方法。

6.2.4.6.1.访问当前的连接点

任何通知方法可以将第一个参数定义为 org.aspectj.lang.JoinPoint 类型 (环绕通知需要定义为 ProceedingJoinPoint 类型的, 它是 JoinPoint 的一个子类。) JoinPoint 接口提供了一系列有用的方法, 比如 getArgs()(返回方法参数)、getThis()(返回代理对象)、getTarget()(返回目标)、getSignature()(返回正在被通知的方法相关信息)和 toString()(打印出正在被通知的方法的有用信息)。详细的内容请参考Javadocs。

6.2.4.6.2.传递参数给通知(Advice)

我们已经看到了如何绑定返回值或者异常(使用后置通知(after returning)和异常后通知(after throwing advice)。 为了可以在通知(adivce)体内访问参数,你可以使用 args 来绑定。 如果在一个参数表达式中应该使用类型名字的地方使用一个参数名字,那么当通知执行的时候对应的参数值将会被传递进来。可能给出一个例子会更好理解。假使你想要通知(advise)接受某个Account对象作为第一个参数的DAO操作的执行,你想要在通知体内也能访问到account对象,你可以写如下的代码:

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" + 
"args(account,..)")
public void validateAccount(Account account) {
 // ...
}

切入点表达式的 args(account,..) 部分有两个目的:首先它保证了只会匹配那些接受至少一个参数的方法的执行,而且传入的参数必须是 Account 类型的实例, 其次它使得可以在通知体内通过 account 参数来访问那个account参数。

另外一个办法是定义一个切入点,这个切入点在匹配某个连接点的时候“提供”了一个Account对象, 然后直接从通知中访问那个命名的切入点。你可以这样写:

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" + 
"args(account,..)")
private void accountDataAccessOperation(Account account) {}
 
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
 // ...
}

如果有兴趣了解更详细的内容,请参阅 AspectJ 编程指南。

代理对象(this)、目标对象(target) 和注解(@within, @target, @annotation, @args)都可以用一种简单格式绑定。 以下的例子展示了如何使用 @Auditable 注解来匹配方法执行,并提取AuditCode。

首先是 @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();
 // ...
}
6.2.4.6.3.决定参数名

绑定在通知上的参数依赖切入点表达式的匹配名,并借此在(通知(advice)和切入点(pointcut))的方法签名中声明参数名。 参数名 无法 通过Java反射来获取,所以Spring AOP使用如下的策略来决定参数名字:

· 如果参数名字已经被用户明确指定,则使用指定的参数名:通知(advice)和切入点(pointcut)注解有一个额外的"argNames"属性,该属性用来指定所注解的方法的参数名 - 这些参数名在运行时是 可以 访问的。例子如下:

· @Before(
· value="com.xyz.lib.Pointcuts.anyPublicMethod() && " + 
· "@annotation(auditable)",
· argNames="auditable")
· public void audit(Auditable auditable) {
· AuditCode code = auditable.value();
· // ...
}

如果一个@AspectJ切面已经被AspectJ编译器(ajc)编译过了,那么就不需要再添加argNames参数了,因为编译器会自动完成这一工作。

· 使用 'argNames' 属性有点不那么优雅,所以如果没有指定'argNames' 属性, Spring AOP 会寻找类的debug信息,并且尝试从本地变量表(local variable table)中来决定参数名字。只要编译的时候使用了debug信息(至少要使用 '-g:vars' ),就可获得这些信息。 使用这个flag编译的结果是: (1)你的代码将能够更加容易的读懂(反向工程), (2)生成的class文件会稍许大一些(通常是不重要的), (3)移除不被使用的本地变量的优化功能将会失效。 换句话说,你在使用这个flag的时候不会遇到任何困难。

· 如果不加上debug信息来编译的话,Spring AOP将会尝试推断参数的绑定。 (例如,要是只有一个变量被绑定到切入点表达式(pointcut expression)、通知方法(advice method)将会接受这个参数,这是显而易见的)。 如果变量的绑定不明确,将会抛出一个 AmbiguousBindingException 异常。

· 如果以上所有策略都失败了,将会抛出一个 IllegalArgumentException 异常。

6.2.4.6.4.处理参数

我们之前提过我们将会讨论如何编写一个 带参数的 的proceed()调用,使得不论在Spring AOP中还是在AspectJ都能正常工作。 解决方法是保证通知签名依次绑定方法参数。比如说:

@Around("execution(List<Account> find*(..)) &&" +
 "com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
 "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp, String accountHolderNamePattern)
throws Throwable {
 String newPattern = preProcess(accountHolderNamePattern);
 return pjp.proceed(new Object[] {newPattern});
}

大多数情况下你都会这样绑定(就像上面的例子那样)。

6.2.4.7.通知(Advice)顺序

如果有多个通知想要在同一连接点运行会发生什么?Spring AOP 的执行通知的顺序跟AspectJ的一样。 在“进入”连接点的情况下,最高优先级的通知会先执行(所以上面给出的两个前置通知(before advice)中,优先级高的那个会先执行)。 在“退出”连接点的情况下,最高优先级的通知会最后执行。(所以上面给出的两个前置通知(before advice)中,优先级高的那个会第二个执行)。 对于定义在相同切面的通知,根据声明的顺序来确定执行顺序。比如下面这个切面:

@Aspect
public class AspectWithMultipleAdviceDeclarations {
 
 @Pointcut("execution(* foo(..))")
 public void fooExecution() {}
 
@Before("fooExecution()")
 public void doBeforeOne() {
 // ...
 }
 
@Before("fooExecution()")
 public void doBeforeTwo() {
 // ...
 }
 
@AfterReturning("fooExecution()")
 public void doAfterOne() {
 // ...
 }
 
 @AfterReturning("fooExecution()")
 public void doAfterTwo() {
 // ...
 }
 
}

这样,假使对于任何一个名字为foo的方法的执行, doBeforeOne、doBeforeTwo、doAfterOne 和 doAfterTwo 通知方法都需要运行。 执行顺序将按照声明的顺序来确定。在这个例子中,执行的结果会是:

doBeforeOne
doBeforeTwo
foo
doAfterOne
doAfterTwo

换言之,因为doBeforeOne先定义,它会先于doBeforeTwo执行,而doAfterTwo后于doAfterOne定义,所以它会在doAfterOne之后执行。 只需要记住通知是按照定义的顺序来执行的就可以了。 - 如果想要知道更加详细的内容,请参阅AspectJ编程指南。

当定义在 不同的 切面里的两个通知都需要在一个相同的连接点中运行,那么除非你指定,否则执行的顺序是未知的。你可以通过指定优先级来控制执行顺序。在Spring中可以在切面类中实现 org.springframework.core.Ordered 接口做到这一点。 在两个切面中,Ordered.getValue() 方法返回值较低的那个有更高的优先级。

6.2.5.引入(Introductions)

引入(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.SystemArchitecture.businessService() &&" +
  "this(usageTracked)")
 public void recordUsage(UsageTracked usageTracked) {
 usageTracked.incrementUseCount();
 }
 
}

实现的接口通过被注解的字段类型来决定。@DeclareParents 注解的 value 属性是一个AspectJ的类型模式:- 任何匹配类型的bean都会实现 UsageTracked 接口。 请注意,在上面的前置通知(before advice)的例子中,service beans 可以直接用作 UsageTracked 接口的实现。 如果需要编程式的来访问一个bean,你可以这样写:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

6.2.6.切面实例化模型

这是一个高级主题...

默认情况下,在application context中每一个切面都会有一个实例。 AspectJ 把这个叫做单个实例化模型(singleton instantiation model)。 也可以用其他的生命周期来定义切面:- Spring支持AspectJ的 perthis 和 pertarget 实例化模型 (现在还不支持percflow、percflowbelow 和 pertypewithin )。

一个"perthis" 切面的定义:在 @Aspect 注解中指定perthis 子句。 让我们先来看一个例子,然后解释它是如何运作的:

@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {
 
 private int someState;
 
 @Before(com.xyz.myapp.SystemArchitecture.businessService())
 public void recordServiceUsage() {
 // ...
 }
 
}

这个perthis子句的效果是每个独立的service对象执行时都会创建一个切面实例(切入点表达式所匹配的连接点上的每一个独立的对象都会绑定到'this'上)。 service对象的每个方法在第一次执行的时候创建切面实例。切面在service对象失效的同时失效。 在切面实例被创建前,所有的通知都不会被执行,一旦切面对象创建完成,定义的通知将会在匹配的连接点上执行,但是只有当service对象是和切面关联的才可以。 如果想要知道更多关于per-clauses的信息,请参阅 AspectJ 编程指南。

'pertarget'实例模型的跟“perthis”完全一样,只不过是为每个匹配于连接点的独立目标对象创建一个切面实例。

6.2.7.例子

现在你已经看到了每个独立的部分是如何运作的了,是时候把他们放到一起做一些有用的事情了!

因为并发的问题,有时候business services可能会失败(例如,死锁失败)。如果重新尝试一下,很有可能就会成功。对于business services来说,重试几次是很正常的(Idempotent操作不需要用户参与,否则会得出矛盾的结论)我们可能需要透明的重试操作以避免让客户看见 PessimisticLockingFailureException 例外被抛出。 很明显,在一个横切多层的情况下,这是非常有必要的,因此通过切面来实现是很理想的。

因为我们想要重试操作,我们会需要使用到环绕通知,这样我们就可以多次调用proceed()方法。下面是简单的切面实现:

@Aspect
public class ConcurrentOperationExecutor implements Ordered {
 
private static final int DEFAULT_MAX_RETRIES = 2;
 
 private int maxRetries = DEFAULT_MAX_RETRIES;
 private int order = 1;
 
 public void setMaxRetries(int maxRetries) {
 this.maxRetries = maxRetries;
 }
 
public int getOrder() {
 return this.order;
 }
 
public void setOrder(int order) {
 this.order = order;
 }
 
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
 public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { 
int numAttempts = 0;
 PessimisticLockingFailureException lockFailureException;
 do {
 numAttempts++;
 try { 
return pjp.proceed();
 }
 catch(PessimisticLockingFailureException ex) {
 lockFailureException = ex;
 }
 }
 while(numAttempts <= this.maxRetries);
 throw lockFailureException;
 }
 
}

请注意切面实现了 Ordered 接口,这样我们就可以把切面的优先级设定为高于事务通知(我们每次重试的时候都想要在一个全新的事务中进行)。 maxRetries 和 order 属性都可以在Spring中配置。 主要的动作在 doConcurrentOperation 这个环绕通知中发生。 请注意这个时候我们所有的 businessService() 方法都会使用这个重试策略。 我们首先会尝试处理,然后如果我们得到一个 PessimisticLockingFailureException 意外,我们只需要简单的重试,直到我们耗尽所有预设的重试次数。

对应的Spring配置如下:

<aop:aspectj-autoproxy/>
 
<bean id="concurrentOperationExecutor"
 class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
 <property name="maxRetries" value="3"/>
 <property name="order" value="100"/> 
</bean>

为了改进切面,使之仅仅重试idempotent操作,我们可以定义一个 Idempotent 注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
 // marker annotation
}

并且对service操作的实现进行注解。 这样如果你只希望改变切面使得idempotent的操作会尝试多次,你只需要改写切入点表达式,这样只有 @Idempotent 操作会匹配:

@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
 "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { 
...
}

6.3.Schema-based AOP support

如果你无法使用Java 5,或者你比较喜欢使用XML格式,Spring2.0也提供了使用新的"aop"命名空间来定义一个切面。和使用@AspectJ风格完全一样,切入点表达式和通知类型同样得到了支持,因此在这一节中我们将着重介绍新的 语法 和回顾前面我们所讨论的如何写一个切入点表达式和通知参数的绑定(Section6.2, “@AspectJ支持”)。

使用本章所介绍的aop命名空间标签(aop namespace tag),你需要引入AppendixA, XML Schema-based configuration中提及的spring-aop schema。参见SectionA.2.6, “The aop schema”

在Spring的配置文件中,所有的切面和通知器都必须定义在 <aop:config> 元素内部。 一个application context可以包含多个 <aop:config>。 一个 <aop:config> 可以包含pointcut,advisor和aspect元素(注意它们必须按照这样的顺序进行声明)。

Warning

<aop:config>风格的配置使得对Spring auto-proxying 机制的使用变得很笨重。如果你已经通过BeanNameAutoProxyCreator或类似的东西使用显式的auto-proxying将会引发问题 (例如通知没有被织入)。推荐的使用模式是只使用<aop:config>风格或只使用 AutoProxyCreator风格

6.3.1.声明一个切面

有了schema的支持,切面就和常规的Java对象一样被定义成application context中的一个bean。 对象的字段和方法提供了状态和行为信息,XML文件则提供了切入点和通知信息。

切面使用<aop:aspect>来声明,backing bean(支持bean)通过 ref 属性来引用:

<aop:config>
 <aop:aspect id="myAspect" ref="aBean">
 ...
 </aop:aspect>
</aop:config>
 
<bean id="aBean" class="...">
 ...
</bean>

切面的支持bean(上例中的"aBean")可以象其他Spring bean一样被容器管理配置以及依赖注入。

6.3.2.声明一个切入点

切入点可以在切面里面声明,这种情况下切入点只在切面内部可见。切入点也可以直接在<aop:config>下定义,这样就可以使多个切面和通知器共享该切入点。

一个描述service层中表示所有service执行的切入点可以如下定义:

<aop:config>
 
 <aop:pointcut id="businessService"
 expression="execution(* com.xyz.myapp.service.*.*(..))"/>
 
</aop:config>

注意切入点表达式本身使用了 Section6.2, “@AspectJ支持” 中描述的AspectJ 切入点表达式语言。如果你在Java 5环境下使用基于schema的声明风格,可参考切入点表达式类型中定义的命名式切入点,不过这在JDK1.4及以下版本中是不被支持的(因为依赖于Java 5中的AspectJ反射API)。 所以在JDK 1.5中,上面的切入点的另外一种定义形式如下:

<aop:config>
 
 <aop:pointcut id="businessService"
 expression="com.xyz.myapp.SystemArchitecture.businessService()"/>
 
</aop:config>

假定你有 Section6.2.3.3, “共享常见的切入点(pointcut)定义”中说描述的 SystemArchitecture 切面。

在切面里面声明一个切入点和声明一个顶级的切入点非常类似:

<aop:config>
 
 <aop:aspect id="myAspect" ref="aBean">
 <aop:pointcut id="businessService"
  expression="execution(* com.xyz.myapp.service.*.*(..))"/>
 ...
 </aop:aspect>
</aop:config>

当需要连接子表达式的时候,'&'在XML中用起来非常不方便,所以关键字'and', 'or' 和 'not'可以分别用来代替'&', '||' 和 '!'。

注意这种方式定义的切入点通过XML id来查找,并且不能定义切入点参数。在基于schema的定义风格中命名切入点支持较之@AspectJ风格受到了很多的限制。

6.3.3.声明通知

和@AspectJ风格一样,基于schema的风格也支持5种通知类型并且两者具有同样的语义。

6.3.3.1.通知(Advice)

Before通知在匹配方法执行前进入。在<aop:aspect>里面使用<aop:before>元素进行声明。

<aop:aspect id="beforeExample" ref="aBean">
 
 <aop:before
  pointcut-ref="dataAccessOperation"
  method="doAccessCheck"/>
 ...
</aop:aspect>

这里 dataAccessOperation 是一个顶级(<aop:config>)切入点的id。 要定义内置切入点,可将 pointcut-ref 属性替换为 pointcut 属性:

<aop:aspect id="beforeExample" ref="aBean">
 
 <aop:before
  pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
  method="doAccessCheck"/>
 ...
</aop:aspect>

我们已经在@AspectJ风格章节中讨论过了,使用命名切入点能够明显的提高代码的可读性。

Method属性标识了提供了通知的主体的方法(doAccessCheck)。这个方法必须定义在包含通知的切面元素所引用的bean中。在一个数据访问操作执行之前(执行连接点和切入点表达式匹配),切面中的"doAccessCheck"会被调用。

6.3.3.2.返回后通知(After returning advice)

After returning通知在匹配的方法完全执行后运行。和Before通知一样,可以在<aop:aspect>里面声明。例如:

<aop:aspect id="afterReturningExample" ref="aBean">
 
 <aop:after-returning
  pointcut-ref="dataAccessOperation"
  method="doAccessCheck"/>
 
 ...
 
</aop:aspect>

和@AspectJ风格一样,通知主体可以接收返回值。使用returning属性来指定接收返回值的参数名:

<aop:aspect id="afterReturningExample" ref="aBean">
 
 <aop:after-returning
  pointcut-ref="dataAccessOperation"
  returning="retVal"
  method="doAccessCheck"/>
 
 ...
 
</aop:aspect>

doAccessCheck方法必须声明一个名字叫 retVal 的参数。 参数的类型强制匹配,和先前我们在@AfterReturning中讲到的一样。例如,方法签名可以这样声明:

public void doAccessCheck(Object retVal) {...
6.3.3.3.抛出异常后通知(After throwing advice)

After throwing通知在匹配方法抛出异常退出时执行。在 <aop:aspect> 中使用after-throwing元素来声明:

<aop:aspect id="afterThrowingExample" ref="aBean">
 
 <aop:after-throwing
  pointcut-ref="dataAccessOperation"
  method="doRecoveryActions"/>
 
 ...
 
</aop:aspect>

和@AspectJ风格一样,可以从通知体中获取抛出的异常。使用throwing属性来指定异常的名称,用这个名称来获取异常:

<aop:aspect id="afterThrowingExample" ref="aBean">
 
 <aop:after-throwing
  pointcut-ref="dataAccessOperation"
  thowing="dataAccessEx"
  method="doRecoveryActions"/>
 
 ...
 
</aop:aspect>

doRecoveryActions方法必须声明一个名字为 dataAccessEx 的参数。 参数的类型强制匹配,和先前我们在@AfterThrowing中讲到的一样。例如:方法签名可以如下这般声明:

public void doRecoveryActions(DataAccessException dataAccessEx) {...
6.3.3.4.后通知(After (finally) advice)

After (finally)通知在匹配方法退出后执行。使用 after 元素来声明:

<aop:aspect id="afterFinallyExample" ref="aBean">
 
 <aop:after
  pointcut-ref="dataAccessOperation"
  method="doReleaseLock"/>
 
 ...
 
</aop:aspect>
6.3.3.5.通知

Around通知是最后一种通知类型。Around通知在匹配方法运行期的“周围”执行。它有机会在目标方法的前面和后面执行,并决定什么时候运行,怎么运行,甚至是否运行。 Around通知经常在需要在一个方法执行前或后共享状态信息,并且是线程安全的情况下使用(启动和停止一个计时器就是一个例子)。注意选择能满足你需求的最简单的通知类型(i.e.如果简单的before通知就能做的事情绝对不要使用around通知)。

Around通知使用 aop:around 元素来声明。 通知方法的第一个参数的类型必须是 ProceedingJoinPoint 类型。 在通知的主体中,调用 ProceedingJoinPoint的proceed() 方法来执行真正的方法。 proceed 方法也可能会被调用并且传入一个 Object[] 对象 - 该数组将作为方法执行时候的参数。参见 Section6.2.4.5, “环绕通知(Around Advice)” 中提到的一些注意点。

<aop:aspect id="aroundExample" ref="aBean">
 
 <aop:around
  pointcut-ref="businessService"
  method="doBasicProfiling"/>
 
 ...
 
</aop:aspect>

doBasicProfiling 通知的实现和@AspectJ中的例子完全一样(当然要去掉注解):

public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
 // start stopwatch
 Object retVal = pjp.proceed();
 // stop stopwatch
 return retVal;
}
6.3.3.6.通知参数

Schema-based声明风格和@AspectJ支持一样,支持通知的全名形式 - 通过通知方法参数名字来匹配切入点参数。参见 Section6.2.4.6, “通知参数(Advice parameters)” 获取详细信息。

如果你希望显式指定通知方法的参数名(而不是依靠先前提及的侦测策略),可以通过 arg-names 属性来实现。示例如下:

<aop:before
 pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
 method="audit"
 arg-names="auditable"/>

The arg-names attribute accepts a comma-delimited list of parameter names.

arg-names属性接受由逗号分割的参数名列表。

请看下面这个基于XSD风格的更复杂一些的实例,它展示了关联多个强类型参数的环绕通知的使用。

首先,服务接口及它的实现将被通知:

package x.y.service;
 
public interface FooService {
 
 Foo getFoo(String fooName, int age);
}
 
// the attendant implementation (defined in another file of course)
 
public class DefaultFooService implements FooService {
 
 public Foo getFoo(String name, int age) {
 return new Foo(name, age);
 }
}

下一步(无可否认的)是切面。注意实际上profile(..)方法接受多个强类型(strongly-typed)参数,第一个参数是方法调用时要执行的连接点,该参数指明了 profile(..)方法被用作一个环绕通知:

package x.y;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
 
public class SimpleProfiler {
 
 public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
 StopWatch clock = new StopWatch(
 "Profiling for '" + name + "' and '" + age + "'");
 try {
 clock.start(call.toShortString());
 return call.proceed();
 } finally {
 clock.stop();
 System.out.println(clock.prettyPrint());
 }
 }
}

最后,下面是为一个特定的连接点执行上面的通知所必需的XML配置:

<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:aop="http://www.springframework.org/schema/aop"
 xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
 
 <!-- this is the object that will be proxied by Spring's AOP infrastructure -->
 <bean id="fooService" class="x.y.service.DefaultFooService"/>
 
 <!-- this is the actual advice itself -->
 <bean id="profiler" class="x.y.SimpleProfiler"/>
 
 <aop:config>
 <aop:aspect ref="profiler">
 
 <aop:pointcut id="theExecutionOfSomeFooServiceMethod"
 expression="execution(* x.y.service.FooService.getFoo(String,int))
 and args(name, age)"/>
 
 <aop:around pointcut-ref="theExecutionOfSomeFooServiceMethod"
 method="profile"/>
 
 </aop:aspect>
 </aop:config>
 
</beans>

如果使用下面的驱动脚本,我们将在标准输出上得到如下的输出:

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.FooService;
 
public final class Boot {
 
 public static void main(final String[] args) throws Exception {
 BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
 FooService foo = (FooService) ctx.getBean("fooService");
 foo.getFoo("Pengo", 12);
 }
}
StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms % Task name
-----------------------------------------
00000 ? execution(getFoo)
6.3.3.7.通知顺序

当同一个切入点(执行方法)上有多个通知需要执行时,执行顺序规则在 Section6.2.4.7, “通知(Advice)顺序” 已经提及了。 切面的优先级通过切面的支持bean是否实现了Ordered接口来决定。

6.3.4.引入

Intrduction (在AspectJ中成为inter-type声明)允许一个切面声明一个通知对象实现指定接口,并且提供了一个接口实现类来代表这些对象。

在 aop:aspect 内部使用 aop:declare-parents 元素定义Introduction。 该元素用于用来声明所匹配的类型有了一个新的父类型(所以有了这个名字)。 例如,给定接口 UsageTracked,以及这个接口的一个实现类 DefaultUsageTracked,下面声明的切面所有实现service接口的类同时实现 UsageTracked 接口。(比如为了通过JMX暴露statistics。)

<aop:aspect id="usageTrackerAspect" ref="usageTracking">
 
 <aop:declare-parents
  types-matching="com.xzy.myapp.service.*+",
  implement-interface="UsageTracked"
  default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
 
 <aop:before
 pointcut="com.xyz.myapp.SystemArchitecture.businessService()
  and this(usageTracked)"
 method="recordUsage"/>
 
</aop:aspect>

usageTracking bean的支持类可以包含下面的方法:

public void recordUsage(UsageTracked usageTracked) {
 usageTracked.incrementUseCount();
}

欲实现的接口由 implement-interface 属性来指定。 types-matching 属性的值是一个AspectJ类型模式:- 任何匹配类型的bean会实现 UsageTracked 接口。 注意在Before通知的例子中,srevice bean可以用作 UsageTracked 接口的实现。 如果编程形式访问一个bean,你可以这样来写:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

6.3.5.切面实例化模型

Schema-defined切面仅支持一种实例化模型就是singlton模型。其他的实例化模型或许在未来版本中将得到支持。

6.3.6.Advisors

"advisors"这个概念来自Spring1.2对AOP的支持,在AspectJ中是没有等价的概念。 advisor就像一个小的自包含的切面,这个切面只有一个通知。切面自身通过一个bean表示,并且必须实现一个通知接口, 在 Section7.3.2, “Spring里的通知类型” 中我们会讨论相应的接口。Advisors可以很好的利用AspectJ切入点表达式。

Spring 2.0 通过 <aop:advisor> 元素来支持advisor 概念。 你将会发现它大多数情况下会和transactional advice一起使用,transactional advice在Spring 2.0中有自己的命名空间。格式如下:

<aop:config>
 
 <aop:pointcut id="businessService"
 expression="execution(* com.xyz.myapp.service.*.*(..))"/>
 
 <aop:advisor
  pointcut-ref="businessService"
  advice-ref="tx-advice"/>
 
</aop:config>
 
<tx:advice id="tx-advice">
<tx:attributes>
 <tx:method name="*" propagation="REQUIRED"/>
 </tx:attributes>
</tx:advice>

和在上面使用的 pointcut-ref 属性一样,你还可以使用 pointcut 属性来定义一个内联的切入点表达式。

为了定义一个advisord的优先级以便让通知可以有序,使用 order 属性来定义 advisor的值 Ordered 。

6.3.7.例子

让我们来看看在 Section6.2.7, “例子” 提过并发锁失败重试的例子,如果使用schema对这个例子进行重写是什么效果。

因为并发锁的关系,有时候business services可能会失败(例如,死锁失败)。如果重新尝试一下,很有可能就会成功。对于business services来说,重试几次是很正常的(Idempotent操作不需要用户参与,否则会得出矛盾的结论) 我们可能需要透明的重试操作以避免让客户看见 PessimisticLockingFailureException 例外被抛出。很明显,在一个横切多层的情况下,这是非常有必要的,因此通过切面来实现是很理想的。

因为我们想要重试操作,我们会需要使用到环绕通知,这样我们就可以多次调用proceed()方法。下面是简单的切面实现(只是一个schema支持的普通Java 类):

public class ConcurrentOperationExecutor implements Ordered {
 
private static final int DEFAULT_MAX_RETRIES = 2;
 
 private int maxRetries = DEFAULT_MAX_RETRIES;
 private int order = 1;
 
 public void setMaxRetries(int maxRetries) {
 this.maxRetries = maxRetries;
 }
 
public int getOrder() {
 return this.order;
 }
 
public void setOrder(int order) {
 this.order = order;
 }
 
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { 
int numAttempts = 0;
 PessimisticLockingFailureException lockFailureException;
 do {
 numAttempts++;
 try { 
return pjp.proceed();
 }
 catch(PessimisticLockingFailureException ex) {
 lockFailureException = ex;
 }
 }
 while(numAttempts <= this.maxRetries);
 throw lockFailureException;
 }
 
}

请注意切面实现了 Ordered 接口,这样我们就可以把切面的优先级设定为高于事务通知(我们每次重试的时候都想要在一个全新的事务中进行)。 maxRetries 和 order 属性都可以在Spring中配置。 主要的动作在 doConcurrentOperation 这个环绕通知中发生。 请注意这个时候我们所有的 businessService() 方法都会使用这个重试策略。 我们首先会尝试处理,然后如果我们得到一个 PessimisticLockingFailureException 异常,我们只需要简单的重试,直到我们耗尽所有预设的重试次数。

这个类跟我们在@AspectJ的例子中使用的是相同的,只是没有使用注解。

对应的Spring配置如下:

<aop:config>
 
 <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
 
 <aop:pointcut id="idempotentOperation"
 expression="execution(* com.xyz.myapp.service.*.*(..))"/>
 
<aop:around
 pointcut-ref="idempotentOperation"
 method="doConcurrentOperation"/>
 
</aop:aspect>
 
</aop:config>
 
<bean id="concurrentOperationExecutor"
 class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
 <property name="maxRetries" value="3"/>
 <property name="order" value="100"/> 
</bean>

请注意我们现在假设所有的bussiness services都是idempotent。如果不是这样,我们可以改写切面,加上 Idempotent 注解,让它只调用idempotent:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
 // marker annotation
}

并且对service操作的实现进行注解。这样如果你只希望改变切面使得idempotent的操作会尝试多次,你只需要改写切入点表达式,这样只有 @Idempotent 操作会匹配:

 <aop:pointcut id="idempotentOperation"
 expression="execution(* com.xyz.myapp.service.*.*(..)) and
 @annotation(com.xyz.myapp.service.Idempotent)"/>

6.4.AOP声明风格的选择

当你确定切面是实现一个给定需求的最佳方法时,你如何选择是使用Spring AOP还是AspectJ,以及选择 Aspect语言(代码)风格、@AspectJ声明风格或XML风格?这个决定会受到多个因素的影响,包括应用的需求、 开发工具和小组对AOP的精通程度。

6.4.1.Spring AOP还是完全用AspectJ?

做能起作用的最简单的事。Spring AOP比完全使用AspectJ更加简单,因为它不需要引入AspectJ的编译器/织入器到你开发和构建过程中。如果你仅仅需要在Spring bean上通知执行操作,那么Spring AOP是合适的选择。如果你需要通知domain对象或其它没有在Spring容器中 管理的任意对象,那么你需要使用AspectJ。如果你想通知除了简单的方法执行之外的连接点(如:调用连接点、字段get或set的连接点等等), 也需要使用AspectJ。

当使用AspectJ时,你可以选择使用AspectJ语言(也称为“代码风格”)或@AspectJ注解风格。 如果切面在你的设计中扮演一个很大的角色,并且你能在Eclipse中使用AspectJ Development Tools (AJDT), 那么首选AspectJ语言 :- 因为该语言专门被设计用来编写切面,所以会更清晰、更简单。如果你没有使用 Eclipse,或者在你的应用中只有很少的切面并没有作为一个主要的角色,你或许应该考虑使用@AspectJ风格 并在你的IDE中附加一个普通的Java编辑器,并且在你的构建脚本中增加切面织入(链接)的段落。

6.4.2.Spring AOP中使用@AspectJ还是XML?

如果你选择使用Spring AOP,那么你可以选择@AspectJ或者XML风格。总的来说,如果你使用Java 5, 我们建议使用@AspectJ风格。显然如果你不是运行在Java 5上,XML风格是最佳选择。XML和@AspectJ 之间权衡的细节将在下面进行讨论。

XML风格对现有的Spring用户来说更加习惯。它可以使用在任何Java级别中(参考连接点表达式内部的命名连接点,虽然它也需要Java 5) 并且通过纯粹的POJO来支持。当使用AOP作为工具来配置企业服务时(一个好的例子是当你认为连接点表达式是你的配置中的一部分时,你可能想单独更改它)XML会是一个很好的选择。对于XML风格,从你的配置中可以清晰的表明在系统中存在那些切面。

XML风格有两个缺点。第一是它不能完全将需求实现的地方封装到一个位置。DRY原则中说系统中的每一项知识都必须具有单一、无歧义、权威的表示。 当使用XML风格时,如何实现一个需求的知识被分割到支撑类的声明中以及XML配置文件中。当使用@AspectJ风格时就只有一个单独的模块 -切面- 信息被封装了起来。 第二是XML风格同@AspectJ风格所能表达的内容相比有更多的限制:仅仅支持"singleton"切面实例模型,并且不能在XML中组合命名连接点的声明。 例如,在@AspectJ风格中我们可以编写如下的内容:

 @Pointcut(execution(* get*()))
 public void propertyAccess() {}
 
 @Pointcut(execution(org.xyz.Account+ *(..))
 public void operationReturningAnAccount() {}
 
 @Pointcut(propertyAccess() && operationReturningAnAccount())
 public void accountPropertyAccess() {}

在XML风格中能声明开头的两个连接点:

 <aop:pointcut id="propertyAccess" 
expression="execution(* get*())"/>
 
 <aop:pointcut id="operationReturningAnAccount" 
expression="execution(org.xyz.Account+ *(..))"/>

但是不能通过组合这些来定义accountPropertyAccess连接点

@AspectJ风格支持其它的实例模型以及更丰富的连接点组合。它具有将将切面保持为一个模块单元的优点。 还有一个优点就是@AspectJ切面能被Spring AOP和AspectJ两者都理解 - 所以如果稍后你认为你需要AspectJ 的能力去实现附加的需求,那么你非常容易转移到基于AspectJ的途径。总而言之,我们更喜欢@AspectJ风格只要你有切面 去做超出简单的“配置”企业服务之外的事情。

6.5.混合切面类型

我们完全可以混合使用以下几种风格的切面定义:使用自动代理的@AspectJ 风格的切面,schema-defined <aop:aspect> 的切面,和用 <aop:advisor> 声明的advisor,甚至是使用Spring 1.2风格的代理和拦截器。 由于以上几种风格的切面定义的都使用了相同的底层机制,因此可以很好的共存。

6.6.代理机制

Spring AOP部分使用JDK动态代理或者CGLIB来为目标对象创建代理。(建议尽量使用JDK的动态代理)

如果被代理的目标对象实现了至少一个接口,则会使用JDK动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB代理。

如果你希望强制使用CGLIB代理,(例如:希望代理目标对象的所有方法,而不只是实现自接口的方法)那也可以。但是需要考虑以下问题:

· 无法通知(advise)Final 方法,因为他们不能被覆写。

· 你需要将CGLIB 2二进制发行包放在classpath下面,与之相较JDK本身就提供了动态代理

强制使用CGLIB代理需要将 <aop:config> 的 proxy-target-class 属性设为true:

<aop:config proxy-target-class="true">
 
 ...
 
</aop:config>

当需要使用CGLIB代理和@AspectJ自动代理支持,请按照如下的方式设置 <aop:aspectj-autoproxy> 的 proxy-target-class 属性:

<aop:aspectj-autoproxy proxy-target-class="true"/>

6.7.编程方式创建@AspectJ代理

除了在配置文件中使用 <aop:config> 或者 <aop:aspectj-autoproxy> 来声明切面。 同样可以通过编程方式来创建代理通知(advise)目标对象。关于Spring AOP API的详细介绍,请参看下一章。这里我们重点介绍自动创建代理。

org.springframework.aop.aspectj.annotation.AspectJProxyFactory 可以为@AspectJ切面的目标对象创建一个代理。该类的基本用法非常简单,示例如下。请参看Javadoc获取更详细的信息。

// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject); 
 
// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);
 
// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker); 
 
// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();

6.8.在Spring应用中使用AspectJ

到目前为止本章讨论的一直是纯Spring AOP。 在这一节里面我们将介绍如何使用AspectJ compiler/weaver来代替Spring AOP或者作为它的补充,因为有些时候Spring AOP单独提供的功能也许并不能满足你的需要。

Spring提供了一个小巧的AspectJ aspect library (你可以在程序发行版本中单独使用 spring-aspects.jar 文件,并将其加入到classpath下以使用其中的切面)。 Section6.8.1, “在Spring中使用AspectJ来为domain object进行依赖注入” Section6.8.2, “Spring中其他的AspectJ切面” 讨论了该库和如何使用该库。 Section6.8.3, “使用Spring IoC来配置AspectJ的切面” 讨论了如何对通过AspectJ compiler织入的AspectJ切面进行依赖注入。最后Section6.8.4, “在Spring应用中使用AspectJ Load-time weaving(LTW)”介绍了使用AspectJ的Spring应用程序如何装载期织入(load-time weaving)。

6.8.1.在Spring中使用AspectJ来为domain object进行依赖注入

Spring容器对application context中定义的bean进行实例化和配置。 同样也可以通过bean factory来为一个已经存在且已经定义为spring bean的对象应用所包含的配置信息。 spring-aspects.jar中包含了一个annotation-driven的切面,提供了能为任何对象进行依赖注入的能力。这样的支持旨在为 脱离容器管理 创建的对象进行依赖注入。 Domain object经常处于这样的情形:它们可能是通过 new 操作符创建的对象, 也可能是ORM工具查询数据库的返回结果对象。

包 org.springframework.orm.hibernate.support 中的类 DependencyInjectionInterceptorFactoryBean 可以让Spring为Hibernate创建并且配置prototype类型的domain object(使用自动装配或者确切命名的bean原型定义)。 当然,拦截器不支持配置你编程方式创建的对象而非检索数据库返回的对象。 其他framework也会提供类似的技术。仍是那句话,Be Pragramatic选择能满足你需求的方法中最简单的那个。 请注意前面提及的类 没有 随Spring发行包一起发布。 如果你希望使用该类,需要从Spring CVS Respository上下载并且自行编译。 你可以在Spring CVS respository下的 'sandbox' 目录下找到该文件。

@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,这样就可以按类型或者按名字自动装配了。

最后,你可以设置 dependencyCheck 属性,通过设置,Spring对新创建和配置的对象的对象引用进行校验 (例如:@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true) )。 如果这个属性被设为true,Spring会在配置结束后校验除了primitives和collections类型的所有的属性是否都被赋值了。

仅仅使用注解并没有做任何事情。但当注解存在时,spring-aspects.jar中的 AnnotationBeanConfigurerAspect 就起作用了。 实质上切面做了这些:当初始化一个有 @Configurable 注解的新对象时,Spring按照注解中的属性来配置这个新创建的对象。 要实现上述的操作,已注解的类型必须由AspectJ weaver来织入 - 你可以使用一个 build-time ant/maven任务来完成 (参见AspectJ Development Environment Guide) 或者使用load-time weaving(参见 Section6.8.4, “在Spring应用中使用AspectJ Load-time weaving(LTW)”)。

AnnotationBeanConfigurerAspect 本身也需要Spring来配置(获得bean factory的引用,使用bean factory配置新的对象)。 为此Spring AOP命名空间定义了一个非常方便的标签。如下所示,可以很简单的在application context配置文件包含这个标签中。

<aop:spring-configured/>

如果你使用DTD代替Schema,对应的定义如下:

<bean
 class="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"
 factory-method="aspectOf"/>

在切面配置完成 之前 创建的@Configurable对象实例会导致在log中留下一个warning,并且任何对于该对象的配置都不会生效。 举一个例子,一个Spring管理配置的bean在被Spring初始化的时候创建了一个domain object。 对于这样的情况,你需要定义bean属性中的"depends-on"属性来手动指定该bean依赖于configuration切面。

<bean id="myService"
 class="com.xzy.myapp.service.MyService"
 depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">
 
 ...
 
</bean>
6.8.1.1. @Configurable object的单元测试

提供 @Configurable 支持的一个目的就是使得domain object的单元测试可以独立进行,不需要通过硬编码查找各种倚赖关系。如果 @Configurable 类型没有通过AspectJ织入, 则在单元测试过程中注解不会起到任何作用,测试中你可以简单的为对象的mock或者stub属性赋值,并且和正常情况一样的去使用该对象。 如果 @Configurable 类型通过AspectJ织入, 我们依然可以脱离容器进行单元测试,不过每次创建一个新的 @Configurable 对象时都会看到一个warning标示该对象不受Spring管理配置。

6.8.1.2.多application context情况下的处理

AnnotationBeanConfigurerAspect 通过一个AspectJ singleton切面来实现对 @Configurable 的支持。 一个singleton切面的作用域和一个静态变量的作用域是一样的,例如,对于每一个classloader有一个切面来定义类型。 这就意味着如果你在一个classloader层次结构中定义了多个application context的时候就需要考虑在哪里定义 <aop:spring-configured/> bean和在哪个classpath下放置Srping-aspects.jar。

考虑一下典型的Spring web项目,一般都是由一个父application context定义大部分business service和所需要的其他资源,然后每一个servlet拥有一个子application context定义。 所有这些context共存于同一个classloader hierarchy下,因此对于全体context,AnnotationBeanConfigurerAspect 仅可以维护一个引用。在这样的情况下,我们推荐在父application context中定义 <aop:spring-configured/> bean: 这里所定义的service可能是你希望注入domain object的。这样做的结果是你不能为子application context中使用@Configurable的domain object配置bean引用(可能你也根本就不希望那么做!)。

当在一个容器中部署多个web-app的时候,请确保每一个web-application使用自己的classloader来加载spring-aspects.jar中的类(例如将spring-aspects.jar放在WEB-INF/lib目录下)。 如果spring-aspects.jar被放在了容器的classpath下(因此也被父classloader加载),则所有的web application将共享一个aspect实例,这可能并不是你所想要的。

6.8.2.Spring中其他的AspectJ切面

除了 @Configurable 支持,spring-aspects.jar包含了一个AspectJ切面可以用来为那些使用了 @Transactional annotation 的类型和方法驱动Spring事务管理(参见 Chapter9, 事务管理)。 提供这个的主要目的是有些用户希望脱离Spring容器使用Spring的事务管理。

解析@Transactional annotations的切面是AnnotationTransactionAspect。 当使用这个切面时,你必须注解这个实现类(和/或这个类中的方法),而不是这个类实现的接口(如果有)。 AspectJ允许在接口上注解的Java规则 不被继承

类之上的一个@Transactional注解为该类中任何public操作的执行指定了默认的事务语义。

类内部方法上的一个@Transactional注解会覆盖类注解(如果存在)所给定的默认的事务语义。具有public、protected和default修饰符的方法都可以被注解。直接注解protected和default方法是让这个操作的执行 获得事务划分的唯一途径。

对于AspectJ程序员,希望使用Spring管理配置和事务管理支持,不过他们不想(或者不能)使用注解,spring-aspects.jar也包含了一些抽象切面供你继承来提供你自己的切入点定义。参见 AbstractBeanConfigurerAspect 和 AbstractTransactionAspect 的Javadoc获取更多信息。 作为一个例子,下面的代码片断展示了如何编写一个切面,然后通过bean原型定义中和类全名匹配的来配置domian object中所有的实例:

public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {
 
 public DomainObjectConfiguration() {
 setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
 }
 
 // the creation of a new bean (any object in the domain model)
 protected pointcut beanCreation(Object beanInstance) :
 initialization(new(..)) &&
 SystemArchitecture.inDomainModel() && 
this(beanInstance);
   
}

6.8.3.使用Spring IoC来配置AspectJ的切面

当在Spring application中使用AspectJ的时候,很自然的会想到用Spring来管理这些切面。 AspectJ runtime自身负责切面的创建,这意味着通过Spring来管理AspectJ 创建切面依赖于切面所使用的AspectJ instantiation model(per-clause)。

大多数AspectJ切面都是 singleton 切面。 管理这些切面非常容易,和通常一样创建一个bean定义引用该切面类型就可以了,并且在bean定义中包含 'factory-method="aspectOf"' 这个属性。 这确保Spring从AspectJ获取切面实例而不是尝试自己去创建该实例。示例如下:

<bean id="profiler" class="com.xyz.profiler.Profiler"
  factory-method="aspectOf">
 <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>

对于non-singleton的切面,最简单的配置管理方法是定义一个bean原型定义并且使用@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>
 <include name="thisBean"/>
 <include name="thatBean"/>
</aop:aspectj-autoproxy>

6.8.4.在Spring应用中使用AspectJ Load-time weaving(LTW)

Load-time weaving(LTW)指的是在虚拟机载入字节码文件时动态织入AspectJ切面。关于LTW的详细信息,请查看 LTW section of the AspectJ Development Environment Guide。在这里我们重点来看一下Java 5环境下Spring应用如何配置LTW。

LTW需要定义一个 aop.xml,并将其置于META-INF目录。 AspectJ会自动查找所有可见的classpath下的META-INF/aop.xml文件,并且通过定义内容的合集来配置自身。

一个基本的META-INF/aop.xml文件应该如下所示:

<!DOCTYPE aspectj PUBLIC
 "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
 
<aspectj>
 <weaver>
  <include within="com.xyz.myapp..*"/>
 </weaver>
</aspectj>

'<include/>'的内容告诉AspectJ那些类型需要被纳入织入过程。使用包名前缀并加上"..*"(表示该子包中的所有类型)是一个不错的默认设定。 使用include元素是非常重要的,不然AspectJ会查找每一个应用里面用到的类型(包括Spring的库和其它许多相关库)。通常你并不希望织入这些类型并且不愿意承担AspectJ尝试去匹配的开销。

希望在日志中记录LTW的活动,请添加如下选项:

<!DOCTYPE aspectj PUBLIC 
"-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd"> 
 
<aspectj> 
<weaver 
 options="-showWeaveInfo
 -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">
  <include within="com.xyz.myapp..*"/>
 </weaver>
</aspectj>

最后,如果希望精确的控制使用哪些切面,可以使用 aspects。 默认情况下所有定义的切面都将被织入(spring-aspects.jar包含了META-INF/aop.xml,定义了配置管理和事务管理切面)。 如果你在使用spring-aspects.jar,但是只希望使用配制管理切面而不需要事务管理的话,你可以像下面那样定义:

<!DOCTYPE aspectj PUBLIC
 "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
 
<aspectj>
 <aspects>
  <include within="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"/>
 </aspects>
 <weaver
 options="-showWeaveInfo -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">
 <include within="com.xyz.myapp..*"/>
 </weaver>
</aspectj>

在Java 5平台下,LTW可以通过虚拟机的参数来启用。

-javaagent:<path-to-ajlibs>/aspectjweaver.jar

6.9.其它资源

更多关于AspectJ的信息可以查看 AspectJ home page

Eclipse AspectJ by Adrian Colyer et. al. (Addison-Wesley, 2005)全面介绍并提供了AspectJ语言参考。

AspectJ in Action by Ramnivas Laddad (Manning, 2003)是一本非常出色介绍AOP的书籍;全书着重介绍了AspectJ,但也对一些通用的AOP场景进行了比较深入的研究。

面向切面编程AOP)提供另外一种角度来思考程序结构,通过这种方式弥补了面向对象编程(OOP)的不足。 除了类(classes)以外,AOP提供了 切面。切面对关注点进行模块化,例如横切多个类型和对象的事务管理。 (这些关注点术语通常称作 横切(crosscutting) 关注点。)

Spring的一个关键的组件就是 AOP框架。 尽管如此,Spring IoC容器并不依赖于AOP,这意味着你可以自由选择是否使用AOP,AOP提供强大的中间件解决方案,这使得Spring IoC容器更加完善。

Spring 2.0 AOP

Spring 2.0 引入了一种更加简单并且更强大的方式来自定义切面,用户可以选择使用基于模式(schema-based)的方式或者使用@AspectJ注解。 这两种风格都完全支持通知(Advice)类型和AspectJ的切入点语言,虽然实际上仍然使用Spring AOP进行织入(Weaving)。

本章主要讨论Spring 2.0对基于模式和基于@AspectJ的AOP支持。请查阅"AOP声明风格的选择"一节获取为你的应用选择适当的声明风格的建议。Spring 2.0完全保留了对Spring 1.2的向下兼容性,下一章 将讨论 Spring 1.2 API所提供的底层的AOP支持。

Spring中所使用的AOP:

· 提供声明式企业服务,特别是为了替代EJB声明式服务。 最重要的服务是 声明性事务管理(declarative transaction management) ,这个服务建立在Spring的抽象事务管理(transaction abstraction)之上。

· 允许用户实现自定义的切面,用AOP来完善OOP的使用。

这样你可以把Spring AOP看作是对Spring的一种增强,它使得Spring可以不需要EJB就能提供声明式事务管理;或者也可以使用Spring AOP框架的全部功能来实现自定义的切面。

本章首先 介绍了AOP的概念,无论你打算采用哪种风格的切面声明,这个部分都值得你一读。本章剩下的部分将着重于Spring 2.0对AOP的支持; 下一章 提供了关于Spring 1.2风格的AOP概述,也许你已经在其他书本,文章以及已有的应用程序中碰到过这种AOP风格。

如果你只打算使用通用的声明式服务或者预先打包的声明式中间件服务,例如缓冲池(pooling),那么你不必直接使用Spring AOP,而本章的大部分内容也可以直接跳过。

6.1.1.AOP概念

首先让我们从定义一些重要的AOP概念开始。这些术语不是Spring特有的。 不幸的是,AOP术语并不是特别的直观;如果Spring使用自己的术语,将会变得更加令人困惑。

· 切面(Aspect): 一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。 在Spring AOP中,切面可以使用通用类(基于模式的风格)或者在普通类中以 @Aspect 注解(@AspectJ风格)来实现。

· 连接点(Joinpoint): 在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点 总是 代表一个方法的执行。 通过声明一个org.aspectj.lang.JoinPoint类型的参数可以使通知(Advice)的主体部分获得连接点信息。

· 通知(Advice): 在切面的某个特定的连接点(Joinpoint)上执行的动作。通知有各种类型,其中包括“around”、“before”和“after”等通知。 通知的类型将在后面部分进行讨论。许多AOP框架,包括Spring,都是以拦截器做通知模型, 并维护一个以连接点为中心的拦截器链。

· 切入点(Pointcut): 匹配连接点(Joinpoint)的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。 切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。

· 引入(Introduction): (也被称为内部类型声明(inter-type declaration))。声明额外的方法或者某个类型的字段。 Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用一个引入来使bean实现 IsModified 接口,以便简化缓存机制。

· 目标对象(Target Object): 被一个或者多个切面(aspect)所通知(advise)的对象。也有人把它叫做 被通知(advised) 对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个 被代理(proxied) 对象。

· AOP代理(AOP Proxy): AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能)。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。 注意:Spring 2.0最新引入的基于模式(schema-based)风格和@AspectJ注解风格的切面声明,对于使用这些风格的用户来说,代理的创建是透明的。

· 织入(Weaving): 把切面(aspect)连接到其它的应用程序类型或者对象上,并创建一个被通知(advised)的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。 Spring和其他纯Java AOP框架一样,在运行时完成织入。

通知的类型:

· 前置通知(Before advice): 在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。

· 返回后通知(After returning advice): 在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。

· 抛出异常后通知(After throwing advice): 在方法抛出异常退出时执行的通知。

· 后通知(After (finally) advice): 当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。

· 环绕通知(Around Advice): 包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。

环绕通知是最常用的一种通知类型。大部分基于拦截的AOP框架,例如Nanning和JBoss4,都只提供环绕通知。

跟AspectJ一样,Spring提供所有类型的通知,我们推荐你使用尽量简单的通知类型来实现需要的功能。例如,如果你只是需要用一个方法的返回值来更新缓存,虽然使用环绕通知也能完成同样的事情, 但是你最好使用After returning通知而不是环绕通知。 用最合适的通知类型可以使得编程模型变得简单,并且能够避免很多潜在的错误。 比如,你不需要调用 JoinPoint(用于Around Advice)的 proceed() 方法,就不会有调用的问题。

在Spring 2.0中,所有的通知参数都是静态类型,因此你可以使用合适的类型(例如一个方法执行后的返回值类型)作为通知的参数而不是使用一个对象数组。

切入点(pointcut)和连接点(join point)匹配的概念是AOP的关键,这使得AOP不同于其它仅仅提供拦截功能的旧技术。切入点使得定位通知(advice)可独立于OO层次。 例如,一个提供声明式事务管理的around通知可以被应用到一组横跨多个对象中的方法上(例如服务层的所有业务操作)。

6.1.2.Spring AOP的功能和目标

Spring AOP用纯Java实现。它不需要专门的编译过程。Spring AOP不需要控制类装载器层次,因此它适用于J2EE web容器或应用服务器。

Spring目前仅支持使用方法调用作为连接点(join point)(在Spring bean上通知方法的执行)。 虽然可以在不影响到Spring AOP核心API的情况下加入对成员变量拦截器支持,但Spring并没有实现成员变量拦截器。 如果你需要把对成员变量的访问和更新也作为通知的连接点,可以考虑其它语法的Java语言,例如AspectJ。

Spring实现AOP的方法跟其他的框架不同。Spring并不是要尝试提供最完整的AOP实现(尽管Spring AOP有这个能力), 相反的,它其实侧重于提供一种AOP实现和Spring IoC容器的整合,用于帮助解决在企业级开发中的常见问题。

因此,Spring AOP通常都和Spring IoC容器一起使用。 Aspect使用普通的bean定义语法(尽管Spring提供了强大的“自动代理(autoproxying)”功能): 与其他AOP实现相比这是一个显著的区别。有些事使用Spring AOP是无法轻松或者高效的完成的,比如说通知一个细粒度的对象。 这种时候,使用AspectJ是最好的选择。不过经验告诉我们:于大多数在J2EE应用中遇到的问题,只要适合AOP来解决的,Spring AOP都没有问题,Spring AOP提供了一个非常好的解决方案。

Spring AOP从来没有打算通过提供一种全面的AOP解决方案来取代AspectJ。我们相信无论是基于代理(proxy-based )的框架比如说Spring亦或是full-blown的框架比如说是AspectJ都是很有价值的,他们之间的关系应该是互补而不是竞争的关系。 Spring 2.0可以无缝的整合Spring AOP,IoC 和AspectJ,使得所有的AOP应用完全融入基于Spring的应用体系。 这样的集成不会影响Spring AOP API或者AOP Alliance API;Spring AOP保留了向下兼容性。接下来的一章会详细讨论Spring AOP API。

6.1.3.Spring的AOP代理

Spring缺省使用J2SE 动态代理(dynamic proxies)来作为AOP的代理。这样任何接口都可以被代理。

Spring也支持使用CGLIB代理. 对于需要代理类而不是代理接口的时候CGLIB代理是很有必要的。如果一个业务对象并没有实现一个接口,默认就会使用CGLIB。 作为面向接口编程的最佳实践,业务对象通常都会实现一个或多个接口。但也有可能会 强制使用CGLIB, 在这种情况(希望不常有)下,你可能需要通知一个没有在接口中声明的方法,或者需要传入一个代理对象给方法作为具体类型

在Spring 2.0之后,Spring可能会提供多种其他类型的AOP代理,包括了完整的生成类。这不会影响到编程模型。

6.2.@AspectJ支持

"@AspectJ"使用了Java 5的注解,可以将切面声明为普通的Java类。 AspectJ 5发布的 AspectJ project 中引入了这种@AspectJ风格。 Spring 2.0 使用了和AspectJ 5一样的注解,使用了AspectJ 提供的一个库来做切点(pointcut)解析和匹配。但是,AOP在运行时仍旧是纯的Spring AOP,并不依赖于AspectJ 的编译器或者织入器(weaver)。

使用AspectJ的编译器或者织入器(weaver)的话就可以使用完整的AspectJ 语言,我们将在 Section6.8, “在Spring应用中使用AspectJ” 中讨论这个问题。

6.2.1.启用@AspectJ支持

为了在Spring配置中使用@AspectJ aspects,你必须首先启用Spring对基于@AspectJ aspects的配置支持,自动代理(autoproxying)基于通知是否来自这些切面。自动代理是指Spring会判断一个bean是否使用了一个或多个切面通知,并据此自动生成相应的代理以拦截其方法调用,并且确认通知是否如期进行。

通过在你的Spring的配置中引入下列元素来启用Spring对@AspectJ的支持:

<aop:aspectj-autoproxy/>

我们假使你正在使用 AppendixA, XML Schema-based configuration 所描述的schema支持。 关于如何在aop的命名空间中引入这些标签,请参见 SectionA.2.6, “The aop schema”

如果你正在使用DTD,你仍旧可以通过在你的application context中添加如下定义来启用@AspectJ支持:

<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" />

你需要在你的应用程序的classpath中引入两个AspectJ库:aspectjweaver.jar 和 aspectjrt.jar。 这些库可以在AspectJ的安装包(1.5.1或者之后的版本)中的 lib 目录里找到,或者也可以在Spring依赖库的 lib/aspectj 目录下找到。

6.2.2.声明一个切面

在启用@AspectJ支持的情况下,在application context中定义的任意带有一个@Aspect切面(拥有@Aspect注解)的bean都将被Spring自动识别并用于配置在Spring AOP。 以下例子展示了为了完成一个不是非常有用的切面所需要的最小定义:

下面是在application context中的一个常见的bean定义,这个bean指向一个使用了 @Aspect 注解的bean类:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
 <!-- configure properties of aspect here as normal -->
</bean>

下面是 NotVeryUsefulAspect 类定义,使用了 org.aspectj.lang.annotation.Aspect 注解。

package org.xyz;
import org.aspectj.lang.annotation.Aspect;
 
@Aspect
public class NotVeryUsefulAspect {
 
}

切面(用 @Aspect 注解的类)和其他类一样有方法和字段定义。他们也可能包括切入点,通知和引入(inter-type)声明。

6.2.3.声明一个切入点(pointcut)

回想一下,切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行。 Spring AOP 只支持 Spring bean 方法执行连接点。所以你可以把切入点看做是匹配 Spring bean 上方法的执行。 一个切入点声明有两个部分:一个包含名字和任意参数的签名,还有一个切入点表达式,该表达式决定了我们关注那个方法的执行。在 @AspectJ 注解风格的 AOP 中,一个切入点签名通过一个普通的方法定义来提供,并且切入点表达式使用 @Pointcut 注解来表示(作为切入点签名的方法必须返回 void 类型)。

用一个例子会帮助我们区分切入点签名和切入点表达式之间的差别,下面的例子定义了一个切入点'anyOldTransfer', 这个切入点将匹配任何名为 "transfer" 的方法的执行:

@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

切入点表达式,也就是 @Pointcut 注解的值,是正规的AspectJ 5切入点表达式。 如果你想要更多了解AspectJ的 切入点语言,请参见 AspectJ 编程指南(如果要了解基于Java 5的扩展请参阅 AspectJ 5 开发手册)或者其他人写的关于AspectJ的书,例如Colyer et. al.著的《Eclipse AspectJ》或者Ramnivas Laddad著的《AspectJ in Action》。

6.2.3.1.切入点指定者的支持

Spring AOP 支持在切入点表达式中使用如下的AspectJ切入点指定者:

其他的切入点类型

完整的AspectJ切入点语言支持额外的切入点指定者,但是Spring不支持这个功能。 他们分别是call, initialization, preinitialization, staticinitialization, get, set, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this 和 @withincode。 在Spring AOP中使用这些指定者将会导致抛出IllegalArgumentException异常。

Spring AOP支持的切入点指定者可能在将来的版本中得到扩展,不但支持更多的AspectJ 切入点指定者(例如"if"),还会支持某些Spring特有的切入点指定者,比如"bean"(用于匹配bean的名字)。

· execution - 匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指定者。

· within - 限定匹配特定类型的连接点(在使用Spring AOP的时候,在匹配的类型中定义的方法的执行)。

· this - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。

· target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中目标对象(被代理的appolication object)是指定类型的实例。

· args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。

· @target- 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中执行的对象的类已经有指定类型的注解。

· @args- 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中实际传入参数的运行时类型有指定类型的注解。

· @within- 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。

· @annotation - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中连接点的主题有某种给定的注解。

因为Spring AOP限制了连接点必须是方法执行级别的,pointcut designators的讨论也给出了一个定义,这个定义和AspectJ的编程指南中的定义相比显得更加狭窄。除此之外,AspectJ它本身有基于类型的语义,在执行的连接点'this'和'target'都是指同一个对象,也就是执行方法的对象。 Spring AOP是一个基于代理的系统,并且严格区分代理对象本身(对应于'this')和背后的目标对象(对应于'target')

6.2.3.2.合并切入点表达式

切入点表达式可以使用using '&', '||' 和 '!'来合并.还可以通过名字来指向切入点表达式。 以下的例子展示了三种切入点表达式: anyPublicOperation(在一个方法执行连接点代表了任意public方法的执行时匹配); inTrading(在一个代表了在交易模块中的任意的方法执行时匹配) 和 tradingOperation(在一个代表了在交易模块中的任意的公共方法执行时匹配)。

 @Pointcut("execution(public * *(..))")
 private void anyPublicOperation() {}
 
 @Pointcut("within(com.xyz.someapp.trading..*")
 private void inTrading() {}
 
 @Pointcut("anyPublicOperation() && inTrading()")
 private void tradingOperation() {}

就上所示的,从更小的命名组件来构建更加复杂的切入点表达式是一种最佳实践。 当用名字来指定切入点时使用的是常见的Java成员可视性访问规则。 (比如说,你可以在同一类型中访问私有的切入点,在继承关系中访问受保护的切入点,可以在任意地方访问公共切入点。成员可视性访问规则不影响到切入点的 匹配

6.2.3.3.共享常见的切入点(pointcut)定义

当开发企业级应用的时候,你通常会想要从几个切面来参考模块化的应用和特定操作的集合。 我们推荐定义一个“SystemArchitecture”切面来捕捉常见的切入点表达式。一个典型的切面可能看起来像下面这样:

package com.xyz.someapp;
 
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
 
@Aspect
public class SystemArchitecture {
 
 /**
 * A join point is in the web layer if the method is defined
 * in a type in the com.xyz.someapp.web package or any sub-package
 * under that.
 */
 @Pointcut("within(com.xyz.someapp.web..*)")
 public void inWebLayer() {}
 
 /**
 * A join point is in the service layer if the method is defined
 * in a type in the com.xyz.someapp.service package or any sub-package
 * under that.
 */
 @Pointcut("within(com.xyz.someapp.service..*)")
 public void inServiceLayer() {}
 
 /**
 * A join point is in the data access layer if the method is defined
 * in a type in the com.xyz.someapp.dao package or any sub-package
 * under that.
 */
 @Pointcut("within(com.xyz.someapp.dao..*)")
 public void inDataAccessLayer() {}
 
 /**
 * A business service is the execution of any method defined on a service
 * interface. This definition assumes that interfaces are placed in the
 * "service" package, and that implementation types are in sub-packages.
 * 
* If you group service interfaces by functional area (for example, 
* in packages com.xyz.someapp.abc.service and com.xyz.def.service) then
 * the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
 * could be used instead.
 */
 @Pointcut("execution(* com.xyz.someapp.service.*.*(..))")
 public void businessService() {}
 
/**
 * A data access operation is the execution of any method defined on a 
* dao interface. This definition assumes that interfaces are placed in the
 * "dao" package, and that implementation types are in sub-packages.
 */
 @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
 public void dataAccessOperation() {}
 
}

示例中的切入点定义了一个你可以在任何需要切入点表达式的地方可引用的切面。比如,为了使service层事务化,你可以写成:

<aop:config>
 <aop:advisor
  pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
  advice-ref="tx-advice"/>
</aop:config>
 
<tx:advice id="tx-advice">
<tx:attributes>
 <tx:method name="*" propagation="REQUIRED"/>
 </tx:attributes>
</tx:advice>

Section6.3, “Schema-based AOP support” 中讨论 <aop:config> 和 <aop:advisor>标签。 在 Chapter9, 事务管理 中讨论事务标签。

6.2.3.4.示例

Spring AOP 用户可能会经常使用 execution pointcut designator。执行表达式的格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

除了返回类型模式(上面代码片断中的ret-type-pattern),名字模式和参数模式以外,所有的部分都是可选的。返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 你会使用的最频繁的返回类型模式是 *,它代表了匹配任意的返回类型。 一个全称限定的类型名将只会匹配返回给定类型的方法。名字模式匹配的是方法名。你可以使用 * 通配符作为所有或者部分命名模式。 参数模式稍微有点复杂:() 匹配了一个不接受任何参数的方法,而 (..) 匹配了一个接受任意数量参数的方法(零或者更多)。 模式 (*) 匹配了一个接受一个任何类型的参数的方法。 模式 (*,String) 匹配了一个接受两个参数的方法,第一个可以是任意类型,第二个则必须是String类型。 请参见AspectJ编程指南的 Language Semantics 部分。

下面给出一些常见切入点表达式的例子。

· 任意公共方法的执行:

execution(public * *(..))

· 任何一个以“set”开始的方法的执行:

execution(* set*(..))

· AccountService 接口的任意方法的执行:

execution(* com.xyz.service.AccountService.*(..))

· 定义在service包里的任意方法的执行:

execution(* com.xyz.service.*.*(..))

· 定义在service包或者子包里的任意方法的执行:

execution(* com.xyz.service..*.*(..))

· 在service包里的任意连接点(在Spring AOP中只是方法执行) :

within(com.xyz.service.*)

· 在service包或者子包里的任意连接点(在Spring AOP中只是方法执行) :

within(com.xyz.service..*)

· 实现了 AccountService 接口的代理对象的任意连接点(在Spring AOP中只是方法执行) :

this(com.xyz.service.AccountService)

'this'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得代理对象可以在通知体内访问到的部分。

· 实现了 AccountService 接口的目标对象的任意连接点(在Spring AOP中只是方法执行) :

target(com.xyz.service.AccountService)

'target'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得目标对象可以在通知体内访问到的部分。

· 任何一个只接受一个参数,且在运行时传入的参数实现了 Serializable 接口的连接点 (在Spring AOP中只是方法执行)

args(java.io.Serializable)

'args'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得方法参数可以在通知体内访问到的部分。

请注意在例子中给出的切入点不同于 execution(* *(java.io.Serializable)): args只有在动态运行时候传入参数是可序列化的(Serializable)才匹配,而execution 在传入参数的签名声明的类型实现了 Serializable 接口时候匹配。

· 有一个 @Transactional 注解的目标对象中的任意连接点(在Spring AOP中只是方法执行)

@target(org.springframework.transaction.annotation.Transactional)

'@target' 也可以在binding form中使用:请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

· 任何一个目标对象声明的类型有一个 @Transactional 注解的连接点(在Spring AOP中只是方法执行)

@within(org.springframework.transaction.annotation.Transactional)

'@within'也可以在binding form中使用:- 请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

· 任何一个执行的方法有一个 @Transactional annotation的连接点(在Spring AOP中只是方法执行)

@annotation(org.springframework.transaction.annotation.Transactional)

'@annotation' 也可以在binding form中使用:- 请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

· 任何一个接受一个参数,并且传入的参数在运行时的类型实现了 @Classified annotation的连接点(在Spring AOP中只是方法执行)

@args(com.xyz.security.Classified)

'@args'也可以在binding form中使用:- 请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

6.2.4.声明通知

通知是跟一个切入点表达式关联起来的,并且在切入点匹配的方法执行之前或者之后或者之前和之后运行。 切入点表达式可能是指向已命名的切入点的简单引用或者是一个已经声明过的切入点表达式。

6.2.4.1.前置通知(Before advice)

一个切面里使用 @Before 注解声明前置通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
 
@Aspect
public class BeforeExample {
 
 @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
 public void doAccessCheck() {
 // ...
 }
 
}

如果使用一个in-place 的切入点表达式,我们可以把上面的例子换个写法:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
 
@Aspect
public class BeforeExample {
 
 @Before("execution(* com.xyz.myapp.dao.*.*(..))")
 public void doAccessCheck() {
 // ...
 }
 
}
6.2.4.2.返回后通知(After returning advice)

返回后通知通常在一个匹配的方法返回的时候执行。使用 @AfterReturning 注解来声明:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
 
@Aspect
public class AfterReturningExample {
 
 @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
 public void doAccessCheck() {
 // ...
 }
 
}

说明:你可以在同一个切面里定义多个通知,或者其他成员。我们只是在展示如何定义一个简单的通知。这些例子主要的侧重点是正在讨论的问题。

有时候你需要在通知体内得到返回的值。你可以使用以 @AfterReturning 接口的形式来绑定返回值:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
 
@Aspect
public class AfterReturningExample {
 
 @AfterReturning(
 pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
 returning="retVal")
 public void doAccessCheck(Object retVal) {
 // ...
 }
 
}

在 returning 属性中使用的名字必须对应于通知方法内的一个参数名。 当一个方法执行返回后,返回值作为相应的参数值传入通知方法。一个 returning 子句也限制了只能匹配到返回指定类型值的方法。 (在本例子中,返回值是 Object 类,也就是说返回任意类型都会匹配)

6.2.4.3.抛出后通知(After throwing advice)

抛出后通知在一个方法抛出异常后执行。使用 @AfterThrowing 注解来声明:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
 
@Aspect
public class AfterThrowingExample {
 
 @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
 public void doRecoveryActions() {
 // ...
 }
 
}

你通常会想要限制通知只在某种特殊的异常被抛出的时候匹配,你还希望可以在通知体内得到被抛出的异常。 使用 throwing 属性不光可以限制匹配的异常类型(如果你不想限制,请使用 Throwable 作为异常类型),还可以将抛出的异常绑定到通知的一个参数上。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
 
@Aspect
public class AfterThrowingExample {
 
 @AfterThrowing(
 pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
 throwing="ex")
 public void doRecoveryActions(DataAccessException ex) {
 // ...
 }
 
}

在 throwing 属性中使用的名字必须与通知方法内的一个参数对应。 当一个方法因抛出一个异常而中止后,这个异常将会作为那个对应的参数送至通知方法。 throwing 子句也限制了只能匹配到抛出指定异常类型的方法(上面的示例为 DataAccessException)。

6.2.4.4.后通知(After (finally) advice)

不论一个方法是如何结束的,在它结束后(finally)后通知(After (finally) advice)都会运行。 使用 @After 注解来声明。这个通知必须做好处理正常返回和异常返回两种情况。通常用来释放资源。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
 
@Aspect
public class AfterFinallyExample {
 
 @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
 public void doReleaseLock() {
 // ...
 }
 
}
6.2.4.5.环绕通知(Around Advice)

最后一种通知是环绕通知。环绕通知在一个方法执行之前和之后执行。 它使得通知有机会既在一个方法执行之前又在执行之后运行。并且,它可以决定这个方法在什么时候执行,如何执行,甚至是否执行。环绕通知经常在在某线程安全的环境下,你需要在一个方法执行之前和之后共享某种状态的时候使用。 请尽量使用最简单的满足你需求的通知。(比如如果前置通知(before advice)也可以适用的情况下不要使用环绕通知)。

环绕通知使用 @Around 注解来声明。通知的第一个参数必须是 ProceedingJoinPoint 类型。 在通知体内,调用 ProceedingJoinPoint 的 proceed() 方法将会导致潜在的连接点方法执行。 proceed 方法也可能会被调用并且传入一个 Object[] 对象-该数组将作为方法执行时候的参数。

当传入一个Object[]对象的时候,处理的方法与通过AspectJ编译器处理环绕通知略有不同。对于使用传统AspectJ语言写的环绕通知来说,传入参数的数量必须和传递给环绕通知的参数数量匹配(不是后台的连接点接受的参数数量),并且特定顺序的传入参数代替了将要绑定给连接点的原始值(如果你看不懂不用担心)。 Spring采用的方法更加简单并且更好得和他的基于代理(proxy-based),只匹配执行的语法相适用。如果你适用AspectJ的编译器和编织器来编译为Spring而写的@AspectJ切面和处理参数,你只需要了解这一区别即可。有一种方法可以让你写出100%兼容Spring AOP和AspectJ的,我们将会在后续的通知参数(advice parameters)的章节中讨论它。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
 
@Aspect
public class AroundExample {
 
 @Around("com.xyz.myapp.SystemArchitecture.businessService()")
 public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
 // start stopwatch
 Object retVal = pjp.proceed();
 // stop stopwatch
 return retVal;
 }
 
}

方法的调用者得到的返回值就是环绕通知返回的值。 例如:一个简单的缓存切面,如果缓存中有值,就返回该值,否则调用proceed()方法。 请注意proceed可能在通知体内部被调用一次,许多次,或者根本不被调用。

6.2.4.6.通知参数(Advice parameters)

Spring 2.0 提供了完整的通知类型 - 这意味着你可以在通知签名中声明所需的参数,(就像在以前的例子中我们看到的返回值和抛出异常一样)而不总是使用Object[]。 我们将会看到如何在通知体内访问参数和其他上下文相关的值。首先让我们看以下如何编写普通的通知以找出正在被通知的方法。

6.2.4.6.1.访问当前的连接点

任何通知方法可以将第一个参数定义为 org.aspectj.lang.JoinPoint 类型 (环绕通知需要定义为 ProceedingJoinPoint 类型的, 它是 JoinPoint 的一个子类。) JoinPoint 接口提供了一系列有用的方法, 比如 getArgs()(返回方法参数)、getThis()(返回代理对象)、getTarget()(返回目标)、getSignature()(返回正在被通知的方法相关信息)和 toString()(打印出正在被通知的方法的有用信息)。详细的内容请参考Javadocs。

6.2.4.6.2.传递参数给通知(Advice)

我们已经看到了如何绑定返回值或者异常(使用后置通知(after returning)和异常后通知(after throwing advice)。 为了可以在通知(adivce)体内访问参数,你可以使用 args 来绑定。 如果在一个参数表达式中应该使用类型名字的地方使用一个参数名字,那么当通知执行的时候对应的参数值将会被传递进来。可能给出一个例子会更好理解。假使你想要通知(advise)接受某个Account对象作为第一个参数的DAO操作的执行,你想要在通知体内也能访问到account对象,你可以写如下的代码:

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" + 
"args(account,..)")
public void validateAccount(Account account) {
 // ...
}

切入点表达式的 args(account,..) 部分有两个目的:首先它保证了只会匹配那些接受至少一个参数的方法的执行,而且传入的参数必须是 Account 类型的实例, 其次它使得可以在通知体内通过 account 参数来访问那个account参数。

另外一个办法是定义一个切入点,这个切入点在匹配某个连接点的时候“提供”了一个Account对象, 然后直接从通知中访问那个命名的切入点。你可以这样写:

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" + 
"args(account,..)")
private void accountDataAccessOperation(Account account) {}
 
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
 // ...
}

如果有兴趣了解更详细的内容,请参阅 AspectJ 编程指南。

代理对象(this)、目标对象(target) 和注解(@within, @target, @annotation, @args)都可以用一种简单格式绑定。 以下的例子展示了如何使用 @Auditable 注解来匹配方法执行,并提取AuditCode。

首先是 @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();
 // ...
}
6.2.4.6.3.决定参数名

绑定在通知上的参数依赖切入点表达式的匹配名,并借此在(通知(advice)和切入点(pointcut))的方法签名中声明参数名。 参数名 无法 通过Java反射来获取,所以Spring AOP使用如下的策略来决定参数名字:

· 如果参数名字已经被用户明确指定,则使用指定的参数名:通知(advice)和切入点(pointcut)注解有一个额外的"argNames"属性,该属性用来指定所注解的方法的参数名 - 这些参数名在运行时是 可以 访问的。例子如下:

· @Before(
· value="com.xyz.lib.Pointcuts.anyPublicMethod() && " + 
· "@annotation(auditable)",
· argNames="auditable")
· public void audit(Auditable auditable) {
· AuditCode code = auditable.value();
· // ...
}

如果一个@AspectJ切面已经被AspectJ编译器(ajc)编译过了,那么就不需要再添加argNames参数了,因为编译器会自动完成这一工作。

· 使用 'argNames' 属性有点不那么优雅,所以如果没有指定'argNames' 属性, Spring AOP 会寻找类的debug信息,并且尝试从本地变量表(local variable table)中来决定参数名字。只要编译的时候使用了debug信息(至少要使用 '-g:vars' ),就可获得这些信息。 使用这个flag编译的结果是: (1)你的代码将能够更加容易的读懂(反向工程), (2)生成的class文件会稍许大一些(通常是不重要的), (3)移除不被使用的本地变量的优化功能将会失效。 换句话说,你在使用这个flag的时候不会遇到任何困难。

· 如果不加上debug信息来编译的话,Spring AOP将会尝试推断参数的绑定。 (例如,要是只有一个变量被绑定到切入点表达式(pointcut expression)、通知方法(advice method)将会接受这个参数,这是显而易见的)。 如果变量的绑定不明确,将会抛出一个 AmbiguousBindingException 异常。

· 如果以上所有策略都失败了,将会抛出一个 IllegalArgumentException 异常。

6.2.4.6.4.处理参数

我们之前提过我们将会讨论如何编写一个 带参数的 的proceed()调用,使得不论在Spring AOP中还是在AspectJ都能正常工作。 解决方法是保证通知签名依次绑定方法参数。比如说:

@Around("execution(List<Account> find*(..)) &&" +
 "com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
 "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp, String accountHolderNamePattern)
throws Throwable {
 String newPattern = preProcess(accountHolderNamePattern);
 return pjp.proceed(new Object[] {newPattern});
}

大多数情况下你都会这样绑定(就像上面的例子那样)。

6.2.4.7.通知(Advice)顺序

如果有多个通知想要在同一连接点运行会发生什么?Spring AOP 的执行通知的顺序跟AspectJ的一样。 在“进入”连接点的情况下,最高优先级的通知会先执行(所以上面给出的两个前置通知(before advice)中,优先级高的那个会先执行)。 在“退出”连接点的情况下,最高优先级的通知会最后执行。(所以上面给出的两个前置通知(before advice)中,优先级高的那个会第二个执行)。 对于定义在相同切面的通知,根据声明的顺序来确定执行顺序。比如下面这个切面:

@Aspect
public class AspectWithMultipleAdviceDeclarations {
 
 @Pointcut("execution(* foo(..))")
 public void fooExecution() {}
 
@Before("fooExecution()")
 public void doBeforeOne() {
 // ...
 }
 
@Before("fooExecution()")
 public void doBeforeTwo() {
 // ...
 }
 
@AfterReturning("fooExecution()")
 public void doAfterOne() {
 // ...
 }
 
 @AfterReturning("fooExecution()")
 public void doAfterTwo() {
 // ...
 }
 
}

这样,假使对于任何一个名字为foo的方法的执行, doBeforeOne、doBeforeTwo、doAfterOne 和 doAfterTwo 通知方法都需要运行。 执行顺序将按照声明的顺序来确定。在这个例子中,执行的结果会是:

doBeforeOne
doBeforeTwo
foo
doAfterOne
doAfterTwo

换言之,因为doBeforeOne先定义,它会先于doBeforeTwo执行,而doAfterTwo后于doAfterOne定义,所以它会在doAfterOne之后执行。 只需要记住通知是按照定义的顺序来执行的就可以了。 - 如果想要知道更加详细的内容,请参阅AspectJ编程指南。

当定义在 不同的 切面里的两个通知都需要在一个相同的连接点中运行,那么除非你指定,否则执行的顺序是未知的。你可以通过指定优先级来控制执行顺序。在Spring中可以在切面类中实现 org.springframework.core.Ordered 接口做到这一点。 在两个切面中,Ordered.getValue() 方法返回值较低的那个有更高的优先级。

6.2.5.引入(Introductions)

引入(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.SystemArchitecture.businessService() &&" +
  "this(usageTracked)")
 public void recordUsage(UsageTracked usageTracked) {
 usageTracked.incrementUseCount();
 }
 
}

实现的接口通过被注解的字段类型来决定。@DeclareParents 注解的 value 属性是一个AspectJ的类型模式:- 任何匹配类型的bean都会实现 UsageTracked 接口。 请注意,在上面的前置通知(before advice)的例子中,service beans 可以直接用作 UsageTracked 接口的实现。 如果需要编程式的来访问一个bean,你可以这样写:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

6.2.6.切面实例化模型

这是一个高级主题...

默认情况下,在application context中每一个切面都会有一个实例。 AspectJ 把这个叫做单个实例化模型(singleton instantiation model)。 也可以用其他的生命周期来定义切面:- Spring支持AspectJ的 perthis 和 pertarget 实例化模型 (现在还不支持percflow、percflowbelow 和 pertypewithin )。

一个"perthis" 切面的定义:在 @Aspect 注解中指定perthis 子句。 让我们先来看一个例子,然后解释它是如何运作的:

@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {
 
 private int someState;
 
 @Before(com.xyz.myapp.SystemArchitecture.businessService())
 public void recordServiceUsage() {
 // ...
 }
 
}

这个perthis子句的效果是每个独立的service对象执行时都会创建一个切面实例(切入点表达式所匹配的连接点上的每一个独立的对象都会绑定到'this'上)。 service对象的每个方法在第一次执行的时候创建切面实例。切面在service对象失效的同时失效。 在切面实例被创建前,所有的通知都不会被执行,一旦切面对象创建完成,定义的通知将会在匹配的连接点上执行,但是只有当service对象是和切面关联的才可以。 如果想要知道更多关于per-clauses的信息,请参阅 AspectJ 编程指南。

'pertarget'实例模型的跟“perthis”完全一样,只不过是为每个匹配于连接点的独立目标对象创建一个切面实例。

6.2.7.例子

现在你已经看到了每个独立的部分是如何运作的了,是时候把他们放到一起做一些有用的事情了!

因为并发的问题,有时候business services可能会失败(例如,死锁失败)。如果重新尝试一下,很有可能就会成功。对于business services来说,重试几次是很正常的(Idempotent操作不需要用户参与,否则会得出矛盾的结论)我们可能需要透明的重试操作以避免让客户看见 PessimisticLockingFailureException 例外被抛出。 很明显,在一个横切多层的情况下,这是非常有必要的,因此通过切面来实现是很理想的。

因为我们想要重试操作,我们会需要使用到环绕通知,这样我们就可以多次调用proceed()方法。下面是简单的切面实现:

@Aspect
public class ConcurrentOperationExecutor implements Ordered {
 
private static final int DEFAULT_MAX_RETRIES = 2;
 
 private int maxRetries = DEFAULT_MAX_RETRIES;
 private int order = 1;
 
 public void setMaxRetries(int maxRetries) {
 this.maxRetries = maxRetries;
 }
 
public int getOrder() {
 return this.order;
 }
 
public void setOrder(int order) {
 this.order = order;
 }
 
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
 public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { 
int numAttempts = 0;
 PessimisticLockingFailureException lockFailureException;
 do {
 numAttempts++;
 try { 
return pjp.proceed();
 }
 catch(PessimisticLockingFailureException ex) {
 lockFailureException = ex;
 }
 }
 while(numAttempts <= this.maxRetries);
 throw lockFailureException;
 }
 
}

请注意切面实现了 Ordered 接口,这样我们就可以把切面的优先级设定为高于事务通知(我们每次重试的时候都想要在一个全新的事务中进行)。 maxRetries 和 order 属性都可以在Spring中配置。 主要的动作在 doConcurrentOperation 这个环绕通知中发生。 请注意这个时候我们所有的 businessService() 方法都会使用这个重试策略。 我们首先会尝试处理,然后如果我们得到一个 PessimisticLockingFailureException 意外,我们只需要简单的重试,直到我们耗尽所有预设的重试次数。

对应的Spring配置如下:

<aop:aspectj-autoproxy/>
 
<bean id="concurrentOperationExecutor"
 class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
 <property name="maxRetries" value="3"/>
 <property name="order" value="100"/> 
</bean>

为了改进切面,使之仅仅重试idempotent操作,我们可以定义一个 Idempotent 注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
 // marker annotation
}

并且对service操作的实现进行注解。 这样如果你只希望改变切面使得idempotent的操作会尝试多次,你只需要改写切入点表达式,这样只有 @Idempotent 操作会匹配:

@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
 "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { 
...
}

6.3.Schema-based AOP support

如果你无法使用Java 5,或者你比较喜欢使用XML格式,Spring2.0也提供了使用新的"aop"命名空间来定义一个切面。和使用@AspectJ风格完全一样,切入点表达式和通知类型同样得到了支持,因此在这一节中我们将着重介绍新的 语法 和回顾前面我们所讨论的如何写一个切入点表达式和通知参数的绑定(Section6.2, “@AspectJ支持”)。

使用本章所介绍的aop命名空间标签(aop namespace tag),你需要引入AppendixA, XML Schema-based configuration中提及的spring-aop schema。参见SectionA.2.6, “The aop schema”

在Spring的配置文件中,所有的切面和通知器都必须定义在 <aop:config> 元素内部。 一个application context可以包含多个 <aop:config>。 一个 <aop:config> 可以包含pointcut,advisor和aspect元素(注意它们必须按照这样的顺序进行声明)。

Warning

<aop:config>风格的配置使得对Spring auto-proxying 机制的使用变得很笨重。如果你已经通过BeanNameAutoProxyCreator或类似的东西使用显式的auto-proxying将会引发问题 (例如通知没有被织入)。推荐的使用模式是只使用<aop:config>风格或只使用 AutoProxyCreator风格

6.3.1.声明一个切面

有了schema的支持,切面就和常规的Java对象一样被定义成application context中的一个bean。 对象的字段和方法提供了状态和行为信息,XML文件则提供了切入点和通知信息。

切面使用<aop:aspect>来声明,backing bean(支持bean)通过 ref 属性来引用:

<aop:config>
 <aop:aspect id="myAspect" ref="aBean">
 ...
 </aop:aspect>
</aop:config>
 
<bean id="aBean" class="...">
 ...
</bean>

切面的支持bean(上例中的"aBean")可以象其他Spring bean一样被容器管理配置以及依赖注入。

6.3.2.声明一个切入点

切入点可以在切面里面声明,这种情况下切入点只在切面内部可见。切入点也可以直接在<aop:config>下定义,这样就可以使多个切面和通知器共享该切入点。

一个描述service层中表示所有service执行的切入点可以如下定义:

<aop:config>
 
 <aop:pointcut id="businessService"
 expression="execution(* com.xyz.myapp.service.*.*(..))"/>
 
</aop:config>

注意切入点表达式本身使用了 Section6.2, “@AspectJ支持” 中描述的AspectJ 切入点表达式语言。如果你在Java 5环境下使用基于schema的声明风格,可参考切入点表达式类型中定义的命名式切入点,不过这在JDK1.4及以下版本中是不被支持的(因为依赖于Java 5中的AspectJ反射API)。 所以在JDK 1.5中,上面的切入点的另外一种定义形式如下:

<aop:config>
 
 <aop:pointcut id="businessService"
 expression="com.xyz.myapp.SystemArchitecture.businessService()"/>
 
</aop:config>

假定你有 Section6.2.3.3, “共享常见的切入点(pointcut)定义”中说描述的 SystemArchitecture 切面。

在切面里面声明一个切入点和声明一个顶级的切入点非常类似:

<aop:config>
 
 <aop:aspect id="myAspect" ref="aBean">
 <aop:pointcut id="businessService"
  expression="execution(* com.xyz.myapp.service.*.*(..))"/>
 ...
 </aop:aspect>
</aop:config>

当需要连接子表达式的时候,'&'在XML中用起来非常不方便,所以关键字'and', 'or' 和 'not'可以分别用来代替'&', '||' 和 '!'。

注意这种方式定义的切入点通过XML id来查找,并且不能定义切入点参数。在基于schema的定义风格中命名切入点支持较之@AspectJ风格受到了很多的限制。

6.3.3.声明通知

和@AspectJ风格一样,基于schema的风格也支持5种通知类型并且两者具有同样的语义。

6.3.3.1.通知(Advice)

Before通知在匹配方法执行前进入。在<aop:aspect>里面使用<aop:before>元素进行声明。

<aop:aspect id="beforeExample" ref="aBean">
 
 <aop:before
  pointcut-ref="dataAccessOperation"
  method="doAccessCheck"/>
 ...
</aop:aspect>

这里 dataAccessOperation 是一个顶级(<aop:config>)切入点的id。 要定义内置切入点,可将 pointcut-ref 属性替换为 pointcut 属性:

<aop:aspect id="beforeExample" ref="aBean">
 
 <aop:before
  pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
  method="doAccessCheck"/>
 ...
</aop:aspect>

我们已经在@AspectJ风格章节中讨论过了,使用命名切入点能够明显的提高代码的可读性。

Method属性标识了提供了通知的主体的方法(doAccessCheck)。这个方法必须定义在包含通知的切面元素所引用的bean中。在一个数据访问操作执行之前(执行连接点和切入点表达式匹配),切面中的"doAccessCheck"会被调用。

6.3.3.2.返回后通知(After returning advice)

After returning通知在匹配的方法完全执行后运行。和Before通知一样,可以在<aop:aspect>里面声明。例如:

<aop:aspect id="afterReturningExample" ref="aBean">
 
 <aop:after-returning
  pointcut-ref="dataAccessOperation"
  method="doAccessCheck"/>
 
 ...
 
</aop:aspect>

和@AspectJ风格一样,通知主体可以接收返回值。使用returning属性来指定接收返回值的参数名:

<aop:aspect id="afterReturningExample" ref="aBean">
 
 <aop:after-returning
  pointcut-ref="dataAccessOperation"
  returning="retVal"
  method="doAccessCheck"/>
 
 ...
 
</aop:aspect>

doAccessCheck方法必须声明一个名字叫 retVal 的参数。 参数的类型强制匹配,和先前我们在@AfterReturning中讲到的一样。例如,方法签名可以这样声明:

public void doAccessCheck(Object retVal) {...
6.3.3.3.抛出异常后通知(After throwing advice)

After throwing通知在匹配方法抛出异常退出时执行。在 <aop:aspect> 中使用after-throwing元素来声明:

<aop:aspect id="afterThrowingExample" ref="aBean">
 
 <aop:after-throwing
  pointcut-ref="dataAccessOperation"
  method="doRecoveryActions"/>
 
 ...
 
</aop:aspect>

和@AspectJ风格一样,可以从通知体中获取抛出的异常。使用throwing属性来指定异常的名称,用这个名称来获取异常:

<aop:aspect id="afterThrowingExample" ref="aBean">
 
 <aop:after-throwing
  pointcut-ref="dataAccessOperation"
  thowing="dataAccessEx"
  method="doRecoveryActions"/>
 
 ...
 
</aop:aspect>

doRecoveryActions方法必须声明一个名字为 dataAccessEx 的参数。 参数的类型强制匹配,和先前我们在@AfterThrowing中讲到的一样。例如:方法签名可以如下这般声明:

public void doRecoveryActions(DataAccessException dataAccessEx) {...
6.3.3.4.后通知(After (finally) advice)

After (finally)通知在匹配方法退出后执行。使用 after 元素来声明:

<aop:aspect id="afterFinallyExample" ref="aBean">
 
 <aop:after
  pointcut-ref="dataAccessOperation"
  method="doReleaseLock"/>
 
 ...
 
</aop:aspect>
6.3.3.5.通知

Around通知是最后一种通知类型。Around通知在匹配方法运行期的“周围”执行。它有机会在目标方法的前面和后面执行,并决定什么时候运行,怎么运行,甚至是否运行。 Around通知经常在需要在一个方法执行前或后共享状态信息,并且是线程安全的情况下使用(启动和停止一个计时器就是一个例子)。注意选择能满足你需求的最简单的通知类型(i.e.如果简单的before通知就能做的事情绝对不要使用around通知)。

Around通知使用 aop:around 元素来声明。 通知方法的第一个参数的类型必须是 ProceedingJoinPoint 类型。 在通知的主体中,调用 ProceedingJoinPoint的proceed() 方法来执行真正的方法。 proceed 方法也可能会被调用并且传入一个 Object[] 对象 - 该数组将作为方法执行时候的参数。参见 Section6.2.4.5, “环绕通知(Around Advice)” 中提到的一些注意点。

<aop:aspect id="aroundExample" ref="aBean">
 
 <aop:around
  pointcut-ref="businessService"
  method="doBasicProfiling"/>
 
 ...
 
</aop:aspect>

doBasicProfiling 通知的实现和@AspectJ中的例子完全一样(当然要去掉注解):

public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
 // start stopwatch
 Object retVal = pjp.proceed();
 // stop stopwatch
 return retVal;
}
6.3.3.6.通知参数

Schema-based声明风格和@AspectJ支持一样,支持通知的全名形式 - 通过通知方法参数名字来匹配切入点参数。参见 Section6.2.4.6, “通知参数(Advice parameters)” 获取详细信息。

如果你希望显式指定通知方法的参数名(而不是依靠先前提及的侦测策略),可以通过 arg-names 属性来实现。示例如下:

<aop:before
 pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
 method="audit"
 arg-names="auditable"/>

The arg-names attribute accepts a comma-delimited list of parameter names.

arg-names属性接受由逗号分割的参数名列表。

请看下面这个基于XSD风格的更复杂一些的实例,它展示了关联多个强类型参数的环绕通知的使用。

首先,服务接口及它的实现将被通知:

package x.y.service;
 
public interface FooService {
 
 Foo getFoo(String fooName, int age);
}
 
// the attendant implementation (defined in another file of course)
 
public class DefaultFooService implements FooService {
 
 public Foo getFoo(String name, int age) {
 return new Foo(name, age);
 }
}

下一步(无可否认的)是切面。注意实际上profile(..)方法接受多个强类型(strongly-typed)参数,第一个参数是方法调用时要执行的连接点,该参数指明了 profile(..)方法被用作一个环绕通知:

package x.y;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
 
public class SimpleProfiler {
 
 public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
 StopWatch clock = new StopWatch(
 "Profiling for '" + name + "' and '" + age + "'");
 try {
 clock.start(call.toShortString());
 return call.proceed();
 } finally {
 clock.stop();
 System.out.println(clock.prettyPrint());
 }
 }
}

最后,下面是为一个特定的连接点执行上面的通知所必需的XML配置:

<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:aop="http://www.springframework.org/schema/aop"
 xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
 
 <!-- this is the object that will be proxied by Spring's AOP infrastructure -->
 <bean id="fooService" class="x.y.service.DefaultFooService"/>
 
 <!-- this is the actual advice itself -->
 <bean id="profiler" class="x.y.SimpleProfiler"/>
 
 <aop:config>
 <aop:aspect ref="profiler">
 
 <aop:pointcut id="theExecutionOfSomeFooServiceMethod"
 expression="execution(* x.y.service.FooService.getFoo(String,int))
 and args(name, age)"/>
 
 <aop:around pointcut-ref="theExecutionOfSomeFooServiceMethod"
 method="profile"/>
 
 </aop:aspect>
 </aop:config>
 
</beans>

如果使用下面的驱动脚本,我们将在标准输出上得到如下的输出:

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.FooService;
 
public final class Boot {
 
 public static void main(final String[] args) throws Exception {
 BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
 FooService foo = (FooService) ctx.getBean("fooService");
 foo.getFoo("Pengo", 12);
 }
}
StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms % Task name
-----------------------------------------
00000 ? execution(getFoo)
6.3.3.7.通知顺序

当同一个切入点(执行方法)上有多个通知需要执行时,执行顺序规则在 Section6.2.4.7, “通知(Advice)顺序” 已经提及了。 切面的优先级通过切面的支持bean是否实现了Ordered接口来决定。

6.3.4.引入

Intrduction (在AspectJ中成为inter-type声明)允许一个切面声明一个通知对象实现指定接口,并且提供了一个接口实现类来代表这些对象。

在 aop:aspect 内部使用 aop:declare-parents 元素定义Introduction。 该元素用于用来声明所匹配的类型有了一个新的父类型(所以有了这个名字)。 例如,给定接口 UsageTracked,以及这个接口的一个实现类 DefaultUsageTracked,下面声明的切面所有实现service接口的类同时实现 UsageTracked 接口。(比如为了通过JMX暴露statistics。)

<aop:aspect id="usageTrackerAspect" ref="usageTracking">
 
 <aop:declare-parents
  types-matching="com.xzy.myapp.service.*+",
  implement-interface="UsageTracked"
  default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
 
 <aop:before
 pointcut="com.xyz.myapp.SystemArchitecture.businessService()
  and this(usageTracked)"
 method="recordUsage"/>
 
</aop:aspect>

usageTracking bean的支持类可以包含下面的方法:

public void recordUsage(UsageTracked usageTracked) {
 usageTracked.incrementUseCount();
}

欲实现的接口由 implement-interface 属性来指定。 types-matching 属性的值是一个AspectJ类型模式:- 任何匹配类型的bean会实现 UsageTracked 接口。 注意在Before通知的例子中,srevice bean可以用作 UsageTracked 接口的实现。 如果编程形式访问一个bean,你可以这样来写:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

6.3.5.切面实例化模型

Schema-defined切面仅支持一种实例化模型就是singlton模型。其他的实例化模型或许在未来版本中将得到支持。

6.3.6.Advisors

"advisors"这个概念来自Spring1.2对AOP的支持,在AspectJ中是没有等价的概念。 advisor就像一个小的自包含的切面,这个切面只有一个通知。切面自身通过一个bean表示,并且必须实现一个通知接口, 在 Section7.3.2, “Spring里的通知类型” 中我们会讨论相应的接口。Advisors可以很好的利用AspectJ切入点表达式。

Spring 2.0 通过 <aop:advisor> 元素来支持advisor 概念。 你将会发现它大多数情况下会和transactional advice一起使用,transactional advice在Spring 2.0中有自己的命名空间。格式如下:

<aop:config>
 
 <aop:pointcut id="businessService"
 expression="execution(* com.xyz.myapp.service.*.*(..))"/>
 
 <aop:advisor
  pointcut-ref="businessService"
  advice-ref="tx-advice"/>
 
</aop:config>
 
<tx:advice id="tx-advice">
<tx:attributes>
 <tx:method name="*" propagation="REQUIRED"/>
 </tx:attributes>
</tx:advice>

和在上面使用的 pointcut-ref 属性一样,你还可以使用 pointcut 属性来定义一个内联的切入点表达式。

为了定义一个advisord的优先级以便让通知可以有序,使用 order 属性来定义 advisor的值 Ordered 。

6.3.7.例子

让我们来看看在 Section6.2.7, “例子” 提过并发锁失败重试的例子,如果使用schema对这个例子进行重写是什么效果。

因为并发锁的关系,有时候business services可能会失败(例如,死锁失败)。如果重新尝试一下,很有可能就会成功。对于business services来说,重试几次是很正常的(Idempotent操作不需要用户参与,否则会得出矛盾的结论) 我们可能需要透明的重试操作以避免让客户看见 PessimisticLockingFailureException 例外被抛出。很明显,在一个横切多层的情况下,这是非常有必要的,因此通过切面来实现是很理想的。

因为我们想要重试操作,我们会需要使用到环绕通知,这样我们就可以多次调用proceed()方法。下面是简单的切面实现(只是一个schema支持的普通Java 类):

public class ConcurrentOperationExecutor implements Ordered {
 
private static final int DEFAULT_MAX_RETRIES = 2;
 
 private int maxRetries = DEFAULT_MAX_RETRIES;
 private int order = 1;
 
 public void setMaxRetries(int maxRetries) {
 this.maxRetries = maxRetries;
 }
 
public int getOrder() {
 return this.order;
 }
 
public void setOrder(int order) {
 this.order = order;
 }
 
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { 
int numAttempts = 0;
 PessimisticLockingFailureException lockFailureException;
 do {
 numAttempts++;
 try { 
return pjp.proceed();
 }
 catch(PessimisticLockingFailureException ex) {
 lockFailureException = ex;
 }
 }
 while(numAttempts <= this.maxRetries);
 throw lockFailureException;
 }
 
}

请注意切面实现了 Ordered 接口,这样我们就可以把切面的优先级设定为高于事务通知(我们每次重试的时候都想要在一个全新的事务中进行)。 maxRetries 和 order 属性都可以在Spring中配置。 主要的动作在 doConcurrentOperation 这个环绕通知中发生。 请注意这个时候我们所有的 businessService() 方法都会使用这个重试策略。 我们首先会尝试处理,然后如果我们得到一个 PessimisticLockingFailureException 异常,我们只需要简单的重试,直到我们耗尽所有预设的重试次数。

这个类跟我们在@AspectJ的例子中使用的是相同的,只是没有使用注解。

对应的Spring配置如下:

<aop:config>
 
 <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
 
 <aop:pointcut id="idempotentOperation"
 expression="execution(* com.xyz.myapp.service.*.*(..))"/>
 
<aop:around
 pointcut-ref="idempotentOperation"
 method="doConcurrentOperation"/>
 
</aop:aspect>
 
</aop:config>
 
<bean id="concurrentOperationExecutor"
 class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
 <property name="maxRetries" value="3"/>
 <property name="order" value="100"/> 
</bean>

请注意我们现在假设所有的bussiness services都是idempotent。如果不是这样,我们可以改写切面,加上 Idempotent 注解,让它只调用idempotent:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
 // marker annotation
}

并且对service操作的实现进行注解。这样如果你只希望改变切面使得idempotent的操作会尝试多次,你只需要改写切入点表达式,这样只有 @Idempotent 操作会匹配:

 <aop:pointcut id="idempotentOperation"
 expression="execution(* com.xyz.myapp.service.*.*(..)) and
 @annotation(com.xyz.myapp.service.Idempotent)"/>

6.4.AOP声明风格的选择

当你确定切面是实现一个给定需求的最佳方法时,你如何选择是使用Spring AOP还是AspectJ,以及选择 Aspect语言(代码)风格、@AspectJ声明风格或XML风格?这个决定会受到多个因素的影响,包括应用的需求、 开发工具和小组对AOP的精通程度。

6.4.1.Spring AOP还是完全用AspectJ?

做能起作用的最简单的事。Spring AOP比完全使用AspectJ更加简单,因为它不需要引入AspectJ的编译器/织入器到你开发和构建过程中。如果你仅仅需要在Spring bean上通知执行操作,那么Spring AOP是合适的选择。如果你需要通知domain对象或其它没有在Spring容器中 管理的任意对象,那么你需要使用AspectJ。如果你想通知除了简单的方法执行之外的连接点(如:调用连接点、字段get或set的连接点等等), 也需要使用AspectJ。

当使用AspectJ时,你可以选择使用AspectJ语言(也称为“代码风格”)或@AspectJ注解风格。 如果切面在你的设计中扮演一个很大的角色,并且你能在Eclipse中使用AspectJ Development Tools (AJDT), 那么首选AspectJ语言 :- 因为该语言专门被设计用来编写切面,所以会更清晰、更简单。如果你没有使用 Eclipse,或者在你的应用中只有很少的切面并没有作为一个主要的角色,你或许应该考虑使用@AspectJ风格 并在你的IDE中附加一个普通的Java编辑器,并且在你的构建脚本中增加切面织入(链接)的段落。

6.4.2.Spring AOP中使用@AspectJ还是XML?

如果你选择使用Spring AOP,那么你可以选择@AspectJ或者XML风格。总的来说,如果你使用Java 5, 我们建议使用@AspectJ风格。显然如果你不是运行在Java 5上,XML风格是最佳选择。XML和@AspectJ 之间权衡的细节将在下面进行讨论。

XML风格对现有的Spring用户来说更加习惯。它可以使用在任何Java级别中(参考连接点表达式内部的命名连接点,虽然它也需要Java 5) 并且通过纯粹的POJO来支持。当使用AOP作为工具来配置企业服务时(一个好的例子是当你认为连接点表达式是你的配置中的一部分时,你可能想单独更改它)XML会是一个很好的选择。对于XML风格,从你的配置中可以清晰的表明在系统中存在那些切面。

XML风格有两个缺点。第一是它不能完全将需求实现的地方封装到一个位置。DRY原则中说系统中的每一项知识都必须具有单一、无歧义、权威的表示。 当使用XML风格时,如何实现一个需求的知识被分割到支撑类的声明中以及XML配置文件中。当使用@AspectJ风格时就只有一个单独的模块 -切面- 信息被封装了起来。 第二是XML风格同@AspectJ风格所能表达的内容相比有更多的限制:仅仅支持"singleton"切面实例模型,并且不能在XML中组合命名连接点的声明。 例如,在@AspectJ风格中我们可以编写如下的内容:

 @Pointcut(execution(* get*()))
 public void propertyAccess() {}
 
 @Pointcut(execution(org.xyz.Account+ *(..))
 public void operationReturningAnAccount() {}
 
 @Pointcut(propertyAccess() && operationReturningAnAccount())
 public void accountPropertyAccess() {}

在XML风格中能声明开头的两个连接点:

 <aop:pointcut id="propertyAccess" 
expression="execution(* get*())"/>
 
 <aop:pointcut id="operationReturningAnAccount" 
expression="execution(org.xyz.Account+ *(..))"/>

但是不能通过组合这些来定义accountPropertyAccess连接点

@AspectJ风格支持其它的实例模型以及更丰富的连接点组合。它具有将将切面保持为一个模块单元的优点。 还有一个优点就是@AspectJ切面能被Spring AOP和AspectJ两者都理解 - 所以如果稍后你认为你需要AspectJ 的能力去实现附加的需求,那么你非常容易转移到基于AspectJ的途径。总而言之,我们更喜欢@AspectJ风格只要你有切面 去做超出简单的“配置”企业服务之外的事情。

6.5.混合切面类型

我们完全可以混合使用以下几种风格的切面定义:使用自动代理的@AspectJ 风格的切面,schema-defined <aop:aspect> 的切面,和用 <aop:advisor> 声明的advisor,甚至是使用Spring 1.2风格的代理和拦截器。 由于以上几种风格的切面定义的都使用了相同的底层机制,因此可以很好的共存。

6.6.代理机制

Spring AOP部分使用JDK动态代理或者CGLIB来为目标对象创建代理。(建议尽量使用JDK的动态代理)

如果被代理的目标对象实现了至少一个接口,则会使用JDK动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB代理。

如果你希望强制使用CGLIB代理,(例如:希望代理目标对象的所有方法,而不只是实现自接口的方法)那也可以。但是需要考虑以下问题:

· 无法通知(advise)Final 方法,因为他们不能被覆写。

· 你需要将CGLIB 2二进制发行包放在classpath下面,与之相较JDK本身就提供了动态代理

强制使用CGLIB代理需要将 <aop:config> 的 proxy-target-class 属性设为true:

<aop:config proxy-target-class="true">
 
 ...
 
</aop:config>

当需要使用CGLIB代理和@AspectJ自动代理支持,请按照如下的方式设置 <aop:aspectj-autoproxy> 的 proxy-target-class 属性:

<aop:aspectj-autoproxy proxy-target-class="true"/>

6.7.编程方式创建@AspectJ代理

除了在配置文件中使用 <aop:config> 或者 <aop:aspectj-autoproxy> 来声明切面。 同样可以通过编程方式来创建代理通知(advise)目标对象。关于Spring AOP API的详细介绍,请参看下一章。这里我们重点介绍自动创建代理。

org.springframework.aop.aspectj.annotation.AspectJProxyFactory 可以为@AspectJ切面的目标对象创建一个代理。该类的基本用法非常简单,示例如下。请参看Javadoc获取更详细的信息。

// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject); 
 
// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);
 
// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker); 
 
// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();

6.8.在Spring应用中使用AspectJ

到目前为止本章讨论的一直是纯Spring AOP。 在这一节里面我们将介绍如何使用AspectJ compiler/weaver来代替Spring AOP或者作为它的补充,因为有些时候Spring AOP单独提供的功能也许并不能满足你的需要。

Spring提供了一个小巧的AspectJ aspect library (你可以在程序发行版本中单独使用 spring-aspects.jar 文件,并将其加入到classpath下以使用其中的切面)。 Section6.8.1, “在Spring中使用AspectJ来为domain object进行依赖注入” Section6.8.2, “Spring中其他的AspectJ切面” 讨论了该库和如何使用该库。 Section6.8.3, “使用Spring IoC来配置AspectJ的切面” 讨论了如何对通过AspectJ compiler织入的AspectJ切面进行依赖注入。最后Section6.8.4, “在Spring应用中使用AspectJ Load-time weaving(LTW)”介绍了使用AspectJ的Spring应用程序如何装载期织入(load-time weaving)。

6.8.1.在Spring中使用AspectJ来为domain object进行依赖注入

Spring容器对application context中定义的bean进行实例化和配置。 同样也可以通过bean factory来为一个已经存在且已经定义为spring bean的对象应用所包含的配置信息。 spring-aspects.jar中包含了一个annotation-driven的切面,提供了能为任何对象进行依赖注入的能力。这样的支持旨在为 脱离容器管理 创建的对象进行依赖注入。 Domain object经常处于这样的情形:它们可能是通过 new 操作符创建的对象, 也可能是ORM工具查询数据库的返回结果对象。

包 org.springframework.orm.hibernate.support 中的类 DependencyInjectionInterceptorFactoryBean 可以让Spring为Hibernate创建并且配置prototype类型的domain object(使用自动装配或者确切命名的bean原型定义)。 当然,拦截器不支持配置你编程方式创建的对象而非检索数据库返回的对象。 其他framework也会提供类似的技术。仍是那句话,Be Pragramatic选择能满足你需求的方法中最简单的那个。 请注意前面提及的类 没有 随Spring发行包一起发布。 如果你希望使用该类,需要从Spring CVS Respository上下载并且自行编译。 你可以在Spring CVS respository下的 'sandbox' 目录下找到该文件。

@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,这样就可以按类型或者按名字自动装配了。

最后,你可以设置 dependencyCheck 属性,通过设置,Spring对新创建和配置的对象的对象引用进行校验 (例如:@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true) )。 如果这个属性被设为true,Spring会在配置结束后校验除了primitives和collections类型的所有的属性是否都被赋值了。

仅仅使用注解并没有做任何事情。但当注解存在时,spring-aspects.jar中的 AnnotationBeanConfigurerAspect 就起作用了。 实质上切面做了这些:当初始化一个有 @Configurable 注解的新对象时,Spring按照注解中的属性来配置这个新创建的对象。 要实现上述的操作,已注解的类型必须由AspectJ weaver来织入 - 你可以使用一个 build-time ant/maven任务来完成 (参见AspectJ Development Environment Guide) 或者使用load-time weaving(参见 Section6.8.4, “在Spring应用中使用AspectJ Load-time weaving(LTW)”)。

AnnotationBeanConfigurerAspect 本身也需要Spring来配置(获得bean factory的引用,使用bean factory配置新的对象)。 为此Spring AOP命名空间定义了一个非常方便的标签。如下所示,可以很简单的在application context配置文件包含这个标签中。

<aop:spring-configured/>

如果你使用DTD代替Schema,对应的定义如下:

<bean
 class="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"
 factory-method="aspectOf"/>

在切面配置完成 之前 创建的@Configurable对象实例会导致在log中留下一个warning,并且任何对于该对象的配置都不会生效。 举一个例子,一个Spring管理配置的bean在被Spring初始化的时候创建了一个domain object。 对于这样的情况,你需要定义bean属性中的"depends-on"属性来手动指定该bean依赖于configuration切面。

<bean id="myService"
 class="com.xzy.myapp.service.MyService"
 depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">
 
 ...
 
</bean>
6.8.1.1. @Configurable object的单元测试

提供 @Configurable 支持的一个目的就是使得domain object的单元测试可以独立进行,不需要通过硬编码查找各种倚赖关系。如果 @Configurable 类型没有通过AspectJ织入, 则在单元测试过程中注解不会起到任何作用,测试中你可以简单的为对象的mock或者stub属性赋值,并且和正常情况一样的去使用该对象。 如果 @Configurable 类型通过AspectJ织入, 我们依然可以脱离容器进行单元测试,不过每次创建一个新的 @Configurable 对象时都会看到一个warning标示该对象不受Spring管理配置。

6.8.1.2.多application context情况下的处理

AnnotationBeanConfigurerAspect 通过一个AspectJ singleton切面来实现对 @Configurable 的支持。 一个singleton切面的作用域和一个静态变量的作用域是一样的,例如,对于每一个classloader有一个切面来定义类型。 这就意味着如果你在一个classloader层次结构中定义了多个application context的时候就需要考虑在哪里定义 <aop:spring-configured/> bean和在哪个classpath下放置Srping-aspects.jar。

考虑一下典型的Spring web项目,一般都是由一个父application context定义大部分business service和所需要的其他资源,然后每一个servlet拥有一个子application context定义。 所有这些context共存于同一个classloader hierarchy下,因此对于全体context,AnnotationBeanConfigurerAspect 仅可以维护一个引用。在这样的情况下,我们推荐在父application context中定义 <aop:spring-configured/> bean: 这里所定义的service可能是你希望注入domain object的。这样做的结果是你不能为子application context中使用@Configurable的domain object配置bean引用(可能你也根本就不希望那么做!)。

当在一个容器中部署多个web-app的时候,请确保每一个web-application使用自己的classloader来加载spring-aspects.jar中的类(例如将spring-aspects.jar放在WEB-INF/lib目录下)。 如果spring-aspects.jar被放在了容器的classpath下(因此也被父classloader加载),则所有的web application将共享一个aspect实例,这可能并不是你所想要的。

6.8.2.Spring中其他的AspectJ切面

除了 @Configurable 支持,spring-aspects.jar包含了一个AspectJ切面可以用来为那些使用了 @Transactional annotation 的类型和方法驱动Spring事务管理(参见 Chapter9, 事务管理)。 提供这个的主要目的是有些用户希望脱离Spring容器使用Spring的事务管理。

解析@Transactional annotations的切面是AnnotationTransactionAspect。 当使用这个切面时,你必须注解这个实现类(和/或这个类中的方法),而不是这个类实现的接口(如果有)。 AspectJ允许在接口上注解的Java规则 不被继承

类之上的一个@Transactional注解为该类中任何public操作的执行指定了默认的事务语义。

类内部方法上的一个@Transactional注解会覆盖类注解(如果存在)所给定的默认的事务语义。具有public、protected和default修饰符的方法都可以被注解。直接注解protected和default方法是让这个操作的执行 获得事务划分的唯一途径。

对于AspectJ程序员,希望使用Spring管理配置和事务管理支持,不过他们不想(或者不能)使用注解,spring-aspects.jar也包含了一些抽象切面供你继承来提供你自己的切入点定义。参见 AbstractBeanConfigurerAspect 和 AbstractTransactionAspect 的Javadoc获取更多信息。 作为一个例子,下面的代码片断展示了如何编写一个切面,然后通过bean原型定义中和类全名匹配的来配置domian object中所有的实例:

public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {
 
 public DomainObjectConfiguration() {
 setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
 }
 
 // the creation of a new bean (any object in the domain model)
 protected pointcut beanCreation(Object beanInstance) :
 initialization(new(..)) &&
 SystemArchitecture.inDomainModel() && 
this(beanInstance);
   
}

6.8.3.使用Spring IoC来配置AspectJ的切面

当在Spring application中使用AspectJ的时候,很自然的会想到用Spring来管理这些切面。 AspectJ runtime自身负责切面的创建,这意味着通过Spring来管理AspectJ 创建切面依赖于切面所使用的AspectJ instantiation model(per-clause)。

大多数AspectJ切面都是 singleton 切面。 管理这些切面非常容易,和通常一样创建一个bean定义引用该切面类型就可以了,并且在bean定义中包含 'factory-method="aspectOf"' 这个属性。 这确保Spring从AspectJ获取切面实例而不是尝试自己去创建该实例。示例如下:

<bean id="profiler" class="com.xyz.profiler.Profiler"
  factory-method="aspectOf">
 <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>

对于non-singleton的切面,最简单的配置管理方法是定义一个bean原型定义并且使用@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>
 <include name="thisBean"/>
 <include name="thatBean"/>
</aop:aspectj-autoproxy>

6.8.4.在Spring应用中使用AspectJ Load-time weaving(LTW)

Load-time weaving(LTW)指的是在虚拟机载入字节码文件时动态织入AspectJ切面。关于LTW的详细信息,请查看 LTW section of the AspectJ Development Environment Guide。在这里我们重点来看一下Java 5环境下Spring应用如何配置LTW。

LTW需要定义一个 aop.xml,并将其置于META-INF目录。 AspectJ会自动查找所有可见的classpath下的META-INF/aop.xml文件,并且通过定义内容的合集来配置自身。

一个基本的META-INF/aop.xml文件应该如下所示:

<!DOCTYPE aspectj PUBLIC
 "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
 
<aspectj>
 <weaver>
  <include within="com.xyz.myapp..*"/>
 </weaver>
</aspectj>

'<include/>'的内容告诉AspectJ那些类型需要被纳入织入过程。使用包名前缀并加上"..*"(表示该子包中的所有类型)是一个不错的默认设定。 使用include元素是非常重要的,不然AspectJ会查找每一个应用里面用到的类型(包括Spring的库和其它许多相关库)。通常你并不希望织入这些类型并且不愿意承担AspectJ尝试去匹配的开销。

希望在日志中记录LTW的活动,请添加如下选项:

<!DOCTYPE aspectj PUBLIC 
"-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd"> 
 
<aspectj> 
<weaver 
 options="-showWeaveInfo
 -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">
  <include within="com.xyz.myapp..*"/>
 </weaver>
</aspectj>

最后,如果希望精确的控制使用哪些切面,可以使用 aspects。 默认情况下所有定义的切面都将被织入(spring-aspects.jar包含了META-INF/aop.xml,定义了配置管理和事务管理切面)。 如果你在使用spring-aspects.jar,但是只希望使用配制管理切面而不需要事务管理的话,你可以像下面那样定义:

<!DOCTYPE aspectj PUBLIC
 "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
 
<aspectj>
 <aspects>
  <include within="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"/>
 </aspects>
 <weaver
 options="-showWeaveInfo -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">
 <include within="com.xyz.myapp..*"/>
 </weaver>
</aspectj>

在Java 5平台下,LTW可以通过虚拟机的参数来启用。

-javaagent:<path-to-ajlibs>/aspectjweaver.jar

6.9.其它资源

更多关于AspectJ的信息可以查看 AspectJ home page

Eclipse AspectJ by Adrian Colyer et. al. (Addison-Wesley, 2005)全面介绍并提供了AspectJ语言参考。

AspectJ in Action by Ramnivas Laddad (Manning, 2003)是一本非常出色介绍AOP的书籍;全书着重介绍了AspectJ,但也对一些通用的AOP场景进行了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值