Spring 2.0 的新特性和应用实践

阅读数:3579 2007 年 7 月 11 日

主题

Spring 开源项目开始于 2003 年 2 月,现在框架正变得越来越强大。目前已经达到了超过一百万的下载量;在很多行业范围内成为事实上的标准;并且改变了企业 Java 应用的开发过程。

最重要的是,它发展了大量而且忠诚的用户,他们理解框架的关键价值并且共享反馈,来帮助框架高速发展。Spring 的使命也一直很清晰:



  • 提供一种非入侵性编程模型。应用程序的代码应该尽可能地与框架解耦。
  • 为内部基础设施提供一种优秀的解决方案,以便开发者能将注意力放在交付业务价值,而不是解决普通的问题。
  • 使开发企业应用程序尽可能简单、增强,而不是减弱、机械。

在 2006 年 10 月份进入最终版的 Spring 2.0,更加提升了这些价值。在核心团队在 2005 年佛罗里达Spring 体验大会前研究发展特性时,我们发布了两个关键的主题——简易性和强大——突出作为 Spring 2.0 的主线,并且依旧忠实于 Spring 的使命。

某些决定很容易。从一开始,我们很清楚 Spring 2.0 将会完全向后兼容,或者说尽可能地完全向后兼容。尤其考虑到 Spring 在很多企业中作为一个事实上的标准这样一个定位,避免任何对用户体验的破坏是非常重要的。幸运地是,Spring 一直致力于非入侵,这样的目标就完全可以达到。

在十个月的 Spring 2.0 开发过程进行中,我们也需要考虑到一些 Spring 在 2005 到 2006 年的使用中越来越明显的趋势:

  • Spring 越来越多地被一些非常大的组织来使用,从战略的角度而不是只从项目角度来采用。这不仅意味着关于向后兼容的责任,而且是与大量不同类别的用户相关的挑战。
  • 越来越多数目的优秀的第三方软件产品正在内部使用 Spring,并需要容器的配置优化和灵活性。这样的例子很多,这里简单列举几个:
    • 即将发布的 BEA WebLogic Server 10,使用了 Spring 和 Pitchfork 项目来执行注入和拦截。
    • BEA WebLogic Real Time(WLRT),来自于 BEA 的一种高端产品,致力于像前端办公交易这样的应用,需要很低的等待时间。
    • 大量广泛使用的开源产品,比如 Mule、ServiceMix 以及 Apache JetSpeed 门户容器。
    • 一些企业厂商使用 Spring 集成他们自己的产品,比如 GigaSpaces,Terracotta 和 Tangosol 等。尤其是网格空间的公司,正在逐步投入 Spring 作为编程模型的选择。
    • Oracle 的 SCA 实现,以及不同的其他 Oracle 产品。

因此我们需要确保当 Spring 变得对企业应用开发者更加友好的同时,也要迎合这些苛刻的用户。

从 35000 英尺

Spring 2.0 的最大愿景是什么?

Spring 2.0 提供了很大范围内的增强,其中最显著的可能是:

  • 配置扩展:在 Spring 2.0 中,Spring 支持可扩展的 XML 配置,使得使用自定义元素开发成为可能,它们为生成 Spring bean 的定义提供一种新层次的抽象。XML 扩展机制同样提供了一些新的标签来简化许多普通的任务。
  • 在 AOP 框架中有重要增强,使得既强大又更易于使用。
  • 增强对 Java 5 的支持。
  • 提供以动态语言实现 Spring bean 的能力,比如 Groovy、JRuby 和 Beanshell,同时保留 Spring 组件模型的所有服务,比如依赖注入,方便的声明性服务以及 AOP。
  • 以及许多新的特征,包括一个 Portlet MVC 框架,“消息驱动 POJO”,与新的 API 的集成,包括 JAVA 持久化 API(JPA), 以及一个异步任务执行框架。

有许多表面上不是很明显的特征,但仍然很重要:

  • 对 Ioc 容器更进一步的扩展,使得在 Spring 之上构建框架或产品更加容易。
  • 对 Spring 特有的集成测试支持的改善。
  • 提供 AspectJ 来暴露 Spring 核心功能给使用 AspectJ 和 Spring 的用户,比如事务管理和依赖注入。

重要的是,这些特性被设计以和谐的整体来一起运行。

概述

这篇文章分为两部分。在第一部分(就是本文),我们将会谈到核心容器,XML 配置扩展,AOP 增强,以及特定于 Java 5 的特征。

在第二部分,我们将会谈到消息,对动态语言的支持,Java 持久化 API 以及 Web 层的增强。也会看一下表面以下的一些改进。

现在就让我们更深入地研究一些新的特性,并且使用一些代码范例来描述它们。

XML 配置扩展

在 Spring 2.0 中最明显的增强就是 XML 配置。

Spring Ioc 容器实际上是独立于元数据的表示的,比如 XML。Spring 以 Java 对象(BeanDefinition 以及它的子接口)的形式拥有自己的内部元数据。有一个对 XML 配置补充的研究,比如使用注解的 Java 配置

然而在今天,XML 是被最多用在配置 Spring 上的,这就是 Spring 核心中配置改进的焦点。

Spring 2.0 中 XML 的增强巧妙概括了简易性和强大的主题:它们简化执行某些普通的任务,但是也使得一些额外的高级任务成为可能。

目标

传统上,在 Spring 的 XML 配置语法和 Spring 的内部元数据之间有一对一的关系。一个元素产生一个 BeanDefinition。

这通常就是我们想要的,并且对于配置那些框架并不了解的应用程序类,也是理想的。

但是,如果框架应该了解一个可能被反复使用的特定的类呢,比如象 JndiObjectFactory 这样的普通的类,用来从 JNDI 寻找对象,并将其作为可注入的对象注入 Spring 容器,或者如果一些 Bean 定义只是在一起使用时才有意义呢?

这样,一种新的抽象形式就能带来重要的好处。

考虑一下下面这个例子,关于 JNDI 的 lookup:

<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">

<property name="jndiName" value="jdbc/jpetsore" />

</bean>

这当然要好过以前那些实现 Service Locator 的令人厌恶的日子,但它并不完美。我们会一直使用这个相同的类。并且(除非我们使用 Java 5)没有一种机制来指明“jndiName”属性是必需的,而其他的属性都是可选的。

Spring 2.0 添加了一个方便的“jee”命名空间,其中包括允许我们执行同样的 JNDI lookup 的标签,就像下面这样:

<jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>

这个比较简单而且清楚。它清晰地表达了意图而且更加简洁。而且 schema 抓住了 jndiName 属性是必需的这样一个事实,并有方便的工具支持。其他可选的属性同样在这个 schema 中得以表达,就像下面这个例子:

<jee:jndi-lookup id="simple"

jndi-name="jdbc/MyDataSource"

cache="true"

resource-ref="true"

lookup-on-startup="false"

expected-type="com.myapp.DefaultFoo"

proxy-interface="com.myapp.Foo"/>

请注意在这样一个相对简单的示例中,attribute 的名称几乎和被配置的类中 property 名称一样,这不是必需的。没有必要做一个命名上的对应,或者在 attribute 和 property 之间做一对一的对应。我们同样能够处理子元素内容。而且,正如我早先提到的,我们可以从一个扩展标签产生我们想要的任意数目的 bean 定义。

现在让我们来考虑一个更加复杂的例子,其中我们要使用自定义标签的能力来产生不止一个 bean 定义。

从 Spring 1.2 开始,Spring 就能够识别 @Transactional 注解,并通过代理来使受影响的 bean 变成事务。这导致了一种简单的部署模型——简单地添加注解过的 bean,它们就能自动变得可以处理事务——但是建立这样模型需要一些令人心烦的戏法。为了所需的合作对象,需要三个 bean 定义——一个 Spring AOP Advisor,一个 TransactionInterceptor,以及一个 DefaultAdvisorAutoProxyCreator 来引发自动的代理。这些 bean 定义组成了一个咒语,能够在不同的应用程序以不变的状态使用,但是暴露了多过用户不需要知道的细节:

<bean

class="org.springframework...DefaultAdvisorAutoProxyCreator"/>

<bean class="org.springframework...TransactionAttributeSourceAdvisor">



<property name="transactionInterceptor

ref="transactionInterceptor"/>

