用 Spring 2.0 和 AspectJ 简化企业应用程序

阅读数:11606 2007 年 10 月 3 日

话题:Java语言 & 开发架构

Spring:简单强大

Spring 的目标是使企业应用程序开发尽可能地简单和高效。这一理论的实例可以从 Spring 的 JDBC、ORM、JMX、依赖注入等方法,以及企业应用程序开发的其他许多重要领域中见到。Spring 还区分了使事情简单化和过分单纯化之间的差异。最不可思议的是同时提供了简单化和强大的功能。企业应用程序中复杂性的一个根源来自影响应用程序多个部分的特性和需求的实现。相关于这些特性的代码最终散布在应用程序代码中,使得它更难以添加、维护和理解。Spring 2.0 使得以模块化的方式实现这些特性变得更加简单,极大地简化了整体的应用程序代码,并且有时使得在实现没有它的情况下十分痛苦的编码需求变得易如反掌。

事务管理是影响应用程序多个部分的一个特性实例:一般来说所有的操作都在服务层。在 Spring 中解决这种需求的方式是通过使用 AOP。Spring 2.0 在它对 AOP 的支持中提供了一个明显的简化,同时还提供了比 Spring 1.x 所提供的更多富有表现力的功能。这些改善之处主要来自两个主要的领域:通过使用 XML schema 极大地简化了配置,以及与 AspectJ 的整合带来了更好的富有表现力的功能和更简单的 advice 模型。

在本文中,我将首先介绍在典型的企业应用程序中,Spring AOP 和 AspectJ 适用于什么地方,之后介绍在 2.0 中新的 Spring AOP 支持。大部分篇幅用来讲解企业应用程序中 AOP 的采用路线图,通过大量可以只用 AOP 实现的特性实例,但是用任何其他的方法进行实现都将非常困难。

简化企业应用程序

典型的企业应用程序——比如一个 Web 应用程序——由许多层构成。一个包含视图和控制器的 Web 层,一个表现系统业务接口的服务层,一个负责保存和获取持久化领域对象的数据访问或者存储层,与所有这些层共事的,还有一个核心业务逻辑所在的领域模型。

Web 层、服务层和数据访问层有着许多相同的重要特征:它们应该尽可能地瘦,它们不应该包含业务逻辑,并且它们一般通过 Spring 组装在一起。在这些层中,Spring 负责创建对象和配置。领域模型则有些不同:领域对象由程序员利用新的操作器创建(或者利用从数据库中获取的 ORM 工具进行扩建)。领域对象有许多唯一的实例,它们(可以)有丰富的行为。

服务层可以包含特定于应用程序用例的逻辑,但是所有领域相关的逻辑都应该放在领域模型本身里面。

服务层一般是使用声明式企业服务(例如事务)的地方。声明式的企业服务,例如事务和安全是影响应用程序中多个点的很好的需求实例。事实上,即使你想让(比如)事务划分只在单个地方,将这项功能与你的应用程序逻辑分开,使得代码更加简单,避免不必要的耦合,这也仍然很好。

由于服务对象是 Spring 管理的 bean,Spring AOP 天生适合于在这个层中处理需求。事实上,任何人在使用 Spring 的声明式事务支持时,就已经是在使用 Spring AOP 了,无论他们是否意识到这一点。Spring AOP 很成熟,得到了广泛的应用。它非常适合于 Web、服务和数据访问层中受 Spring 管理的 bean,只要你的需求可以通过 advice bean 方法执行得到处理(且这些层的许多用例都属于这一类)。

当提到影响你领域模型中多个点的需求时,你应用程序的最重要部分——Spring AOP——的帮助就小多了。你可以编程式地使用 Spring AOP,但是这样会很难使用,并且还要你自己负责创建代理和管理同一性。AspectJ 天生适合于实现影响领域对象的特性。AspectJ 方面不需要任何特殊的代理创建,并且可以很恰当地通知运行时在你的应用程序代码中,或者通过你可能使用的框架所创建的对象。当你想要模块化影响你应用程序的所有不同层的行为,或者模块化性能以任何方式感知的行为时,AspectJ 也是一种非常好的解决方案。

因此,我们最想要的是一种一致的 Spring AOP 和 AspectJ 方法,以便我们可以很容易地一起使用这两种工具,以便如果需求发生变化,你用(比如)Spring AOP 开发的能力就可以转移到 AspectJ 上。无论我们正在使用哪种组合,我们仍然喜欢依赖注入和 Spring 所提供的配置的所有益处。Spring 2.0 中新的 AOP 支持正好带来了这一点。

底层的技术:AspectJ 和 Spring AOP 简介

AOP 使得实现在应用程序中影响多个点的特性变得更加简单。这主要因为 AOP 提供了对名为通知(advice)的这个东西的支持。通知不同于必须显式调用的方法,每当发生匹配的触发事件时,它就自动地执行。继续事务主题,触发事件是服务层中一个方法的执行,并且通知逻辑提供所需的事务划分。用 AOP 的话来说,触发事件被称作连接点(join point),而切入点表达式(pointcut expression)则用来选择通知要在那里运行的连接点。这个简单的倒置意味着不用将调用散布到你全部应用程序代码中的事务管理器,而是只要编写一个切入点表达式,定义你需要事务管理器在什么地方完成某事的所有点,并将它与适当的通知关联起来。AspectJ 和 Spring AOP 提供对这个模型的支持,事实上,它们有着完全相同的切入点表达语言。

