http://www.infoq.com/cn/articles/Simplifying-Enterprise-Apps
用Spring 2.0和AspectJ简化企业应用程序
作者 Adrian Colyer译者 俞黎敏 发布于 2007年10月3日 上午4时10分
社区
Java
主题
AOP
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 1、part 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.iteye.com。