</bean>

<bean id="transactionInterceptor"



class="org.springframework...TransactionInterceptor">

<property name="transactionManager"

ref="transactionManager"/>

<property name="transactionAttributeSource">

<bean

class="org.springframework...AnnotationsTransactionAttributeSource">

</bean>

</property>

</bean>

这是错误的抽象层次。你并不需要看到 Spring AOP 框架如何控制事务管理这种层次的细节,本意不需要这样的清晰。Spring 2.0 在新的“tx”命名空间中提供了一个标签,来替换所有这些定义,像下面这样:

<tx:annotation-driven />

这个简便的标签达到了相同的效果。这个标签清楚地表达了意图——自动识别事务注解。那三个 bean 定义仍然通过底层的扩展标签来创建,但那现在是框架的事情了,而不是用户的。

Spring 2.0 扩展标签可以在必要时定义它自己的 attribute 以及子元素结构。定义命名空间的开发者完全可控。处理 Spring 2.0 扩展标签的 NamespaceHandler 可以从一个扩展标签创建任意数目的 bean 定义。

为了使用扩展标签,要使用 XML schema 而不是 DTD,并且导入相关的命名空间。缺省的命名空间应该是 beans schema。下面的“tx”命名空间的例子,允许使用 <tx:annotation-driven>:

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

xsi:schemaLocation="http://www.springframework.org/schema/beans

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

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

http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
扩展标签可以和正规的 bean 定义混合使用,可以引入任意数目的命名空间,并在同一个文档中使用。

方便的命名空间

Spring 2.0 提供一些方便的命名空间。其中最重要的是:

  • 事务管理(“tx”):在 Spring 2.0 中使 Spring bean 能够处理事务变得相当容易,就像我们看到的那样。它同样使定义“transaction attributes”来映射事务行为到方法上变得简单的多。
  • AOP(“aop”):Spring 2.0 中专门的用来 AOP 配置的标签比以前更加简洁,Ioc 容器不再需要依赖 AOP 框架。
  • Java EE(“jee”):这样简化了对 JNDI 和其他 Java EE API 的使用,正如我们看到的那样。EJB lookup 比 JNDI lookup 获得的简单性还要多。
  • 动态语言(“lang”):以动态语言简化 bean 的定义——Spring 2.0 的一个新特性。
  • Utils(util):简化加载 java.util.Properties 对象和其他通用的任务。

在 Spring2.1 以及往后版本中,将会加入更多的命名空间,来将简单性引入新的领域。简化 Spring MVC 和 JPA 使用的命名空间可能会最先加入到核心中去。

第三方配置扩展

作为一种扩展机制,Spring 2.0 命名空间最重要的可能性是在 Spring 核心的外围。

许多产品构建于 Spring 基础之上,它们的配置可以使用命名空间来变得更加简单。一个很好的例子是 Acegi Security for Spring(将在 2007 年早些时候改名为 Spring Security),它需要配置一些协作 bean 的定义。Spring 2.0 的命名空间会使这变得非常简单。再一次更加清楚地表达了简单性的意图。

许多产品和 Spring 紧密集成,这样的好处不言而喻。Tangosol 对 Coherence 的集成就是现成的案例。

其他潜在的例子包括支持 Spring 配置的产品,比如 IBM 的 ObjectGrid。虽然 ObjectGrid 目前没有在内部使用 Spring,但它被设计成通过 Java 来配置,使得能更加容易地集成到基于 Spring 的应用程序中。扩展 schema 会让这个变得相当简单。

一个 XML 文档使用某个扩展标签作为顶层的元素是可能的。这样避免需要通过命名空间给扩展 schema 元素加上前缀,意味着这样的配置看起来更自然一些,而非以 Spring 为中心的。(通常,元素是在缺省的命名空间,因此传统的 Spring bean 定义并不需要前缀。)

随着时间过去,和 JSP 自定义标签的发展,经验会通过实证明了的价值引出通用目的的标签。我们期望用户来创建命名空间的库,来让这个社区受益。

实现 XML 扩展

实现命名空间相对简单。它分三步:

  1. 定义你的 XML schema。这是最困难的一步,需要有合适的工具。对于 schema 没有限制,当然你需要明白它是如何在运行时引导 BeanDefinition 的生成的。
  2. 实现 NamespaceHandler 接口,从你的 schema 中的元素和属性来产生 BeanDefinition。
  3. 编辑一个专门的注册文件,spring.handlers,来让 Spring 知道新建的 NamespaceHandler 类。

Spring 配备的 spring.handlers 文件显示了“标准”的命名空间是如何配置的:

http\://www.springframework.org/schema/util=org.springframework.beans.factory.xml.UtilNamespaceHandler

http\://www.springframework.org/schema/aop=org.springframework.aop.config.AopNamespaceHandler

http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler

http\://www.springframework.org/schema/tx=org.springframework.transaction.config.TxNamespaceHandler

http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler

http\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.SimplePropertyNamespaceHandler

你可以在不同的 /META-INF 目录拥有多个 spring.handlers 文件。Spring 会在运行时合并它们。

一旦遵循了这些步骤,就可以使用你的新的扩展。

NameSpaceHandler 接口并不难实现。它采用 W3C DOM 元素,并通过处理它们来生成 BeanDefinition 元数据。Spring 解析 XML:你的代码仅仅需要遍历 XML 树。

public interface NamespaceHandler {

void init();

BeanDefinition parse(Element element, ParserContext parserContext);

BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder definition, ParserContext parserContext);



}

parse() 方法是最重要的,它负责将 BeanDefinitions 添加到提供的上下文中。

同样要注意 decorate() 方法。NamespaceHandler 同样可以装饰一个包含的 bean 定义,就像下面这个创建作用域内的代理的语法所显示的:

<bean id="scopedList" class="java.util.ArrayList" scope="request">

<aop:scoped-proxy/>

</bean>

<aop:scoped-proxy> 标签用来修饰包含它的标准的 <bean> 元素;如果它能够访问 BeanDefinition,它就能对 BeanDefinition 进行修改。

为了简化 BeanDefinition 元数据的生成,Spring 2.0 引入了一种方便的新的 BeanDefinitionBuilder 类,提供一种流畅的、构建器风格的 API。开始实现 NamespaceHandlers 的最佳指导是那些存在于 Spring 核心中的类。其中 UtilNamespaceHandler 是个相对简单的例子;而 AopNamespaceHandler 是个比较高级的例子,它解析了一个复杂的子元素结构。

最佳实践:什么时候应该定义你自己的命名空间?

你有锤子并不意味着其他一切都是钉子。正如我们已经看到的,Spring 2.0 的 XML 扩展机制在很多案例中交付了很大的价值。然而,如果没有很好的理由就不应该使用它。因为 XML 扩展标签是一种新的抽象,它同样提供了一些需要学习的新的内容。Spring 的正规的 XML 格式对成千上万的开发者来说已经很熟悉了,甚至对那些新接触 Spring 的人都是用直觉就可以判断的。Spring XML 文件提供了易于理解的某个应用程序结构的蓝图。如果过度配置使用了不熟悉的自定义标签,就没什么必要了。

让我们在这个领域内考虑一些相关的经验。JSP 自定义标签是个很好的例子。最终它们通过设计得很棒的标签库,比如 JSTL,Struts 和 Spring MVC 的标签库,产生了真实的价值。但在早些年,它们会引起厌恶,甚至是混乱的 JSP 页面。(我在这里可以根据经验来解释,因为我自己实现了一两个这样的标签库)。

把命名空间处理器看作是一个重要的新的扩展点,以及对 Spring 很有价值的新的抽象。它们对于那些在 Spring 之上构建第三方产品的人来说非常棒;它们对于非常大型的项目也很有用。很快就会发现,没有了它们,很难想象生活会变成什么样子。但是最终用户还是应该对实现它们持谨慎态度,但使用没有问题。

当然,伴随 Spring 提供的方便的扩展标签,比如 aop,tx 以及 jee 命名空间,将很快成为 Spring 配置词表的核心部分,就跟元素一样被广泛了解。你当然应该优先使用这些,而不是传统的冗长的方式,来完成相同的任务。

语法糖