在接下来的讨论中,注意 Spring 和 AspectJ 保持为独立的工程,这很重要。Spring 只使用反射和由 AspectJ 5 作为一个库所暴露的工具 API。Spring 2.0 仍然是一个运行时基于代理的框架,且 AspectJ 织入器(weaver)不用于 Spring 方面。

我相信你们中大多数人都知道,AspectJ 是一种包含完整编译器的语言(构建为 Eclipse JDT Java 编译器的一个扩展),对离线或者在运行时将(与)二进制的 class 文件(链接的方面)作为类织入的支持,被加载到了虚拟机中。AspectJ 的最新发布版本是 AspectJ 5,它为 Java 5 语言提供完整的支持。

AspectJ 5 也引入了方面声明的第二种风格,我们称之为“@AspectJ”,它允许你将一个方面编写为一个包含注解的 Java 类。这种方面可以通过一般的 Java 5 编译器进行编译。例如,传统的“HelloWorld”方面在 AspectJ 编程语言中看起来像这样:

public aspect HelloFromAspectJ {

pointcut mainMethod() : execution(* main(..));

after() returning : mainMethod() {



System.out.println("Hello from AspectJ!);

}

}



与传统的 HelloWorld 类共同编译这个方面,当你运行应用程序时,会看到这样的输出:

Hello World!

Hello from AspectJ!

我们可以用 @Aspect 风格编写相同的方面如下:

@Aspect

public class HelloFromAspectJ {

@Pointcut("execution(* main(..))")



public void mainMethod() {}

@AfterReturning("mainMethod()")



public void sayHello() {

System.out.println("Hello from AspectJ!");

}

}



就本文而言,AspectJ 5 中另一项重要的新特性是一个完全 AspectJ 感知的反射 API(你可以在运行时为它的通知和切入点成员等等请求一个方面),和让第三方使用 AspectJ 的切入点解析和匹配引擎的工具 API。这些 API 的第一大用户,就像你很快会见到的,是 Spring AOP。

与 AspectJ 相反,Spring AOP 是一个基于代理的运行时框架。在使用 Spring AOP 时,并没有特殊的工具或者构建需求,因而 Spring AOP 是一种很容易开始的方法。作为一种基于代理的框架,它既有优点也有缺点。除了已经提到过的容易使用的因素之外,基于代理的框架还能够独立地通知相同类型的不同实例。将这一点与 AspectJ 基于类型的语义相比,在这里,类型的每一个实例都有着相同的行为。对于像 Spring 这样的框架而言,能够独立地通知独立的对象(Spring beans)是一个重要的必要条件。另一方面,Spring AOP 只支持 AspectJ 功能的一个子集:有可能在 Spring beans 中通知方法的执行,但是其他没什么。

基于代理的框架一般会有同一性的问题:有两个对象(代理和目标)都表示应用程序中的同一个实体。必须始终小心地传递适当的引用,确保给实例化过的任何新的目标对象创建代理。Spring AOP 通过管理 bean 实例化(以便代理可以被透明地创建)和通过依赖注入(以便 Spring 始终可以注入适当的引用),巧妙地解决了这些问题。

Spring 2.0 中新的 AOP 支持

2.0 中的 Spring AOP 可以完全向后与 Spring 1.x 应用程序和配置兼容。它还提供了比 Spring 1.x 更简单且更强大的配置。新的 AOP 支持是基于 schema 的,因此在你的 Spring beans 配置文件中将需要相关的命名空间和 schema 定位属性。它看起来像这样:

<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.xsd

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

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

...

</beans>



与使用 DTD 时所需要的更简单的 xml 配置相比,那么目前为止我们还没有超越——但这是标准的 xml 配置,并且可以在你的 IDE 中的一个模板里创建,并且只在每当你需要创建一个 Spring 配置时才被重用。当我们开始将一些内容添加到配置中时,你会领略到这一好处。

Spring 2.0 默认使用 AspectJ 切入点语言(受执行连接点种类的限制)。如果它看到一个 AspectJ 切入点表达式,它就调出 AspectJ 对它进行解析和匹配。这意味着你用 Spring AOP 编写的任何切入点表达式都将以与 AspectJ 完全相同的方式进行工作。此外,Spring 实际上能理解 @AspectJ 方面,因此有可能共用 Spring 和 AspectJ 之间完整的方面定义。激活这项功能很容易,只要将 <aop:aspectj-autoproxy> 元素包括在你的配置中。如果 AspectJ 自动代理以这种方式激活,那么在你的应用程序上下文中定义的、包含 @AspectJ 方面的任何 bean,都将被 Spring AOP 视为一个方面,并将相应地通知上下文中的 bean。

下面是当你以这种方式使用 Spring AOP 时的 Hello World 程序。首先,应用程序上下文文件中 bean 元素的内容:

	<bean id="helloService"

class="org.aspectprogrammer.hello.spring.HelloService"/>

<aop:aspectj-autoproxy/>

<bean id="helloFromAspectJ"

class="org.aspectprogrammer.hello.aspectj.HelloFromAspectJ"/>

HelloService 是一个简单的 Java 类:

public class HelloService {

public void main() {



System.out.println("Hello World!");

}

}



HelloFromAspectJ 与你在本文前面见过的被注解的 Java 类(@AspectJ 方面)完全相同。以下是启动 Spring 容器的一个小主类,获得一个对 helloService bean 的引用,并在它上面调用’main’方法:

public class SpringBoot {

public static void main(String[] args) {



ApplicationContext context = new ClassPathXmlApplicationContext(

"org/aspectprogrammer/hello/spring/application-context.xml");

HelloService service = (HelloService) context.getBean("helloService");

service.main();

}

}



运行这个程序产生下面的输出:

Hello World!

Hello from AspectJ!

记住,这仍然是 Spring AOP(我们根本没有在使用 AspectJ 编译器或者织入器),但它是提供关于 @AspectJ 方面的反射信息和解析并匹配代表 Spring 的切入点的 AspectJ。

Spring 2.0 还支持用一个简单的 POJO 支持的方面声明的一种 xml 形式(不需要任何注解)。xml 形式也使用相同的 AspectJ 切入点语言子集,并支持相同的五种 AspectJ 通知类型(前置通知(before advice)、后置通知(after returning advice)、异常通知(after throwing advice)、后通知(after [finally] advice)和 环绕通知(around advice))。

下面是使用一个基于 XML 的方面声明的 hello world 应用程序:

   <bean id="helloService"

class="org.aspectprogrammer.hello.spring.HelloService"/>

<aop:config>



<aop:aspect ref="helloFromSpringAOP">

<aop:pointcut id="mainMethod" expression="execution(* main(..))"/>

<aop:after-returning pointcut-ref="mainMethod" method="sayHello"/>

</aop:aspect>



</aop:config>

<bean id="helloFromSpringAOP"



class="org.aspectprogrammer.hello.spring.HelloAspect"/>

aop 命名空间中的元素可以用来声明方面、切入点和通知,有着与它们的 AspectJ 和 @AspectJ 等效物完全相同的语义。“aspect”元素引用 Spring bean(完全由 Spring 配置和实例化),并且每个通知元素都在该 bean 中指定将被调用来执行通知的方法。在这个例子中,HelloAspect 类只是:

public class HelloAspect {

public void sayHello() {



System.out.println("Hello from Spring AOP!");

}

}



运行程序将产生熟悉的输出:

Hello World!

Hello from Spring AOP!

如果你还没有编写过这样的程序,就下载 Spring 2.0,亲自尝试一下,这可是个好主意。

我不想把本文变成是关于 Spring AOP 的一个完全的教程,而是想要加紧看一些可以有效地以这种方式实现的特性实例。我将只是指出,传递 Spring 从使用 AspectJ 切入点语言中获得的其中某个东西,是编写静态类型的通知(声明它们真正需要的那些参数的方法)的能力,与始终使用非类型的 Object 数组相反——这使得通知方法更容易编写。

采用路线图

理论说得够多了……让我们看一下你在企业应用程序中实际上如何以及为什么要使用 AOP 的一些例子。开始 AOP,并不一定是一种肯定一切或者否定一切的爆炸性方法。采用可以分阶段进行,每个阶段都为增加的技术暴露回报以更多的益处。

建议的采用路线图是只开始使用 Spring 提供的开箱即用的方面(例如事务管理)。许多 Spring 用户将已经在这么做了,但多半不太欣赏 AOP 被“背地里”使用着。根据这一点,你可以实现在使用 Spring AOP 的 Web、服务和数据访问层中可能会有的任何定制横切需求。

实现影响领域模型的特性必需使用 AspectJ。你听到这句话时可能感到惊讶:有大量的 AspectJ 方面对于你在开发时都非常有帮助,而且不影响在产品中以任何方式运行的应用程序。这些方面可以增加很多价值,并且采用风险非常小,因此建议用它们开始 AspectJ。根据这一点,你可以选择通过 AspectJ 实现“基础结构的”需求——典型的实例为剖析(profiling)、跟踪(tracing)、错误处理(error-handling)等等。随着你越来越习惯于 AspectJ 和所配套的工具,最终你可以用方面在领域逻辑自身中开始实现功能。

关于 AOP 采用路线图的其他信息,请见《Eclipse AspectJ》一书中的第 11 章,或者 developerWorks AOP@Work 系列中“Next steps with aspects”一文。这两个资源都专门关注 AspectJ,而我在这里则正在讨论同时使用 Spring 和 AspectJ。

让我们依次看一下这每一种采用阶段。

当在一个工程中使用 AOP 时,首先要做的最有意义的事是定义一组切入点表达式,描述你应用程序中的不同模块或者层。这些切入点表达式在采用的所有不同阶段中都将很有帮助,并且定义一次将减少重复,改善代码的清晰度。如果我们用 @AspectJ 符号编写这些切入点,它们就可以通过任何常规的 Java 5 编译器进行编译。利用一般的 AspectJ 语言关键字也可能编写相同的东西,用 ajc 编译源文件,并将生成的.class 文件添加到 classpath 中。我将用 @AspectJ 作为开始 Spring AOP 的两种方法中更为容易的那一种。许多读者将会熟悉 Spring 所携带的“jpetstore”范例应用程序。我已经稍微重写了这个应用程序,给它增加了一些方面(本文稍后会讨论到)。以下是在 pet store 中捕捉主要层和模块的“SystemArchitecture”方面的开头部分:

@Aspect

public class SystemArchitecture {

/**



* we're in the pet store application if we're within any

* of the pet store packages

*/

@Pointcut("within(org.springframework.samples.jpetstore..*)")

public void inPetStore() {}

// modules



// ===========

@Pointcut("within(org.springframework.samples.jpetstore.dao..*)")



public void inDataAccessLayer() {}

@Pointcut("within(org.springframework.samples.jpetstore.domain.*)")



public void inDomainModel() {}

@Pointcut("within(org.springframework.samples.jpetstore.service..*)")



public void inServiceLayer() {}

@Pointcut("within(org.springframework.samples.jpetstore.web..*)")



public void inWebLayer() {}

@Pointcut("within(org.springframework.samples.jpetstore.remote..*)")



public void inRemotingLayer() {}

@Pointcut("within(org.springframework.samples.jpetstore.validation..*)")



public void inValidationModule() {}

// module operations



// ==================

@Pointcut("execution(* org.springframework.samples.jpetstore.dao.*.*(..))")



public void doaOperation() {}

@Pointcut("execution(* org.springframework.samples.jpetstore.service.*.*(..))")



public void businessService() {}

@Pointcut("execution(public * org.springframework.samples.jpetstore.validation.*.*(..))")



public void validation() {}

}



既然我们已经有了谈论应用程序(“inServiceLayer”、“businessOperation”等等)的术语,让我们用它来做一些有意义的事情吧。

使用开箱即用的 Spring 方面

advisor是 Spring 1.x 遗留下来的一个 Spring 概念,它包含了一个非常小的方面,带有单独的一条通知,和关联的切入点表达式。对于事务划分而言,advisor 就是我们所需要的一切。典型的事务需求为:服务层中的所有操作都要利用(几个)底层资源管理器的默认隔离级别在一个事务(REQUIRED 语义)中执行。此外,一些操作可以被标识为“只读”事务——这一知识可以给这类事务带来明显的性能改善。jpestore advisor 声明如下:

<!--

all aspect and advisor declarations are gathered inside an

aop:config element

-->

<aop:config>

<aop:advisor



pointcut="org.springframework.samples.jpetstore.SystemArchitecture.businessService()"

advice-ref="txAdvice"/>

</aop:config>



这个声明仅仅意味着:当执行一个“businessService”时,我们需要运行被“txAdvice”引用的通知。“BusinessService”切入点在我们前面讨论过的 org.springframework.samples.jpetstore.SystemArchitecture 方面中定义。它与在服务接口中定义的任何操作的执行相匹配。由于事务通知本身可能需要相当多的配置,因此 Spring 在 tx 命名空间中提供了 tx:advice 元素,使得这项工作变得更加简单和清晰。这就是给 jpetstore 应用程序的“txAdvice”定义:

<!--

Transaction advice definition, based on method name patterns.

Defaults to PROPAGATION_REQUIRED for all methods whose name starts with

"insert" or "update", and to PROPAGATION_REQUIRED with read-only hint

for all other methods.

-->

<tx:advice id="txAdvice">



<tx:attributes>

<tx:method name="insert*"/>

<tx:method name="update*"/>

<tx:method name="*" read-only="true"/>

</tx:attributes>



</tx:advice>

还有一种更加简单的方法来配置使用注解的事务。在使用 @Transactional 注解时,你唯一需要的 XML 是:

<!--

Tell Spring to apply transaction advice based on the presence of

the @Transactional annotation on Spring bean types.

-->

<tx:annotation-driven/>

使用注解方法时,PetService 实现要做如下注解:

/*

* all operations have TX_REQUIRED, default isolation level,

* read-write transaction semantics by default

*/

@Transactional

public class PetStoreImpl implements PetStoreFacade, OrderService {

...

/**



* override defaults on a per-method basis

*/

@Transactional(readOnly=true)

public Account getAccount(String username) {

return this.accountDao.getAccount(username);

}

...

}



简化 Web、服务和数据访问层

Spring AOP 可以用来简化 Web、服务和数据访问层。在本节中,我们要看两个实例:一个取自数据访问层,一个取自服务层。

假设你已经用 Hibernate 3 而不是用 Spring HibernateTemplate 支持类实现了你的数据访问层。你现在准备开始在应用程序中使用 Spring,想要在服务层中利用 Spring 的细粒度 DataAccessException 层次结构。Spring 的 HibernateTemplate 将自动为你把 HibernateExceptions 转换成 DataAccessExceptions,但是由于现阶段你已经有一个非常满意的数据层实现,因此并不想马上用 Spring 支持类对它进行重写。这意味着你需要自己实现异常转换。这个需求声明起来很简单:

从数据访问层中抛出任何 HibernateException 之后,在将它递给调用者之前将它转换成一个 DataAccessException。

利用 AOP,实现几乎与需求声明一样简单。没有AOP 时实现这个需求是件非常令人头痛的事。这就是“myapp”的 HibernateExceptionTranslator 方面:

@Aspect

public class HibernateExceptionTranslator {

private HibernateTemplate hibernateTemplate;

public void setHibernateTemplate(HibernateTemplate aTemplate) {



this.hibernateTemplate = aTemplate;

}

@AfterThrowing(



throwing="hibernateEx",

pointcut="org.aspectprogrammer.myapp.SystemArchitecture.dataAccessOperation()"

)



public void rethrowAsDataAccessException(HibernateException hibernateEx) {

throw this.hibernateTemplate

.convertHibernateAccessException(hibernateEx);

}

}



方面需要一个 HibernateTemplate,以便执行转换——我们要用依赖注入对它进行配置,就像任何其他的 Spring bean 一样。通知声明应该有望非常容易地理解为需求声明的一个直接转换:“@AfterThrowing 从 dataAccessOperation() 操作中抛出一个 HibernateException (hibernateEx) ,并重新抛出 rethrowAsDataAccessException”。简单有力!

我们现在可以用 ajc(AspectJ 编译器)构建应用程序,这样我们就完事了。但是这里不需要使用 ajc,因为 Spring AOP 也能识别 @AspectJ 方面。

在应用程序上下文文件中,我们需要两个配置。首先我们要告诉 Spring,包含 @AspectJ 方面的类型的任何 bean 都应该用来配置 Spring AOP 代理。这是通过在应用程序上下文配置文件中的任何位置声明下列元素来实现的一个一次性配置:

<aop:aspectj-autoproxy>

然后我们需要声明异常转换 bean,并对它进行配置,就像对待任何一般的 Spring bean 一样(这里并没有任何特定于 AOP 的东西):

<bean id="hibernateExceptionTranslator"

class="org.aspectprogrammer.myapp.dao.hibernate.HibernateExceptionTranslator">

<property name="hibernateTemplate">

<bean class="org.springframework.orm.hibernate3.HibernateTemplate">

<constructor-arg index="0" ref="sessionFactory" />



</bean>

</property>

</bean>

仅仅因为 bean 的类(HibernateExceptionTranslator)是一个 @AspectJ 方面,就足以配置 Spring AOP 了。

为了完整起见,我们也看一下如何用方面声明的 xml 形式来完成这项工作(例如对于在 JDK 1.4 下进行工作的)。hibernateExceptionTranslator 的 bean 定义与上面所述的一样。类本身不再被注解,但是它剩下的部分也完全相同:

public class HibernateExceptionTranslator {

private HibernateTemplate hibernateTemplate;

public void setHibernateTemplate(HibernateTemplate aTemplate) {



this.hibernateTemplate = aTemplate;

}

public void rethrowAsDataAccessException(HibernateException hibernateEx) {



throw this.hibernateTemplate

.convertHibernateAccessException(hibernateEx);

}

}



由于这不再是一个 @AspectJ 方面,我们无法使用 aspectj-autoproxy 元素,而是用 XML 定义该方面:

<aop:config>

<aop:aspect ref="hibernateExceptionTranslator">



<aop:after-throwing

throwing="hibernateEx"

pointcut="org.aspectprogrammer.myapp.SystemArchitecture.dataAccessOperation()"



method="rethrowAsDataAccessException"/>

</aop:aspect>

</aop:config>



这看起来与前一个版本一样:after-throwing 从 dataAccessOperation 操作中抛出 hibernateEx,并且重新抛出 rethrowAsDataAccessException。注意 aop:aspect 元素的“ref”属性,它引用了我们前面定义的 hibernateExceptionTranslator bean。这是 rethrowAsDataAccessException 方法将要在那里被调用的 bean 实例,而 hibernateEx 则是在该方法中声明的参数名(这个例子中的唯一参数)。就是这样。我们已经实现了需求(两次!)。利用 @AspectJ 风格,我们有 15 个非空的代码行,和一行 XML。这足以为我们在整个数据访问层中提供一致、正确的行为,但是它可能很大。

这个特殊方面的一大好处在于,如果你以后想要将数据层移植到一个基于利用 Hibernate 的实现、或者任何其他 JPA 实现的 JPA(EJB 3 持久化),你的服务层将不会受到影响,并且可以继续使用 DataAccessExceptions(Spring 将为 JPA 提供模板和异常转换,就像对其他的 ORM 实现所做的一样)。

既然我们可以在服务层中使用细粒度的 DataAccessExceptions 了,就可以利用这一点做些有意义的事情。让我们在将失败传递给客户端之前,实现由于并发失败而失败的任何等幂服务操作都将被透明地重试可设定次数的横切需求。

以下是完成这项工作的一个方面:

@Aspect

public class ConcurrentOperationExecutor implements Ordered {

private static final int DEFAULT_MAX_RETRIES = 2;



private int maxRetries = DEFAULT_MAX_RETRIES;

private int order = 1;

private boolean retryOnOptimisticLockFailure = false;

/**



* configurable number of retries

*/

public void setMaxRetries(int maxRetries) {

this.maxRetries = maxRetries;

}

/**



* Whether or not optimistic lock failures should also be retried.

* Default is not to retry transactions that fail due to optimistic

* locking in case we overwrite another user's work.

*/

public void setRetryOnOptimisticLockFailure(boolean retry) {

this.retryOnOptimisticLockFailure = retry;

}

/**



* implementing the Ordered interface enables us to specify when

* this aspect should run with respect to other aspects such as

* transaction management. We give it the highest precedence

* (1) which means that the retry logic will wrap the transaction

* logic - we need a fresh transaction each time.

*/

public int getOrder() {

return this.order;

}

public void setOrder(int order) {



this.order = order;

}

/**



* For now, just assume that all business services are idempotent

*/

@Pointcut("org.aspectprogrammer.myapp.SystemArchitecture.businessService()")

public void idempotentOperation() {}

@Around("idempotentOperation()")



public Object doConcurrentOperation(ProceedingJoinPoint pjp)

throws Throwable {

int numAttempts = 0;

ConcurrencyFailureException failureException;

do {

try {

return pjp.proceed();

}

catch(OptimisticLockingFailureException ex) {

if (!this.retryOnOptimisticLockFailure) {

throw ex;

}

else {

failureException = ex;

}

}

catch(ConcurrencyFailureException ex) {

failureException = ex;

}

}

while(numAttempts++ < this.maxRetries);

throw lockFailureException;

}

}



这个方面还是可以被 Spring AOP 或者 AspectJ 使用,这一点不变。around advice (doConcurrentOperation) 采用了类型 ProceedingJoinPoint 的一个特殊参数。当 proceed 在这个对象中被调用时,无论“around”什么样的通知(在这个例子中为服务操作)都将执行。如果你去掉注释和样板 getters-and-setters,这个方面的业务端仍然只有 32 行代码。由于我们在配置文件中已经有 aspectj-autoproxy 元素,我们需要增加的就只是一个简单的 bean 定义了:

<bean id="concurrentOperationExecutor"

class="org.aspectprogrammer.myapp.service.impl.ConcurrentOperationExecutor">

<property name="maxRetries" value="3"/>

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



</bean>

如果服务层中并非所有的操作都是等幂的,该怎么办?我们如何判断等幂的操作呢?这就是切入点语言的威力开始显现之处。我们已经有一个表示等幂操作的概念的抽象:

  @Pointcut("org.aspectprogrammer.myapp.SystemArchitecture.businessService()")

public void idempotentOperation() {}

如果我们想要改变构成表示等幂操作的东西,我们所要做的就是改变切入点。例如,我们可以给等幂操作定义一个标识注解:@Idempotent。我们可以非常简单地将切入点表达式改为只与包含 Idempotent 注解的业务服务相匹配:

  @Pointcut(

"org.aspectprogrammer.myapp.SystemArchitecture.businessService() &&

@annotation(org.aspectprogrammer.myapp.Idempotent)")

public void idempotentOperation() {}

现在比使用 APT 简单一些了!切入点只说:“idempotentOperation 是有着 Idempotent 注解的 businessService”。

希望你的大多数服务操作都等幂的。在这种情况下,注解等幂的操作就可能比挑出等幂操作要容易得多。像 @IrreversibleSideEffects 这样的东西应该会成功。这在技术上和心理上都说得过去(指想要用 IrreversibleSideEffects 对他们的代码进行注解的人!我宁愿重写代码而避免使用它们;)。由于 idempotentOperation 的定义只有一处,很容易改变:

  @Pointcut(

"org.aspectprogrammer.myapp.SystemArchitecture.businessService() &&

!@annotation(org.aspectprogrammer.myapp.IrreversibleSideEffects)")

public void idempotentOperation() {}

idempotentOperation 是一个没有 IrreversibleSideEffects 注解的 businessService。

用开发时间方面提升生产力

  一旦你习惯了给 Spring AOP 编写 @AspectJ 方面,就会从 AspectJ 中获得额外的益处,即使你只在开发期间使用它(并且在你正在运行的应用程序中没有 AspectJ 编译的方面)。方面可以用来针对测试(它们使得某些模拟和错误注入变得更加容易)、调试和诊断问题,以及确保为你的应用程序所设计的设计指导方针得到实施。首先,让我们看一个设计实施方面(enforcement aspects)的实例。继续在数据访问层中进行,我们现在要引入 Spring HibernateTemplate,让 Spring 替我们管理 Hibernate 会话,而不用我们自己管理。以下这个方面将确保程序员不会忘记开始管理他们自己的会话:

public aspect SpringHibernateUsageGuidelines {

pointcut sessionCreation()



: call(* SessionFactory.openSession(..));

pointcut sessionOrFactoryClose()



: call(* SessionFactory.close(..)) ||

call(* Session.close(..));

declare error



: sessionCreation() || sessionOrFactoryClose()

: "Spring manages Hibernate sessions for you, " +

"do not try to do it programmatically";

}

有了这个方面之后,如果一位程序员在给 Eclipse 使用 AspectJ Development Tools(AJDT)插件,他或者她就将在问题视图中看到一个编译错误的标识,并在源代码中出错的位置(与任何一般的编译错误完全一样)会有错误文本:“Spring 替你管理 Hibernate 会话,请不要试图编程式地进行管理”(Spring manages Hibernate sessions for you, do not try to do it programmatically)。建议引入像这样的实施方面的方法是,将 AspectJ 编译步骤增加到用实施方面“织入”应用程序的构建过程——如果被方面发现构建错误,这项任务将会失败。

现在让我们看一下简单的诊断方面(diagnosis aspect)。回顾一下我们曾将一些事务标识为只读(一项很重要的性能优化)。随着应用程序复杂性的增加,从概念上来说,从事务划分所发生的服务层操作的位置,到作为指定用例的一部分而执行的业务领域逻辑,这之间可能十分遥远。如果在一个只读的事务期间,领域逻辑更新了一个领域对象的状态,我们就会有丢失更新的风险(从来没有提交到数据库)。这可能成为那些莫名其妙 bug 的根源。

LostUpdateDetector 方面可以在开发时间用来侦测可能的丢失更新。

public aspect LostUpdateDetector {

private Log log = LogFactory.getLog(LostUpdateDetector.class);

pointcut readOnlyTransaction(Transactional txAnn) :



SystemArchitecture.businessService() &&

@annotation(txAnn) && if(txAnn.readOnly());

pointcut domainObjectStateChange() :



set(!transient * *) &&

SystemArchitecture.inDomainModel();

..



我已经通过在方面中定义两个有用的切入点开始了。readOnlyTransaction 是有着 @Transactional 注解的 businessService() 的执行,readOnly() 属性设置为 true。domainObjectStateChange 是任何非瞬时领域 inDomainModel() 的更新。(注意,这是进行了简化,但是对于组成一个领域对象状态变化的东西仍然很有用——我们可以将该方面扩展为处理集合等等,如果我们希望如此的话)。利用所定义的这两个概念,我们现在就可以通过 potentialLostUpdate() 表达想说的话了:

  pointcut potentialLostUpdate() :

domainObjectStateChange() &&

cflow(readOnlyTransaction(Transactional));

potentialLostUpdate 是在一个 readOnlyTransaction(期间)的控制流中所做的一个 domainObjectState 变化。你从这里可以领略到切入点语言生效的威力。通过组成两个具名的切入点表达式,我们已经能够非常简单地表达一个很强大的概念。与你只有一个粗糙的拦截模型可用时相比,利用切入点语言更容易表达像 potentialLostUpdate 这样的条件。它也比像 EJB 3 所提供的那些过于单纯的拦截机制要强大得多。

最后,当发生 potentialLostUpdate 时,我们当然需要真正地做一些事情:

  after() returning : potentialLostUpdate() {

logLostUpdate(thisJoinPoint);

}

private void logLostUpdate(JoinPoint jp) {



String fieldName = jp.getSignature().getName();

String domainType = jp.getSignature().getDeclaringTypeName();

String newValue = jp.getArgs()[0].toString();

Throwable t = new Throwable("potential lost update");

t.fillInStackTrace();

log.warn("Field [" + fieldName + "] in type [" + domainType + "] " +

"was updated to value [" + newValue + "] in a read-only " +

"transaction, update will be lost.",t);

}

}



以下是有了这个方面之后,运行一个测试案例所得到的日志信息:

WARN - LostUpdateDetector.logLostUpdate(41) | Field [name] in type

[org.aspectprogrammer.myapp.domain.Pet] was updated to value [Mr.D.]

in a read-only transaction, update will be lost.

java.lang.Throwable: potential lost update

at org.aspectprogrammer.myapp.debug.LostUpdateDetector.logLostUpdate(LostUpdateDetector.aj:40)

at org.aspectprogrammer.myapp.debug.LostUpdateDetector.afterReturning(LostUpdateDetector.aj:32)

at org.aspectprogrammer.myapp.domain.Pet.setName(Pet.java:32)

at org.aspectprogrammer.myapp.service.impl.PetServiceImpl.updateName(PetServiceImpl.java:40)

at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:100)

at org.aspectprogrammer.myapp.service.impl.ConcurrentOperationExecutor.doConcurrentOperation(ConcurrentOperationExecutor.java:37)

at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:478)

at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:344)

