InfoQ: Spring 2.0的新特性和应用实践

来源:百度文库 编辑:神马文学网 时间:2024/04/26 18:12:49
主题
Spring开源项目开始于2003年2月,现在框架正变得越来越强大。目前已经达到了超过一百万的下载量;在很多行业范围内成为事实上的标准;并且改变了企业Java应用的开发过程。
最重要的是,它发展了大量而且忠诚的用户,他们理解框架的关键价值并且共享反馈,来帮助框架高速发展。Spring的使命也一直很清晰:
提供一种非入侵性编程模型。应用程序的代码应该尽可能地与框架解耦。
为内部基础设施提供一种优秀的解决方案,以便开发者能将注意力放在交付业务价值,而不是解决普通的问题。
使开发企业应用程序尽可能简单、增强,而不是减弱、机械。
在2006年10月份进入最终版的Spring 2.0,更加提升了这些价值。在核心团队在2005年佛罗里达Spring体验大会前研究发展特性时,我们发布了两个关键的主题——简易性和强大——突出作为Spring 2.0的主线,并且依旧忠实于Spring的使命。
某些决定很容易。从一开始,我们很清楚Spring2.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:



这当然要好过以前那些实现Service Locator的令人厌恶的日子,但它并不完美。我们会一直使用这个相同的类。并且(除非我们使用Java 5)没有一种机制来指明“jndiName”属性是必需的,而其他的属性都是可选的。
Spring 2.0添加了一个方便的“jee”命名空间,其中包括允许我们执行同样的JNDI lookup的标签,就像下面这样:

这个比较简单而且清楚。它清晰地表达了意图而且更加简洁。而且schema抓住了jndiName属性是必需的这样一个事实,并有方便的工具支持。其他可选的属性同样在这个schema中得以表达,就像下面这个例子:
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定义。
从Spring1.2开始,Spring就能够识别@Transactional注解,并通过代理来使受影响的bean变成事务。这导致了一种简单的部署模型——简单地添加注解过的bean,它们就能自动变得可以处理事务——但是建立这样模型需要一些令人心烦的戏法。为了所需的合作对象,需要三个bean定义——一个Spring AOPAdvisor,一个TransactionInterceptor,以及一个DefaultAdvisorAutoProxyCreator来引发自动的代理。这些bean定义组成了一个咒语,能够在不同的应用程序以不变的状态使用,但是暴露了多过用户不需要知道的细节:
class="org.springframework...DefaultAdvisorAutoProxyCreator"/>



class="org.springframework...TransactionInterceptor">
ref="transactionManager"/>

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



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

这个简便的标签达到了相同的效果。这个标签清楚地表达了意图——自动识别事务注解。那三个bean定义仍然通过底层的扩展标签来创建,但那现在是框架的事情了,而不是用户的。
Spring 2.0扩展标签可以在必要时定义它自己的attribute以及子元素结构。定义命名空间的开发者完全可控。处理Spring 2.0扩展标签的NamespaceHandler可以从一个扩展标签创建任意数目的bean定义。
为了使用扩展标签,要使用XML schema而不是DTD,并且导入相关的命名空间。缺省的命名空间应该是beans schema。下面的“tx”命名空间的例子,允许使用

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 forSpring(将在2007年早些时候改名为Spring Security),它需要配置一些协作bean的定义。Spring2.0的命名空间会使这变得非常简单。再一次更加清楚地表达了简单性的意图。
许多产品和Spring紧密集成,这样的好处不言而喻。Tangosol对Coherence的集成就是现成的案例。
其他潜在的例子包括支持Spring配置的产品,比如IBM的ObjectGrid。虽然ObjectGrid目前没有在内部使用Spring,但它被设计成通过Java来配置,使得能更加容易地集成到基于Spring的应用程序中。扩展schema会让这个变得相当简单。
一个XML文档使用某个扩展标签作为顶层的元素是可能的。这样避免需要通过命名空间给扩展schema元素加上前缀,意味着这样的配置看起来更自然一些,而非以Spring为中心的。(通常,元素是在缺省的命名空间,因此传统的Spring bean定义并不需要前缀。)
随着时间过去,和JSP自定义标签的发展,经验会通过实证明了的价值引出通用目的的标签。我们期望用户来创建命名空间的库,来让这个社区受益。
实现XML扩展
实现命名空间相对简单。它分三步:
定义你的XML schema。这是最困难的一步,需要有合适的工具。对于schema没有限制,当然你需要明白它是如何在运行时引导BeanDefinition的生成的。
实现NamespaceHandler接口,从你的schema中的元素和属性来产生BeanDefinition。
编辑一个专门的注册文件,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定义,就像下面这个创建作用域内的代理的语法所显示的:



标签用来修饰包含它的标准的元素;如果它能够访问BeanDefinition,它就能对BeanDefinition进行修改。
为了简化BeanDefinition元数据的生成,Spring2.0引入了一种方便的新的BeanDefinitionBuilder类,提供一种流畅的、构建器风格的API。开始实现NamespaceHandlers的最佳指导是那些存在于Spring核心中的类。其中UtilNamespaceHandler是个相对简单的例子;而AopNamespaceHandler是个比较高级的例子,它解析了一个复杂的子元素结构。
最佳实践:什么时候应该定义你自己的命名空间?
你有锤子并不意味着其他一切都是钉子。正如我们已经看到的,Spring2.0的XML扩展机制在很多案例中交付了很大的价值。然而,如果没有很好的理由就不应该使用它。因为XML扩展标签是一种新的抽象,它同样提供了一些需要学习的新的内容。Spring的正规的XML格式对成千上万的开发者来说已经很熟悉了,甚至对那些新接触Spring的人都是用直觉就可以判断的。Spring XML文件提供了易于理解的某个应用程序结构的蓝图。如果过度配置使用了不熟悉的自定义标签,就没什么必要了。
让我们在这个领域内考虑一些相关的经验。JSP自定义标签是个很好的例子。最终它们通过设计得很棒的标签库,比如JSTL,Struts和SpringMVC的标签库,产生了真实的价值。但在早些年,它们会引起厌恶,甚至是混乱的JSP页面。(我在这里可以根据经验来解释,因为我自己实现了一两个这样的标签库)。
把命名空间处理器看作是一个重要的新的扩展点,以及对Spring很有价值的新的抽象。它们对于那些在Spring之上构建第三方产品的人来说非常棒;它们对于非常大型的项目也很有用。很快就会发现,没有了它们,很难想象生活会变成什么样子。但是最终用户还是应该对实现它们持谨慎态度,但使用没有问题。
当然,伴随Spring提供的方便的扩展标签,比如aop,tx以及jee命名空间,将很快成为Spring配置词表的核心部分,就跟元素一样被广泛了解。你当然应该优先使用这些,而不是传统的冗长的方式,来完成相同的任务。语法糖
转向使用schema也允许一点点快捷方式,比如对property值使用attribute而不是子元素。这些attribute不会被验证,但因为我们使用的是XMLschema,而不是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进行配置:

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">
p:name="Tony"
p:age="53"
p:house-ref="number10"
/>
id="number10"
p:name="10 Downing Street"
/>

请注意property是如何通过使用attribute来提供的,而不是元素。这样使用了特殊命名空间“p”的魔力。这个命名空间并没有验证,但允许使用attribute名称来和Java property名称进行匹配。
通过简单的几句,我们就简化了使用“p”命名空间中的property名称。就像“p:name”。当注入对其他Spring bean的引用时,使用“-ref”后缀,就像“p:house-ref”。
这种快捷的语法在你想要使用autowiring时尤其方便。比如,考虑下面的变量:
autowire="byType"
p:name="Tony"
p:age="53"
/>
这里我们没有设置“house”property,因为autowiring会考虑它。你甚至可以使用在元素层次使用default-autowire来在整个文件使用autowiring。
下面这个来自于Spring 1.0或者 1.1的用法演示了Spring配置在最近两个主要的发布版本中(1.2和2.0)减少了多少数目的尖括号。

"Tony"
"53"


在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发送事件通知——比如,关闭任何可以管理的资源,像连接池。
Prototypebean是任何时候通过注入到另外一个bean而被引用,或者是对所属容器上getBean()调用的响应时创建的。在这种情况下,bean定义与一个单个对象没有关系,而是一个用来创建对象的配方。每个创建好的实例会有完全相同的配置,但会有不同的身份。Spring容器不会持有对原型的引用;它的生命周期由获得它的那段代码来负责。
在Spring 2.0中,我们添加了自定义作用域的能力。可以给它们起任何名字。某个自定义作用域通常与能够管理对象实例的后端存储相对应。在这种情况下,Spring提供了它熟悉的编程模型,支持注入和查找,而且后端存储提供了作用域内对象的实例管理。
典型的后端存储有:
HTTP session
Clustered cache
其他持久化存储
2.Web作用域
对于这个特性,最通常的需求是关于在web应用程序HTTP session中透明存储对象。这在Spring 2.0中得到很方便的支持。因为这种需求很普通,所以举例会很容易:
考虑下面这个bean的定义:

这里我们指定了“session”作用域而不是缺省的“singleton”。作用域可以指定成任何名字,但在Web应用程序中提供“session”和“request”便于使用。
当我们通过名称“userPreferences”调用getBean()时,Spring会很明显地从当前HTTPSession中找出UserPreference对象。如果没找到UserPreferences对象,Spring就会创建一个。注入使得可以为用户描绘一个可以自定义的预配置的UserPreferences。
为了让它工作起来,你需要在你的web.xml文件中像下面这样定义一个过滤器。它会在底层处理一个ThreadLocal绑定,以便Spring能够知道去哪个HTTP Session对象中去查找。

...


org.springframework.web.context.request.RequestContextListener>


...

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









现在解析会如期动态地发生;UserController中的userPreferences实例变量就会是从HTTPSession中解析出正确UserPreferences对象的代理。我们不需要直接跟HTTPSession对象打交道,而且可以轻松地对UserController进行单元测试,而不需要一个mock HttpSession对象。
另外一种获得“非常及时”的注入的方式是使用一个查找方法。这是一个自从Spring1.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配置看起来应该是这样:







这种机制需要类路径中包含CGLIB。
3.其他可能性
无疑,在真实的Spring风格中,底层的机制是可插拔的,而不是绑定到Web层的。比如,Tangosol Coherence作用域可以像下面这样使用,通过Tangosol和Interface21提供的“datagrid”命名空间。

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”作用域内声明一个bean,意味着bean的状态管理是由Coherence来执行,在一个ClusteredCache中,而不是在本地的服务器上。当然,bean是由Spring来实例化和注入的,而且可以从Spring服务中受益。我们预见特定作用域内的bean会在SOA和批处理的环境中非常有用,并且期望能在将来Spring的版本中添加更多方便的bean作用域。
4.自定义作用域
定义你自己的作用域很简单。参考手册详细解释了这个过程。你将需要一种策略来辨别如何在当前的作用域内解析出对象。典型的情况下,这会涉及到ThreadLocal,就像Web作用域在底层所做的那样。
类型推断
如果你运行的是Java 5,Spring 2.0会从它新的能力中获益,比如泛型。比如,考虑一下下面这个类:
public class DependsOnLists {
private List plainList;
private List floatList;
public List getFloatList() {
return floatList;
}
public void setFloatList(List floatList) {
this.floatList = floatList;
}
public List getPlainList() {
return plainList;
}
public void setPlainList(List plainList) {
this.plainList = plainList;
}
}
“plainList”属性是个旧式风格的集合,而“flodatList”属性有个类型化的说明。
考虑下面的Spring配置:



1
2
3




1
2
3



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 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模块的设计,我们过去不能在SpringDTD中提供特定于AOP的标签——因此在这种情况下需要依赖可以详细一点的通用配置。随着Spring 2.0的出现,这样的问题没有了,因为XMLschema并不像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命名空间:

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定义成如下这样:
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,并添加一个额外的标签来引起自动应用切面。

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通知中看起来的样子。因为通过一个AOPAlliance 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编译器来编译。请注意我们可以使用aspect和pointcut关键词。
我们可以像下面这样,在被用在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结构,构建于已有的SpringAOP运行时之上。SpringAOP一直非常灵活,因此这不需要什么显著的改变。(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统一
Spring2.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 queryForMap(String sql, Object... args)
throws DataAccessException
如果是返回这种map的列表的方法,那还会更清楚:
public List> queryForList(String sql, Object ... args)
throws DataAccessException
增加SimpleJdbcTemplate的目标之一是,提供那些最经常使用的方法。JdbcTemplate是Spring中最大的类之一,有很多的方法,其中一些是用于非常深奥的用途。在这种高级的情况下,根据全部问题的复杂性,语言语法糖就可能不再重要。对于这样的案例,SimpleJdbcTemplate封装了一个JdbcTemplate实例,通过getJdbcOperations()方法可以访问到。
为了支持扩展DAO支持类的用法模型,Spring2.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结构,比如泛型和变参。