转向使用 schema 也允许一点点快捷方式,比如对 property 值使用 attribute 而不是子元素。这些 attribute 不会被验证,但因为我们使用的是 XML schema,而不是 DTD,我们仍然可以保留所有其他的验证。因为 attribute 名称就是 property 名称,XML 验证不会再添加任何东西;这是基于 Java 的验证的问题,而不是 XML 结构的。

考虑一下下面这个 Java 对象,它有两个简单的 property,以及对一个关联对象的依赖:

public class Person {

private int age;

private String name;

private House house;

public void setAge(int age) {

this.age = age;

}

public void setName(String name) {



this.name = name;

}

public void setHouse(House house) {



this.house = house;

}

}

可以像下面这样使用 XML 进行配置:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:p="http://www.springframework.org/schema/p"

xsi:schemaLocation="http://www.springframework.org/schema/beans

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

<bean class="com.interface21.spring2.ioc.Person"



p:name="Tony"

p:age="53"

p:house-ref="number10"

/>

<bean class="com.interface21.spring2.ioc.House"



id="number10"

p:name="10 Downing Street"

/>

</beans>

请注意 property 是如何通过使用 attribute 来提供的,而不是元素。这样使用了特殊命名空间“p”的魔力。这个命名空间并没有验证,但允许使用 attribute 名称来和 Java property 名称进行匹配。

通过简单的几句,我们就简化了使用“p”命名空间中的 property 名称。就像“p:name”。当注入对其他 Spring bean 的引用时,使用“-ref”后缀,就像“p:house-ref”。

这种快捷的语法在你想要使用 autowiring 时尤其方便。比如,考虑下面的变量:

<bean class="com.interface21.spring2.ioc.Person"

autowire="byType"

p:name="Tony"

p:age="53"

/>

这里我们没有设置“house”property,因为 autowiring 会考虑它。你甚至可以使用在 <beans> 元素层次使用 default-autowire 来在整个文件使用 autowiring。

下面这个来自于 Spring 1.0 或者 1.1 的用法演示了 Spring 配置在最近两个主要的发布版本中(1.2 和 2.0)减少了多少数目的尖括号。

<bean class="com.interface21.spring2.ioc.Person">

<property name="name"><value>"Tony"</value></property>

<property name="age"><value>"53"</value></property>

<property name="house"><ref local="number10" /></property>

</bean>

在 Spring1.2 中,我们引入了“value”和“ref”attribute,在大多数情况下不需要子元素,而在 Spring 2.0 中则可以更纯粹和简单地使用 attribute。

当然,传统的 XML 格式可以继续工作。当 property 值很复杂,而且不合法或者不能读作一个 attribute 值时,就可以使用它们(传统的 XML 格式)。而且,当然,没有必要重写已经存在的配置文件。

除了 XML 配置扩展,在 Spring Ioc 容其中还有很多其他的新的特性。

其他 Ioc 容器增强

新的 bean 作用域

和 XMl 扩展一起,最重要的新的 Ioc 容器特性就是对于 bean 生命周期管理的新增的自定义作用域。

1.背景

Spring 之前为 bean 提供了两种作用域:单例原型(或者叫非单例)。

Singleton bean 是一个在所属容器上下文中的单例对象。在容器的生命周期中只会有一个实例存在,当容器关闭,它就会向所有需要知道容器关闭事件的单例 bean 发送事件通知——比如,关闭任何可以管理的资源,像连接池。

Prototype bean 是任何时候通过注入到另外一个 bean 而被引用,或者是对所属容器上 getBean() 调用的响应时创建的。在这种情况下,bean 定义与一个单个对象没有关系,而是一个用来创建对象的配方。每个创建好的实例会有完全相同的配置,但会有不同的身份。Spring 容器不会持有对原型的引用;它的生命周期由获得它的那段代码来负责。

在 Spring 2.0 中,我们添加了自定义作用域的能力。可以给它们起任何名字。某个自定义作用域通常与能够管理对象实例的后端存储相对应。在这种情况下,Spring 提供了它熟悉的编程模型,支持注入和查找,而且后端存储提供了作用域内对象的实例管理。

典型的后端存储有:

  • HTTP session
  • Clustered cache
  • 其他持久化存储

2.Web 作用域

对于这个特性,最通常的需求是关于在 web 应用程序 HTTP session 中透明存储对象。这在 Spring 2.0 中得到很方便的支持。因为这种需求很普通,所以举例会很容易:

考虑下面这个 bean 的定义:

<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>

这里我们指定了“session”作用域而不是缺省的“singleton”。作用域可以指定成任何名字,但在 Web 应用程序中提供“session”和“request”便于使用。

当我们通过名称“userPreferences”调用 getBean() 时,Spring 会很明显地从当前 HTTP Session 中找出 UserPreference 对象。如果没找到 UserPreferences 对象,Spring 就会创建一个。注入使得可以为用户描绘一个可以自定义的预配置的 UserPreferences。

为了让它工作起来,你需要在你的 web.xml 文件中像下面这样定义一个过滤器。它会在底层处理一个 ThreadLocal 绑定,以便 Spring 能够知道去哪个 HTTP Session 对象中去查找。

<web-app>

...

<listener>

<listener-class>

org.springframework.web.context.request.RequestContextListener></listener-class>

</listener>

...

</web-app>

这样就解决了查找的问题。但通常我们宁愿使用 API 尽量少的注入风格。如果我们想要把“userPreferences”bean 注入到其他 bean 中去会发生什么呢,哪个会有更长的生命周期呢?比如,如果我们想在像下面这样的一个单例 Spring MVC 控制器中采用单独的 UserPreferences 对象时,又会发生什么呢:

public class UserController extends AbstractController {

private UserPreferences userPreferences;

public void setUserPreferences(UserPreferences userPreferences) { this.userPreferences = userPreferences;

}

@Override



protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)

throws Exception {

// do work with this.userPreferences

// Will relate to current user
}

}

在这个案例中,我们想要“非常及时”的注入,对短期存在的 UserPreferences 对象的引用是在被注入者使用的时候解析的。

通常有这样的误解,就是 Spring 注入是静态的,因此必然是无状态的。这并不正确。因为 Spring 有一个复杂的基于代理的 AOP 框架,它能在运行时通过提供这样“非常及时”的功能来隐藏查找的动作。因为 Ioc 容器控制着注入的内容,它能注入一个隐藏了需要查找的代理。

我们可以很容易地通知容器来执行这样的代理,像下面这样。我们使用一个子元素来修饰 userPreferences bean 定义,

<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">

<aop:scoped-proxy/> </bean>

<!-- a singleton-scoped bean injected with a proxy to the above bean -->



<bean id="userController" class="com.mycompany.web.UserController">

<!-- a reference to the proxied 'userPreferences' bean --> <property name="userPreferences" ref="userPreferences"/>

</bean>



</beans>

现在解析会如期动态地发生;UserController 中的 userPreferences 实例变量就会是从 HTTP Session 中解析出正确 UserPreferences 对象的代理。我们不需要直接跟 HTTP Session 对象打交道,而且可以轻松地对 UserController 进行单元测试,而不需要一个 mock HttpSession 对象。

另外一种获得“非常及时”的注入的方式是使用一个查找方法。这是一个自从 Spring 1.1 就存在的技术,它使生命周期长的 bean(通常是单例)能依赖一个潜在的生命周期短的 bean。容器能够重载一个抽象(或者具体)的方法来在方法被调用时返回执行 getBean() 调用的结果。

在这个例子中,我们没有在被注入者中定义一个实例变量,而是定义了一个返回所需对象的抽象方法。方法必须是公有的或者受保护的:

public abstract class UserController extends AbstractController {

protected abstract UserPreferences getUserPreferences();

@Override protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)



throws Exception {

// do work with object returned by getUserPreferences()

// Will relate to current user
}

}

在这个例子中,没有必要使用一个作用域内的代理。我们改变了被注入对象的 bean 定义,而不是被注入的 bean。XML 配置看起来应该是这样:

<bean id="userPreferences" class="com.foo.UserPreferences" scope="session" />

<!-- a singleton-scoped bean injected with a proxy to the above bean --> <bean id="userController" class="com.mycompany.web.UserController">

<!-- a reference to the proxied 'userPreferences' bean --> <lookup-method name="getUserPreferences" bean="userPreferences" />

</bean>



</beans>