at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)

作为题外话,解释一下干净且易读的堆栈轨迹(和适当的乐观重试逻辑)。易读的堆栈轨迹(stack trace)是由于从异常堆栈轨迹项中去除了干扰的另一个方面。没有适当的堆栈轨迹管理方面,所有的 Spring AOP 拦截堆栈框也都被显示出来,出现了像下面所示这样的堆栈轨迹。我想,你会认同说简化版是一个很大的改进!

WARN - LostUpdateDetector.logLostUpdate(41) | Field [name] in type

[org.aspectprogrammer.myapp.domain.Pet] was updated to value [Mr.D.]

in a read-only transaction, update will be lost.

java.lang.Throwable: potential lost update

at org.aspectprogrammer.myapp.debug.LostUpdateDetector.logLostUpdate(LostUpdateDetector.aj:40)

at org.aspectprogrammer.myapp.debug.LostUpdateDetector.ajc$afterReturning$org_aspectprogrammer_myapp_debug_LostUpdateDetector$1$b5d4ce0c(LostUpdateDetector.aj:32)

at org.aspectprogrammer.myapp.domain.Pet.setName(Pet.java:32)

at org.aspectprogrammer.myapp.service.impl.PetServiceImpl.updateName(PetServiceImpl.java:40)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)

at java.lang.reflect.Method.invoke(Unknown Source)

at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:287)

at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:181)

at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:148)

at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:100)

at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:170)

at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:71)

at org.aspectprogrammer.myapp.service.impl.ConcurrentOperationExecutor.doConcurrentOperation(ConcurrentOperationExecutor.java:37)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)

at java.lang.reflect.Method.invoke(Unknown Source)

at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:568)

at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:558)

at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:57)

at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:170)

at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)

at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:170)

at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:176)

at $Proxy8.updateName(Unknown Source)

at org.aspectprogrammer.myapp.debug.LostUpdateDetectorTests.testLostUpdateInReadOnly(LostUpdateDetectorTests.java:23)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)

at java.lang.reflect.Method.invoke(Unknown Source)

at junit.framework.TestCase.runTest(TestCase.java:154)

at junit.framework.TestCase.runBare(TestCase.java:127)

at junit.framework.TestResult$1.protect(TestResult.java:106)

at junit.framework.TestResult.runProtected(TestResult.java:124)

at junit.framework.TestResult.run(TestResult.java:109)

at junit.framework.TestCase.run(TestCase.java:118)

at junit.framework.TestSuite.runTest(TestSuite.java:208)

at junit.framework.TestSuite.run(TestSuite.java:203)

at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:478)

at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:344)

at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)

简化“基础结构”需求的实现