这种机制需要类路径中包含 CGLIB。

3.其他可能性

无疑,在真实的 Spring 风格中,底层的机制是可插拔的,而不是绑定到 Web 层的。比如,Tangosol Coherence 作用域可以像下面这样使用,通过 Tangosol 和 Interface21 提供的“datagrid”命名空间。

<?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"

xmlns:datagrid="http://schemas.tangosol.com/schema/datagrid-for-spring"



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

http://schemas.tangosol.com/schema/datagrid-for-spring

http://schemas.tangosol.com/schema/datagrid-for-spring/datagrid-for-spring.xsd">

<datagrid:member/>

<bean id="brian" class="Person" scope="datagrid">



<aop:scoped-proxy/>

<property name="firstName" value="brian" />



<property name="lastName" value="oliver" />

<property name="age" value="21" />

</bean>

</beans>

在一个“datagrid”作用域内声明一个 bean,意味着 bean 的状态管理是由 Coherence 来执行,在一个 Clustered Cache 中,而不是在本地的服务器上。当然,bean 是由 Spring 来实例化和注入的,而且可以从 Spring 服务中受益。我们预见特定作用域内的 bean 会在 SOA 和批处理的环境中非常有用,并且期望能在将来 Spring 的版本中添加更多方便的 bean 作用域。

4.自定义作用域

定义你自己的作用域很简单。参考手册详细解释了这个过程。你将需要一种策略来辨别如何在当前的作用域内解析出对象。典型的情况下,这会涉及到 ThreadLocal,就像 Web 作用域在底层所做的那样。

类型推断

如果你运行的是 Java 5,Spring 2.0 会从它新的能力中获益,比如泛型。比如,考虑一下下面这个类:

public class DependsOnLists {

private List plainList;

private List<Float> floatList;

public List<Float> getFloatList() {

return floatList;



}

public void setFloatList(List<Float> floatList) {



this.floatList = floatList;

}

public List getPlainList() {

return plainList;

}

public void setPlainList(List plainList) {



this.plainList = plainList;

}

}

“plainList”属性是个旧式风格的集合,而“flodatList”属性有个类型化的说明。

考虑下面的 Spring 配置:

<bean class="com.interface21.spring2.ioc.DependsOnLists">

<property name="plainList">

<list>

<value>1</value>

<value>2</value>

<value>3</value>

</list>

</property>

<property name="floatList">

<list>

<value>1</value>

<value>2</value>

<value>3</value>

</list>

</property>

</bean>

Spring 会正确地为“floatList”属性填上浮点数,而不是字符串,因为它能非常聪明地意识到它需要执行类型转换。

下面的测试演示了这种情况:

public class GenericListTest extends

AbstractDependencyInjectionSpringContextTests {

private DependsOnLists dependsOnLists;

public void setDependsOnLists(DependsOnLists dependsOnLists) {



this.dependsOnLists = dependsOnLists;

}

@Override



protected String[] getConfigLocations() {

return new String[] { "/com/interface21/spring2/ioc/inference.xml" };

}

public void testLists() {



List plainList = dependsOnLists.getPlainList();

List<Float> floatList = dependsOnLists.getFloatList();

for (Object o : plainList) {

assertTrue(o instanceof String);

System.out.println("Value='" + o + "', class=" +

o.getClass().getName());

}

for (Float f : floatList) {

System.out.println("Value='" + f + "', class=" +

f.getClass().getName());

}

}

}

输出看起来会像下面这样:

Value='1', class=java.lang.String

Value='2', class=java.lang.String

Value='3', class=java.lang.String

Value='1.0', class=java.lang.Float

Value='2.0', class=java.lang.Float

Value='3.0', class=java.lang.Float

新扩展点

在 Ioc 容器中有一些新的扩展点,包括:

  • 额外的 PostPostProcessor 钩子,为像 Pitchfork 这样的项目提供更强大的能力,来处理自定义注解,或者在 Spring bean 实例化和配置时执行其他操作。
  • 添加任意元数据到 BeanDefinition 元数据的能力。添加信息很有用,虽然对 Spring 本身没有意义,但可以被构建于 Spring 之上的框架,或者像集群产品这样跟 Spring 集成的产品来处理。

这些主题主要和高级用户,以及那些使用 Spring 编写产品的人有关,这同样超出本篇文章的范围。但是理解 Spring 2.0 并不仅仅是在表面上增强,这很重要;它在底层完成了相当多的有难度的工作。

同样也有大量的增强是支持与 OSGi 的集成,形成 Spring OSGi 集成项目,它将 OSGi 的动态模块管理的能力与 Spring 组件模型相集成。这个工作会在 Spring2.1 中继续进行,会把 Spring 的 JAR 文件打包成 OSGi 的 bundle。

AOP 增强

在 Spring 2.0 中最激动人心的增强之一是关于 Spring AOP,它变得更加便于使用而且更加强大,主要是通过复杂而成熟的 AspectJ 语言的支持功能来实现,而同时保留纯的基于代理的 Java 运行时。

我们一直坚信 AOP(面向切面编程)很重要。为什么?因为它提供给我们一种新的思考程序结构的方法,能够解决很多纯 OOP 无法解决的问题——让我们能够在一个模块中实现某些需求,而不是以发散的方式实现。

为了理解这些好处,让我们考虑一些我们可以在需求中表达但无法直接用纯 OO 代码实现的情况。企业开发者使用一个通常的词汇表来让他们进行清楚的沟通。比如,像服务层,DAO 层,Web 层或者 Web 控制器这样的术语,这不需要什么解释。

许多需求是用这个词汇表中的术语来表达的。比如:

  • 服务层应该是可以处理事务的。
  • 当 DAO 操作失败时,SQLException 或者其他特殊持久化技术的异常应该被翻译,以确保 DAO 接口不会有漏掉的抽象。
  • 服务层对象不应该调用 Web 层,因为各层应该只依赖直接处在其下方的层。
  • 由于并发相关操作的失败而导致失败的等幂业务服务可以重试。

虽然这些需求都是现实存在的,并来自于经验,但它们并不能用纯 OOP 来优雅地解决。为什么?主要有两个原因:

  • 这些来自于我们词汇表的术语有意义,但它们并不是抽象。我们不能使用术语编程;我们需要抽象。
  • 所有这些都是所谓横切关注点的例子。一个横切关注点,在用传统 OO 方法实现时,会分解成很多类和方法。比如,想象一下在跨 DAO 层遭遇特殊异常时要使用重试逻辑。这个关注点横切许多 DAO 方法,而且在传统的方式中会需要实现许多单独的修改。

AOP 就是通过对横切关注点进行模块化,并让我们从普通的还可以编程的抽象的词汇表来表达术语,来解决这样问题的技术,这些抽象叫做切入点,我很快会再解释一些关于它们的细节。这种方法带来一些主要好处,比如:

  • 因为减少了剪切粘贴风格的复制而减少代码行数。这在像异常转换和性能监测这样的 try/catch/finally 习惯用法中尤其有效。
  • 在单个代码模块中捕捉这样需求的能力,提升可追踪能力。
  • 在单个地方修补 bug 的能力,而不需要重新访问应用程序中许多位置。
  • 确保横切关注点不混淆主要的业务逻辑——随着开发的进展,这很有可能成为危险之处。
  • 开发者和团队之间更好的职责分离。比如,重试功能可以有单个开发者或者团队来编码,而不需要由许多开发者跨多个子系统进行编码。

因此 AOP 很重要,我们想提供最好的解决方案。

Spring AOP 无疑是最广泛使用的 AOP 技术,归功于以下优点:

  • 采用成本几近为零。
  • 提供正确的切入点,这才称得上是 AOP 而不仅仅是拦截。
  • 提供一个支持许多使用方式的灵活的框架,可编程也可通过 XML。