当你越来越习惯于 AspectJ 和所配套的工具组时,你就可以用 AspectJ 来实现影响你应用程序所有部分的需求,包括领域模型。作为一个简单的实例,我将向你介绍如何剖析 jpetstore 范例的应用程序。让我们首先看一下 Profiler 方面,然后填入一些外围的细节:

public aspect Profiler {

private ProfilingStrategy profiler = new NoProfilingStrategy();

public void setProfilingStrategy(ProfilingStrategy p) {



this.profiler = p;

}

pointcut profiledOperation() :



Pointcuts.anyPublicOperation() &&

SystemArchitecture.inPetStore() &&

!within(ProfilingStrategy+);

Object around() : profiledOperation() {



Object token = this.profiler.start(thisJoinPointStaticPart);

Object ret = proceed();

this.profiler.stop(token,thisJoinPointStaticPart);

return ret;

}

}

我们已经将 profiledOperation() 定义为 [the]PetStore() 中的 anyPublicOperation() 了。该方面表现得就像委托给 ProfilingStrategy 的控制器,我们将利用依赖注入通过 Spring 对它进行配置。

  <bean id="profiler"

class="org.springframework.samples.jpetstore.profiling.Profiler"

factory-method="aspectOf">

<property name="profilingStrategy">



<ref local="jamonProfilingStrategy"/>