然而,在 Spring 2.0 之前,Spring 中的 AOP 有一些缺点:

  • 不写 Java 代码,只能表达简单的切入点。并没有一种切入点表达语言来以字符串形式,简洁表达复杂的切入点,虽然 RegexpMethodPointcutAdvisor 允许定义简单正规的基于表达的切入点。
  • 当配置复杂 AOP 使用场景时,XML 配置会变得很复杂。泛型元素被用来配置 AOP 类;虽然这对一致性来说很棒,对切面和类提供 DI 和其他服务,但它没有一个专门的配置方法来得简洁。
  • Spring AOP 不适合通知细粒度的对象——对象需要由 Spring 管理或者通过编程被代理。
  • 基于代理的方法的性能负载在少数案例中成为问题。
  • 因为 Spring AOP 分离了代理和目标(被修饰或者被通知的对象),如果某个目标方法调用了目标上的方法,就不会使用到代理,意味着 AOP 通知并不适用。AOP 使用基于代理的方法的正反面影响超出了本文的范围:有一些积极的因素(比如能够对同一个类的不同实例应用不同的通知),但主要还是消极的。

为了在 Spring 2.0 中增强这个重要领域,我们希望在它的优势上构建,同时解决缺点。

目标

最先的两个缺点也是最显著的。它们都跟切入点相关。后面的三个缺点在 Spring 用户的正常使用中很少发生,如果它们证明是的确有问题的,我们建议使用 AspectJ。(就像你会看到的,这是 Spring AOP 直接的进步。)

XML 配置扩展解决了关键的挑战之一。因为我们想要保持 Spring 模块的设计,我们过去不能在 Spring DTD 中提供特定于 AOP 的标签——因此在这种情况下需要依赖可以详细一点的通用配置。随着 Spring 2.0 的出现,这样的问题没有了,因为 XML schema 并不像 DTD,它允许扩展。我们可以提供一个 AOP 命名空间,看起来能让 Ioc 容器识别 AOP 结构,但不会影响模块化。

AOP 术语 101:理解切入点和通知

让我们简要地修正一下某些 AOP 术语。如果你使用过 AOP 这些概念,可能对你来说很熟悉——这些概念是相同的,仅仅有一点不同,即更加优雅和强大的表达方式。

切入点是匹配规则。它在程序执行中确定应该应用某个切面的点的集合。这些点叫做连接点。在应用程序运行时,连接点随时会有,比如对象的实例化和方法的调用。在 Spring AOP(所有版本)的案例中,唯一支持的连接点是公有方法的执行。

通知是可以被切面应用到连接点的行为。通知能在连接点之前或之后应用。通知的所有类型包括:

  • Before advice:在连接点之前调用的通知。比如,记录方法调用即将发生的日志。
  • After returning adive:如果在连接点的方法正常返回时调用的通知。
  • AfterThrowing advice(在 Spring1.x 中叫做 Throws 通知):如果连接点的方法抛出一个特殊的异常时调用的通知。
  • After advice:在连接点之后调用的通知,无论结果是什么。特别像 Java 中的 finally。
  • Around advice:能够完全控制是否执行连接点的通知。比如,用来在事务中封装某个方法调用,或者记录方法的执行时间。

切面是结合切入点和通知成一个模块方案,解决特殊的横切问题。

如果这有点抽象,请不要担心:代码示例会很快解释清楚的。

对在 Spring 2.0 和 AspectJ 的环境中关于 AOP 基础的更深讨论,请参考 Adrian 在 InfoQ 上很棒的文章,"Simplifying Enterprise Applications with Spring 2.0 and AspectJ."

为什么会是 AspectJ 切入点表达式?

迄今为止,我们讨论过的概念都是基本的 AOP 概念,对于 Spring AOP 或者 AspectJ 而且这并不特别,在 Spring1.x 中已经是存在的。那么为什么我们选择在 Spring 2.0 中采用 AspectJ 呢?

如果我们需要一种切入点表达语言,那么选择就会很简单。AspectJ 有个思路很好,严格定义和充足文档的切入点语言。它最近实现了一个当在 Java 5 上运行时,能对采用 Java 5 语法的编码全面检查。它不仅有很棒的参考材料,而且很多书籍和文章都对它进行了介绍。

我们不相信重新发明的轮子,而且定义我们自己的切入点表达语言是不合理的。进一步而言,自从 AspectWerkz 在 2005 年早期和冰岛 AspectJ 项目之后,很明显 AspectJ 是除了 Spring 2.0 之外唯一一个主流的 AOP 技术。因此关键的合并既是一种考虑也是一种技术优势。

新的 XML 语法

新的 AOP 命名空间允许 Spring XML 配置指定 AspectJ 切入点表达式,通过由切入点匹配的方法将通知指向任何 Spring bean。

考虑一下我们上面看到的 Person 类。它有个 age 属性,以及一个增加 age 的 birthday 方法:

public void birthday() {

++age;

}

我们假设有这样的需求,任何时候调用 birthday 方法,我们都应该发送一张生日贺卡给过生日的人。这是一个经典的横切需求:它并不是我们主要的业务逻辑部分,而是个单独的关注点。理想的情况下,我们希望能够将那个功能模块化,而不影响 Person 对象。

让我们考虑一下通知。实际上,邮递发送一张生日贺卡,或者甚至发送一张电子贺卡,当然会是这个方法的主要工作。然而,由于这篇文章中我们的兴趣在于触发的基础结构,而不是发送生日贺卡的机制。因此我们就简单使用控制台输出。这个方法需要访问到过生日的这个 Person,而且无论何时 brithday 方法被调用,它都应该被调用。下面是简单的通知实现:

public class BirthdayCardSender {

public void onBirthday(Person person) {

System.out.println("I will send a birthday card to " +

person.getName() + "; he has just turned "

person.getAge());

}

}

本质上我们在 Person 上想要一种 Observer 机制,但并不把 Person 修改成可被观察的。请同时注意在这个例子中,BirthdayCardSender 对象被用作一个切面,但不需要实现任何特定于框架的接口。这样就使得使用已经存在的类作为切面成为可能,而且扩展 Spring 的非入侵编程模型到潜在的切面以及普通的类。

通过 Spring 2.0,我们可以像下面这样把 BirthdayCardSender 当作一个切面来使用。首先,我们把 BirthdayCardSender 类定义成一个 bean。这很简单:

我们可以依赖注入这个对象,或者应用任何其他的 Spring 组件模型服务,如果我们愿意的话。

下一步,我们添加 AOP 配置来告诉 Spring,无论何时 Spring 管理的 Person 对象的 birthday() 方法被调用了,就调用 BirthdayCardSenderbean 的 onBirthday() 方法。这是通过使用新的标签来实现的。首先,我们必须导入方便的 AOP 命名空间:

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

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

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

接着我们使用新的 AOP 标签,就像下面这样。这是应用这个切面的完整的配置:

标签适用于 after 通知。它指定方法调用 onBirthday(),也就是通知。它指明了什么时候调用这个方法——切入点——以一个 AspectJ 切入点表达式的形式。

切入点表达式是关键。让我们进一步来研究它。

execution(void com.interface21..Person.birthday()) and this(person)

execution() 前缀表明我们正在匹配某个方法的执行。也就是说,我们在改变方法的行为。

execution() 一句的类型定义了匹配的方法。我们可以在这编写一个表达式来匹配许多类的方法的集合。(实际上,那是一种更普遍更有价值的用法:当通知只匹配一个方法,实际上也没什么意义)最后,我们把被调用的对象绑定到 onBirthday() 方法的参数。这样缩小了切入点,可以仅仅匹配到一个 Person 对象的执行——而且提供一种优雅的方式来获得被调用的对象,而不需要任何查找。我们可以使用参数绑定来绑定方法参数,返回类型和异常,以及我们希望的目标。去掉查找的代码应该听起来很熟悉:这是对被通知的类型的依赖有效的注入!在 Spring 的精神中,它去掉了一个 API,无意间却也使单元测试通知方法更简单。

如果切入点表达式匹配一个或多个方法,任何定义在 Spring 上下文中的 Person 会被自动代理。那些没有包含对于这个切入点的匹配的类的 Bean 不会受到影响,匹配切入点的没有被 Spring 容器实例化的对象也一样(不会受到影响)。这个切面配置是设计用来添加到上面 Ioc 实例中已有的 bean 配置,或者在一个额外的 XML 文件中,或者在同一个文件中。提示一下,Person 定义成如下这样:

<bean class="com.interface21.spring2.ioc.Person"

p:name="Tony"

p:age="53"

p:house-ref="number10"

/>

不需要任何配置来使 Person 或者其他的 bean 定义符合通知。当我们调用某个配置在 Spring 上下文中的 Person 对象的 birthday() 方法时,我们看到下面这样的输出:

I will send a birthday card to Tony; he has just turned 54

@AspectJ 语法

通知总是包含在 Java 方法中。但到目前为止,我们看到的是定义在 Spring XML 中的切入点。

AspectJ 5 也提供一种完美的解决方案,来定义切面,通知包含在方法中以及切入点在 Java 5 的注解中。这种切入点表达语言和 AspectJ 自己的语法一样,而且语义相同。但以这种风格——叫做 @AspectJ模型——切面能使用 javac 进行编译。

通过 @AspectJ 语法,特定于框架的注解存在于切面中,而不是业务逻辑中。没有必要把注解引入到业务逻辑中来驱动切面。

这种注解驱动的编程模型最先由 AspectWerkz 提出,在 2005 年早期它合并入 AspectJ 项目。在 AspectJ 中,@AspectJ 切面被加载时织入(load time weaving)运用:类加载器钩子修改正在加载的类的字节码,来应用这些切面。AspectJ 编译器也明白这些切面,因此有个实现策略的选择。

Spring 2.0 为 @AspectJ 切面提供一种额外的选择:Spring 可以使用它的基于代理的 AOP 运行时来将这样的切面应用到 Spring beans。

让我们看一下,我们早先例子中同样的功能是如何使用这种风格实现的。

这个切面类包含和 BirthdayCardSender 类相同的通知方法体,但使用 org.aspectj.lang.annotation.Aspect 注解来把它识别为一个切面。在同一个包中更多的注解定义了通知方法。

@Aspect

public class AnnotatedBirthdayCardSender {

@After("execution(void com.interface21..Person.birthday()) and this(person)")



public void onBirthday(Person person) {

System.out.println("I will send a birthday card to " +

person.getName() + "; he has just turned " +

person.getAge());

}

}

这将切入点和通知方法结合在一起,使切面成为一个完整的模块。@AspectJ 切面, 就像所有的 AspectJ 切面,可以包含任意数目的切入点和通知方法。

在 Spring XML 中,我们再次把这个定义成一个 bean,并添加一个额外的标签来引起自动应用切面。

<aop:aspectj-autoproxy />

<bean id="birthdayCardSenderAspect"



class="com.interface21.spring2.aop.AnnotatedBirthdayCardSender" />

这个自动代理 aspectj 标签告诉 Spring 自动识别 @AspectJ 切面,并把它们应用到任何与它们的切入点相匹配的相同上下文中 bean 中。

在 XML 和 @AspectJ 风格之间选择

哪个方法更好呢——XML 还是注解?首先,因为注解存在于切面中,而不是核心业务逻辑,所以转换的成本并不高。决定通常取决于使切入点描述完全从 Java 代码具体出来是否有意义。

如果你的切面是特定领域的,就考虑注解风格:也就是说,切入点和通知是紧密相联的,而且通知并不普通,不可能在不同的场景中重复使用。比如说,发送生日贺卡对于注解风格是个很好的选择 ,因为它是特定于一个特殊应用类(Person)的。然而,一个性能监测切面可能在不同的应用中有不同的使用,通知方法最好从切入点解耦出来,切入点存在于 XML 配置中更自然一些。

使用 XML 如果:

  • 你不能使用 Java 5,而且没有其他选择。Spring 2.0 的 AOP 增强,除了能处理 @AspectJ 语法,也能工作在 Java1.3,1.4 以及 Java 5 上,虽然你不能编写切入点表达式匹配注解或者其他的 Java 5 结构。
  • 你可能想要在不同的上下文中使用通知。
  • 你想要使用已有代码作为通知,而且不想引入 AspectJ 注解:比如,引入一个 Observer 行为调用任意 POJO 的方法。

编程用法

你也可以以编程的方式创建 AOP 代理,像下面这样使用 @AspectJ 切面:

Person tony = new Person();

tony.setName("Tony");

tony.setAge(53);

AspectJProxyFactory ajpf = new AspectJProxyFactory(tony);



ajpf.addAspect(new AnnotatedBirthdayCardSender());

Person proxy = ajpf.getProxy();

AnnotatedBirthdayCardSender 会被自动识别为一个 @AspectJ 切面,而且代理会使用它定义的行为来修饰目标。这种风格并不需要 Spring Ioc 容器。

编程式的代理创建在编写基础结构代码和测试时比较有用,但在通常的业务应用中并不经常使用。

通过 AspectJ 切入点你能做的很棒的事情

到目前为止我们看到的仅仅是表面的东西。

让我们来看一些 AspectJ 切入点表达式更高级的能力,在 Spring XML 中,以 @AspectJ 风格(或者当然,AspectJ 语言本身):

  • 参数,目标,异常以及返回值绑定。
  • 从类型安全中受益,其中方法签名中的类型是在切入点中指定。
  • 用切入点表达式的组合来构建复杂的表达式。
  • 切入点表达式的重复使用。

我们已经见过了目标绑定。让我们举一个参数绑定的例子:

@Aspect

public class ParameterCaptureAspect {

@Before("execution(* *.*(String, ..)) && args(s)")



public void logStringArg(String s) {

System.out.println("String arg was '" + s + "'");

}

}

args() 子句绑定到被匹配的方法中的参数,缩小范围到具有第一个参数类型是 String 的方法。因为切入点绑定了第一个参数,必须是 String 类型的参数,在通知方法中做强制转换就没有必要了。

这种机制很自然地提供了类型安全。这个切入点的通知目标永远不可能被错误调用,通过错误类型的参数,或者没有匹配的参数。

为了给一个这种机制的优越性的例子,下面是在 Spring1.x MethodBefore 通知中看起来的样子。因为通过一个 AOP Alliance MethodInterceptor(在 Spring1.x 中最通用的接口),我们需要遍历一个参数数组来找出我们要寻找的参数:

public class ParameterCaptureInterceptor implements MethodBeforeAdvice {

public void before(Method method, Object[] args, Object target) throws Throwable {

if (args.length >= 1 && method.getParameterTypes()[0] == String.class) {

String s = (String) args[0];

System.out.println("String arg was '" + s + "'");

}

}

}

我们可以使用一个 Spring AOP 切入点来去除 MethodBefore 通知中的保护,但正如前面提到的,这样可能需要编写 Java 代码。在拦截器中有个保护比使用切入点要慢,因为它不允许 AOP 运行时优化出永远不能调用的通知。

在这里我们能够看到从拦截中去除 AOP 是多么的正确,以及为什么更简单和更强大。EJB3.0 拦截显然比 Spring 的第一代 AOP 功能更加糟糕,因为它缺少一个真实的切入点机制,这意味着 ClassCastException 和 ArrayIndexOutofBoundsException 很可能是风险。同样有必要使用 Around 通知(拦截器)而不是 Before 通知,因为 EJB3.0 没有提供特殊的通知类型。而且,需要提供一个 InvocationContext 对象使得单元测试通知方法困难得多。

AspectJ 切入点表达式语言的强大不仅仅是关于复杂的结构。它也在避免潜在的错误和应用程序更加智能化方面扮演重要的角色。它还能通过在通知方法中移除所需的保护代码,显著地减少所需代码的数量。

组合和重用是真实语言的特征。AspectJ 的主要目标就是把它们提供给切入点表达式。

让我们看一下实际中切入点的重用。

@AspectJ 语法(就像 Spring 2.0 的 AOP XML 格式)允许我们定义有名字的切入点。在 @AspectJ 语法,我们在一个 void 方法上使用 @Pointcut 注解,就像下面这样:

@Pointcut("execution(public !void get*())")

public void getter() {}

@Pointcut 注解允许我们定义切入点表达式,而且必要时,还有被切入点绑定的参数的个数和类型。方法名被用作切入点的名称。

上面的切入点匹配 JavaBean 的 getter 方法。请注意这里匹配的优势:我们不仅仅处理通配符,而且处理语言语义。这方法会把 getter 方法看作任何名称以 get 开头的方法。这种切入点要健壮得多,因为它断言 getter 是公有的,有一个非 void 的返回(!void)以及没有参数(通过对参数的圆括号来指明)。

让我们添加一种切入点来匹配返回 int 的方法:

@Pointcut("execution(public int *())")

public void methodReturningInt() {}

现在我们可以根据这些切入点来表达通知。我们第一个例子简单引用了我们的第一个有名称的切入点,“getter”:

@After("getter()")

public void getterCalled(JoinPoint jp) {

System.out.println("Method " + jp.getSignature().getName() +

" is a getter");

}

然而,现在事情变得更有趣了。我们通过把两个切入点加到一个表达式,对一个返回 in 的 getter 应用通知:

@After("getter() and methodReturningInt()")

public void getterCalledThatReturnsInt(JoinPoint jp) {

System.out.println("ANDing of pointcuts: Method " +

jp.getSignature().getName() +

" is a getter that also returns int");

}

ANDing 意味着两个切入点都必须应用。ORing 意味着必须应用其中一个切入点。我们可以构建我们需要的任何复杂程度的表达式。

在下面这个完整的切面中演示了 ANDing 和 ORing:

@Aspect public class PointcutReuse {

@Pointcut("execution(public !void get*())" )



public void getter() {}

@Pointcut("execution(public int *())" )



public void methodReturningInt() {}

@Pointcut("execution(public void *(..))" )



public void voidMethod() {}

@Pointcut("execution(public * *())" )



public void takesNoArgs() {}

@After("methodReturningInt()" )



public void returnedInt(JoinPoint jp) {

System.out .println("Method " + jp.getSignature().getName() +

" returned int" );

}

@After("getter()")

public void getterCalled(JoinPoint jp) {

System.out .println("Method " + jp.getSignature().getName() +

" is a getter"
);



}

@After("getter() and methodReturningInt()" )



public void getterCalledThatReturnsInt(JoinPoint jp) {

System.out.println("ANDing of pointcuts: Method " +

jp.getSignature().getName() +

" is a getter that also returns int");

}

@After("getter() or voidMethod()" )



public void getterOrVoidMethodCalled(JoinPoint jp) {

System.out .println("ORing of pointcuts: Method " +

jp.getSignature().getName() +

" is a getter OR is void" );

}

}

这会产生下面的输出,显示切入点表达式的 ORing 和 ANDing:

Method getName is a getter

ORing of pointcuts: Method getName is a getter OR is void

ORing of pointcuts: Method birthday is a getter OR is void

Method getName is a getter

ORing of pointcuts: Method getName is a getter OR is void

Method getAge returned int

Method getAge is a getter

ANDing of pointcuts: Method getAge is a getter that also returns int

ORing of pointcuts: Method getAge is a getter OR is void

I will send a birthday card to Tony; he has just turned 54

切入点组合同样可以在 Spring AOP XML 中完成。在那种情况下,使用“and”和“or”替代“&&”和“||”操作符来避免使用 XML 属性值的问题。

为高级用户重用 AspectJ 库切面

重用 AspectJ 语言编写的 AspectJ 切入点表达式,并编译成一个 JAR 文件是可能的。如果你使用 Eclipse,你可以使用 AJDT 插件开发这样的切面。或者,如果你已经在使用 AspectJ,你就已经拥有这样的切面而且想要重用它们。

作为演示,我会重写我们早先例子的一部分,把切入点放在一个 AspectJ 切面中:

public aspect LibraryAspect {

pointcut getter() :

execution(public !void get*());

...
}

这个切面用 Aspect 语言编写,因此需要由 AspectJ ajc 编译器来编译。请注意我们可以使用aspectpointcut关键词。

我们可以像下面这样,在被用在 Spring 中的 @AspectJ 切面中,引用这个切面。请注意我们使用了这个切面的 FQN,它通常会被打包在某个位于类路径的 JAR 文件中:

@Aspect

public class PointcutReuse {

@After("mycompany.mypackage.LibraryAspect.getter()")

public void getterCalled(JoinPoint jp) {

System.out.println("Method " + jp.getSignature().getName() +

" is a getter");

}

这个类,在另一方面,可以使用 javac 进行编译并由 Spring 应用。

你也可以在 Spring XML 中引用 AspectJ 切面。正如你能看到的,Spring 2.0 AOP 和 AspectJ 集成得非常紧密,虽然 Spring AOP 提供了一个完整的运行时,而不需要使用 AspectJ 编译器或编织器。

如果你有非常复杂的切入点表达式,那使用 AspectJ 库的切面是最好的实践,因为那样 Aspect 语言和工具支持是非常引人注目的。

最佳实践

那么这对 Spring 用户意味着什么?

很希望你同意 AOP 解决了企业软件中很重要的问题,而且你意识到 AspectJ 编程模型相比较 AOP 或者任何可用于拦截的选择,是多么的强大和优雅。

你没有必要把你已有的 Spring MethodInterceptors 或者其他的通知实现迁移到新的编程模型:它们仍然能工作的很好。但再往后,应该采用新的编程模型,它会更加引人注目。

如果你在使用 ProxyFactoryBean 或者 TransactionProxyFactoryBean 来一次一个地配置代理,你会发现自动代理(它在 Spring 2.0 中变得更容易更自然)能够显著减少 Spring 配置的工作量,以及在团队成员间更好的分工。

运行时看起来怎样?

虽然用法模型看起来不同,但记住概念很重要——连接点、切入点和通知——就跟 Spring 自 2003 年已经实现的 Spring AOP 和 AOP Alliance API 中的一模一样。Spring AOP 对这些概念一直有对应的接口。

也许更令人惊讶的是,表面以下的实现实际上更加相同。新的编程模型,支持 AspectJ 结构,构建于已有的 Spring AOP 运行时之上。Spring AOP 一直非常灵活,因此这不需要什么显著的改变。(org.springframework.aop.framework.Advised 接口仍然能够用来查询和修改 AOP 代理的状态,就像 Spring1.x 中那样。)

这意味着你能够使用 AspectJ 风格的切面,混合和匹配 AOP Alliance 和 Spring AOP 切面:如果你想要支持已有的切面这尤其重要。

让我们增加编程式创建代理的例子来演示这个。我们会增加一个传统的 Spring AOP 风格的 MethodInterceptor:

Person tony = new Person();

tony.setName("Tony");

tony.setAge(53);

AspectJProxyFactory ajpf = new AspectJProxyFactory(tony);



ajpf.addAspect(new AnnotatedBirthdayCardSender());

Person proxy = ajpf.getProxy();

ajpf.addAdvice(new MethodInterceptor() {

public Object invoke(MethodInvocation mi) throws Throwable {

System.out.println("MethodInterceptor: Call to " + mi.getMethod());

return mi.proceed();

}

});

这会产生下面的输出,来自于 MethodInterceptor 的输出(没有切入点,匹配所有的方法调用)和来自于 @AspectJ 风格编写的 BirthdayCardSender 的输出混杂在一起。

MethodInterceptor: Call to public void

com.interface21.spring2.ioc.Person.birthday()

MethodInterceptor: Call to public java.lang.String

com.interface21.spring2.ioc.Person.getName()

MethodInterceptor: Call to public int

com.interface21.spring2.ioc.Person.getAge()

I will send a birthday card to Tony; he has just turned 54

向着 AOP 统一

Spring 2.0 给 AOP 的世界带来一种新的受欢迎的统一。第一次,切面的实现独立于它的部署模型。我们看到的每一个 @AspectJ 例子都能使用 AspectJ 编译器编译,或者使用 AspectJ 加载时织入来使用,也能被 Spring 应用。在 Spring 的精神中,我们有一个能够跨越不同运行时场景的编程模型。

而且如果你想要采用 AspectJ 本身,这会有正常的进步,因为 Spring 把 AspectJ 切入点表达式概念带给了更广泛的受众。

你什么时候应该使用 AspectJ 呢?下面是一些指示:

  • 你想要通知细粒度的对象,它们可能不会被 Spring 容器实例化。
  • 除了公有方法的执行,你想要通知连接点,比如字段访问或者对象创建。
  • 你想要以透明的方式通知自调用。
  • 当一个对象需要被通知会被多次调用,而且不接受任何代理性能负载。(在这个基础上做决定前要小心基准:Spring AOP 代理的负载在通常使用中是无法觉察到的。)
  • 你想要使用 AspectJ 的能力来声明由编译器标记的警告或者错误。这对架构增强尤其有用。

没有必要有什么或者的选择。同时使用 AspectJ 和 Spring AOP 是可能的:它们并不冲突。

Java 5

Spring 2.0 保持向后对 Java1.3 和 1.4 的兼容。然而,Java 5 带来越来越多的新特性。

其中一些,比如已经讨论过的类型推断,可以随意使用。而其他的需要自己来选择。让我们快速预览其中的一些。

新的 API

大量的新的 API 在核心功能上提供 Java 5 的功能,这些核心功能继续运行在 Java 的早些版本上。

尤其是:

  • SimpleJdbcTemplate:和熟悉的 JdbcTemplate 类似的新类,这使得 JDBC 使用更加简单。
  • AspectJProxyFactory:和 ProxyFactory 类似的新类,设计用来使用 @AspectJ 切面以编程方式创建代理。

随着时间延续,这样的类的数目会越来越多。

SimpleJdbcTemplate 是用来例证的。让我们看一下在调用一个计算总数功能时的效果。在 Java 5 之前使用 JdbcTemplate,我们需要在一个数组中封装绑定参数,就像下面这样:

jdbcTemplate.queryForInt("SELECT COUNT(0) FROM T_CLIENT WHERE TYPE=? AND CURRENCY=?",

new Object[] { new Integer(13), "GBP" }

);

如果我们使用 Java 5,自动装箱去除了一点困扰,因为我们不再需要原始封装器类型。这仅仅来自于语言特性,而不需要 Spring 提供任何新的东西:

jdbcTemplate.queryForInt("SELECT COUNT(0) FROM T_CLIENT WHERE TYPE=? AND CURRENCY=?",

new Object[] { 13, "GBP" }

);

然而,通过采用 Java 5 我们能够完全不再需要对象数组。下面的例子显示了 SimpleJdbcTemplate 是如何使用变参来绑定变量的,意味着开发者可以提供任意数目的变参,而不需要数组:

simpleJdbcTemplate.queryForInt("SELECT COUNT(0) FROM T_CLIENT WHERE TYPE=? AND CURRENCY=?",

13, "GBP"

);

作为增加的一个好处,我们不再需要区分带有绑定和没绑定参数的情况。虽然这需要 JdbcTemplate 上的两个方法,来避免需要给执行 SQL 的重载方法传入一个空的 Object 数组,但通过 SimpleJdbcTemplate,框架代码可以检查变参的长度。这样下面的例子调用同一个方法:

simpleJdbcTemplate.queryForInt("SELECT COUNT(0) FROM T_CLIENT");

还有更显著的好处。泛型使得签名更加清晰,并去除强制转换。比如,JdbcTemplate 的 queryForMap() 方法返回一个 ResultSet 中的从列名到列值的 Map。当它是 SimpleJdbcTemplate 上方法签名的显式部分时,这就变得清楚得多:

public Map<String, Object> queryForMap(String sql, Object... args)

throws DataAccessException

如果是返回这种 map 的列表的方法,那还会更清楚:

public List<Map<String, Object>> queryForList(String sql, Object ... args)

throws DataAccessException

增加 SimpleJdbcTemplate 的目标之一是,提供那些最经常使用的方法。JdbcTemplate 是 Spring 中最大的类之一,有很多的方法,其中一些是用于非常深奥的用途。在这种高级的情况下,根据全部问题的复杂性,语言语法糖就可能不再重要。对于这样的案例,SimpleJdbcTemplate 封装了一个 JdbcTemplate 实例,通过 getJdbcOperations() 方法可以访问到。

为了支持扩展 DAO 支持类的用法模型,Spring 2.0 提供了 SimpleJdbcDaoSupport 类,提供一个预先配置的 JdbcTemplate。SimpleJdbcTemplate 也象 JdbcTemplate 一样很易于实例化或者直接注入,通过仅仅提供一个 javax.sql.Datasource 实现——这是 Spring 对所有关系型数据库访问的支持的开始点。就像 JdbcTemplate,SimpleJdbcTemplate 可以当作一个类库来使用,而无需使用 Spring 的其他部分。

注解

我们首先在 Spring1.2 中引入注解,作为一个可选的特性,而且我们逐渐增加更多。

我已经提到过来自于 AspectJ 的对 AOP 的注解的使用。它提供一种在单个代码模块中表达切入点和通知的优雅的方式。

Spring 还提供许多它自己的某些领域的注解,比如事务管理。下面这些在 1.2 中引入:

  • @Transactional: 标记一个类,接口或者方法为可事务的。
  • 不同的注解,包括在 org.springframework.jmx.export.annotation 包中的 @ManagedResource,识别操作、属性和对象来到处以便 JMX 管理。

下面是 2.0 中最重要的新的注解:

  • @Configurable:表明某个特殊的对象应该在构建后使用 Spring 依赖注入,虽然它不是由 Spring 实例化的。驱动一个 AspectJ DI 切面,这在本文的第二部分中描述。
  • @Required:指明所需的某个 JavaBean 的 setter 方法。为了采用这个增强,在你的应用上下文中定义 RequiredAnnotationBeanPostProcessor。在 Spring 的非入侵编程模型精神中,以及与已有代码一起共同工作的能力,Spring 也能加强其他注解的使用来指明某个所需的属性,通过 RequiredAnnotationBeanPostProcessor 的配置。
  • @Repository: 把一个 DAO 对象认作 Repository 模式(在领域驱动设计术语中)。Spring 2.0 提供了一个切面(PersistenceExceptionTranslationAdvisor),能自动把来自于用 @Repository 注解的对象的特定技术异常转换成 Spring 的普通的 DataAccessException。

对于 JPA 测试,Spring 的集成测试创建了一些超类,比如新的 AbstractJpaTest 和泛型超类 AbstractAnnotationAwareTransactionalTests,现在提供对于注解的支持,比如 @Repeat(引起重复测试)和 @ExceptedException(指明这个测试应该抛出一个特殊的异常,如果没有抛出就失败)。不幸的是,由于 JUnit3 的设计基于具体的继承,这些有用的注解对于使用 Spring 的其他测试不再有用。随着 JUnit4 的广泛使用,我们会提供我们集成测试的一个版本,它应该能够对其他的用户开放这个功能。

如果你想要解释自己的注解该怎么办呢?当然在这种情况下,Spring 的许多扩展钩子会帮上忙。比如说,你能编写一个 BeanPostProcessor 来使用给定的注解来区别方法,就跟 RequiredAnnotationBeanPostProcessor 一样的工作方式。用于即将发布的WebLogic10 的 Pitchfork 项目,使用这些扩展点在 Spring 之上实现了 JSR-250 注解和 EJB3.0 拦截注解。

同样值得注意的是,Spring 2.0 中提供的 AspectJ 切入点表达式语言有非常强大的注解匹配。很容易编写切入点来匹配注解。比如,下面的切入点表达式会匹配任何用 Spring 框架中的注解进行注解的方法:

execution(@(org.springframework..*) * *(..))

下面的表达式会匹配任何用 @Transaction 注解的类:

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

AspectJ 5 是 AspectJ 语言的主要扩展,并且在保持随着基础语言的进化而更新方面给人印象深刻,切入点表达式也能够匹配其他的 Java 5 结构,比如泛型和变参。

学习更多

在本系列的下一文章中,我会讨论:

  • 动态语言支持,以及 Spring 2.0 中组件模型如何变成跨语言的。
  • 消息和异步调用支持。
  • 数据访问和与新的 Java 持久化 API(JPA)的集成。
  • Spring MVC 增强,主要集中在方便使用上。
  • 新的 Spring Porlet MVC 框架。
  • 对于领域对象的依赖注入的新可能性。

同时,如果要阅读更多我今天讨论的主题,我建议如下资源:

  • Spring 参考手册,它一直很棒,而且在 Spring 2.0 中有显著的提高。
  • 为了更好理解 AspectJ 的强大,有许多好的 AspectJ 的书籍,我推荐 Ramnivas Laddad 的 AspectJ in Action(Manning,2003)和 Adrian Colyer,Andy Clement,George Harley 和 Matthew Webster 的 Eclipse AspectJ(Addison-Wesley,2005)。
  • 为了理解 AspectJ 中的变化,许多书籍做了介绍,但同时AspectJ Development Kit Developer’s Notebook非常有用,尤其是“An Annotation Based Development Style”一章。

本文中的示例代码可以从这儿下载

查看英文原文:Spring 2.0: What's New and Why it Matters

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论