</property>

</bean>

<bean id="jamonProfilingStrategy"

class="org.springframework.samples.jpetstore.profiling.JamonProfilingStrategy"



init-method="reset"

destroy-method="report">

</bean>

注意给方面 bean 使用了“factory-method”属性,这是配置单例(singleton)AspectJ 方面和配置一般的 Spring bean 之间的唯一区别。我正在用JAMon进行剖析,它提供了一个非常简单的 API。

public class JamonProfilingStrategy implements ProfilingStrategy {

public Object start(StaticPart jpStaticPart) {



return MonitorFactory.start(jpStaticPart.toShortString());

}

public void stop(Object token, StaticPart jpStaticPart) {



if (token instanceof Monitor) {

Monitor mon = (Monitor) token;

mon.stop();

}

}

}

这就是我们激活适用于整个 pet store 的剖析所必须做的全部工作。通过将 JAMon 提供的 jsp 增加到 pet store 应用程序,我们就可以在 Web 浏览器中观看到剖析的输出。以下是我在应用程序周围点击一会之后的屏幕快照:

简化领域模型

具有影响你领域模型的多个部分的业务逻辑需求,这也并不罕见。有些明显的实例为:设计模式实现(请见 Nick Leseicki 关于这个主题的精彩的 developerWorks 文章 :part 1part 2),领域对象的依赖注入(例如使用 Spring 的 @Configurable 注解),以及业务规则和策略的实现。在采用的这个阶段,你的核心业务逻辑变成了依赖于方面的存在。

你编写的方面将特定于你的领域。AspectJ 和 AJDT 都利用 AspectJ 构建,我们在它们的构建中使用了大量特定于领域的方面。举个例子,下面是我在 1.5.1 发布的开发期间增加到 AspectJ 的一个方面:它实现了一项经常被请求的特性,当一个异常被一个空的捕捉块淹没时,用它来发布一个警告。

public aspect WarnOnSwallowedException {

pointcut resolvingATryStatement(TryStatement tryStatement, BlockScope inScope)



: execution(* TryStatement.resolve(..)) &&

this(tryStatement) &&

args(inScope,..);

after(TryStatement tryStatement, BlockScope inScope) returning



: resolvingATryStatement(tryStatement,inScope) {

if (tryStatement.catchBlocks != null) {

for (int i = 0; i < tryStatement.catchBlocks.length; i++) {

Block catchBlock = tryStatement.catchBlocks[i];

if (catchBlock.isEmptyBlock() ||

catchBlock.statements.length == 0) {

warnOnEmptyCatchBlock(catchBlock,inScope);

}

}

}

}

private void warnOnEmptyCatchBlock(Block catchBlock, BlockScope inScope) {



inScope.problemReporter()

.swallowedException(catchBlock.sourceStart(),

catchBlock.sourceEnd());

}

}

即使在这个实例中,这个方面只在代码库中建议了一个位置,但它除了 JDT 编译器的功能之外,还通过将这个 AspectJ 模块化,使得代码更加清楚了,也使得未来的维护人员非常清楚如何实现这项特性。涉及利用方面给领域建模的进一步详情,则是另一篇文章的主题了。

小结

Spring 的目标是提供一种简单而强大的企业应用程序开发方法。利用它对 AOP 的支持,以及与 AspectJ 的整合,这种方法延伸到了影响应用程序多个部分的特性的实现。传统上而言,这些特性的实现都散布到整个应用程序逻辑中,使得它难以添加、去除和维护特性,并且使得应用程序逻辑复杂化。利用方面,Spring 让你能够给这些特性编写整洁、简单且模块化的实现。AOP 的采用可以分多个阶段进行:通过利用 Spring 提供的开箱即用的方面开始,然后可以利用 Spring AOP 在 Web、服务和数据访问层中添加你自己的 @AspectJ 方面。AspectJ 本身可以被用来提供开发生产力,而不用在 AspectJ 中引入任何依赖。更进一步探讨了横贯你应用程序多个层的基础结构需求,可以利用 AspectJ 方面被简单地实现。最后,你可以用方面来简化你领域模型本身的实现。

关于作者

Adrian Colyer 是 Interface21 的首席科学家,是 Eclipse.org 的 AspectJ 项目负责人,以及 AspectJ Development Tools(AJDT)项目的创办人。2004 年,他被 MIT Technology Review 投票选为世界前 100 名年轻的改革者之一,并且经常进行关于 Spring、AOP 和 AspectJ 主题的演讲。

关于 Interface21

Interface21 提供 Spring、AOP 和 AspectJ 方面的培训和咨询。至于课程安排或者要安排培训的,请见www.interface21.com

Adrian Colyer 和 Spring 社区其他成员出席 2006 年 12 月 7 至 10 日会议的相关内容请见http://www.thespringexperience.com

查看英文原文:Simplifying Enterprise Applications with Spring 2.0 and AspectJ
译者简介:俞黎敏(网名:阿敏总司令),技术顾问,自由撰稿人,开源爱好者,曾经参与 Spring 中文论坛组织 Spring 2.0 Reference 中文版的技术审校和满江红开源组织 Seam 1.2.1 Reference 的中文翻译工作;另外他还翻译了《CSS: The Missing Manual》、《Java Persistence with Hibernate》等书籍,并担任 CSDN、CJSDN、Dev2Dev、Matrix、JavaWorldTW 等技术网站 Java 论坛版主。他的博客是:http://YuLimin.JavaEye.com