译者:yanger,taowen
校对:taowen
关于Spring Framework,今年夏天你可能已经听见很多的议论。在本文中,我将试图解释Spring能完成什么,和我怎么会认为它能帮助你开发J2EE应用程序。
又来一个framework?
你可能正在想“不过是另外一个的framework”。当已经有许多开放源代码(和专有) J2EE framework时,为什么你还要耐下心子读这篇文章或去下载Spring Framework?
我相信Spring是独特的,有几个原因:
它关注的领域是其他许多流行的Framework未曾关注的。Spring要提供的是一种管理你的业务对象的方法。
Spring既是全面的又是模块化的。Spring有分层的体系结构,这意味着你能选择仅仅使用它任何一个独立的部分,而它的架构又是内部一致。因此你能从你的学习中,得到最大的价值。例如,你可能选择仅仅使用Spring来简单化JDBC的使用,或用来管理所有的业务对象。
它的设计从一开始就是要帮助你编写易于测试的代码。Spring是使用测试驱动开发的工程的理想框架。
Spring不会给你的工程添加对其他的框架依赖。Spring也许称得上是个一站式解决方案,提供了一个典型应用所需要的大部分基础架构。它还涉及到了其他framework没有考虑到的内容。
尽管它仅仅是一个从2003年2月才开始的开源项目,但Spring有深厚的历史根基。这个开源工程是起源自我在2002年晚些时候出版的《Expert One-on-One J2EE设计与开发》书中的基础性代码。这本书展示了Spring背后的基础性架构思想。然而,对这个基础架构的概念可以追溯到2000年的早些时候,并且反映了我为一系列商业工程开发基础结构的成功经验。
2003年1月,Spring已经落户于SourceForge上了。现在有10个开发人员,其中6个是高度投入的积极分子。
Spring架构上的好处
在我们进入细节之前,让我们来看看Spring能够给工程带来的种种好处:
Spring能有效地组织你的中间层对象,不管你是否选择使用了EJB。如果你仅仅使用了Struts或其他为J2EE的 API特制的framework,Spring致力于解决剩下的问题。
Spring能消除在许多工程中常见的对Singleton的过多使用。根据我的经验,这是一个很大的问题,它降低了系统的可测试性和面向对象的程度。
通过一种在不同应用程序和项目间一致的方法来处理配置文件,Spring能消除各种各样自定义格式的属性文件的需要。曾经对某个类要寻找的是哪个魔法般的属性项或系统属性感到不解,为此不得不去读Javadoc甚至源编码?有了Spring,你仅仅需要看看类的JavaBean属性。Inversion of Control的使用(在下面讨论)帮助完成了这种简化。
通过把对接口编程而不是对类编程的代价几乎减少到没有,Spring能够促进养成好的编程习惯。
Spring被设计为让使用它创建的应用尽可能少的依赖于他的APIs。在Spring应用中的大多数业务对象没有依赖于Spring。
使用Spring构建的应用程序易于单元测试。
Spring能使EJB的使用成为一个实现选择,而不是应用架构的必然选择。你能选择用POJOs或local EJBs来实现业务接口,却不会影响调用代码。
Spring帮助你解决许多问题而无需使用EJB。Spring能提供一种EJB的替换物,它们适用于许多web应用。例如,Spring能使用AOP提供声明性事务管理而不通过EJB容器,如果你仅仅需要与单个数据库打交道,甚至不需要一个JTA实现。
Spring为数据存取提供了一个一致的框架,不论是使用的是JDBC还是O/R mapping产品(如Hibernate)。
Spring确实使你能通过最简单可行的解决办法来解决你的问题。而这是有有很大价值的。
Spring做了些什么?
Spring提供许多功能,在此我将依次快速地展示其各个主要方面。
任务描述
首先,让我们明确Spring范围。尽管Spring覆盖了许多方面,但我们对它应该涉什么,什么不应该涉及有清楚的认识。
Spring的主要目的是使J2EE易用和促进好编程习惯。
Spring不重新轮子。因此,你发现在Spring中没有logging,没有连接池,没有分布式事务调度。所有这些东西均有开源项目提供(例如我们用于处理所有日志输出的Commons Logging以及Commons DBCP),或由你的应用程序服务器提供了。出于同样的的原因,我们没有提供O/R mapping层。对于这个问题已经有了像Hibernate和JDO这样的优秀解决方案。
Spring的目标就是让已有的技术更加易用。例如,尽管我们没有底层事务协调处理,但我们提供了一个抽象层覆盖了JTA或任何其他的事务策略。
Spring没有直接和其他的开源项目竞争,除非我们感到我们能提供新的一些东西。例如,象许多开发人员一样,我们从来没有对Struts感到高兴过,并且觉得到在MVC web framework中还有改进的余地。在某些领域,例如轻量级的IoC容器和AOP框架,Spring确实有直接的竞争,但是在这些领域还没有已经较为流行的解决方案。(Spring在这些领域是开路先锋。)
Spring也得益于内在的一致性。所有的开发者都在唱同样的的赞歌,基础想法依然与Expert One-on-One J2EE设计与开发中提出的差不多。 并且我们已经能够在多个领域中使用一些中心的概念,例如Inversion of Control。
Spring在应用服务器之间是可移植的。当然保证可移植性总是一种挑战,但是我们避免使用任何平台特有或非标准的东西,并且支持在WebLogic,Tomcat,Resin,JBoss,WebSphere和其他的应用服务器上的用户。
Inversion of Control 容器
Spring设计的核心是 org.springframework.beans 包, 它是为与JavaBeans一起工作而设计的。 这个包一般不直接被用户使用,而是作为许多其他功能的基础。
下一个层面高一些的抽象是"Bean Factory"。一个Spring bean factory 是一个通用的Factory,它使对象能够按名称获取,并且能管理对象之间的关系。
Bean factories 支持两种模式的对象:
Singleton:在此模式中,有一个具有特定名称的共享对象实例,它在查找时被获取。这是默认的,而且是最为经常使用的。它对于无状态对象是一种理想的模式。
Prototype:在此模式中,每次获取将创建一个独立的对象。例如,这可以被用于让用户拥有他们自己的对象。
由于 org.springframwork.beans.factory.BeanFactory是一个简单的接口,它能被大量底层存储方法实现。你能够方便地实现你自己的BeanFactory,尽管很少用户需要这么做。最为常用的BeanFactory定义是:
XmlBeanFactory: 可解析简单直观的定义类和命名对象属性的XML结构。 我们提供了一个DTD来使编写更容易。
ListableBeanFactoryImpl:提供了解析存放在属性文件中的bean定义的能力,并且可通过编程创建BeanFactories。
每个bean定义可能是一个POJO(通过类名和JavaBean初始属性定义),或是一个FactoryBean。FactoryBean接口添加了一个间接层。通常,这用于创建使用AOP或其他方法的代理对象:例如,添加声明性事务管理的代理。(这在概念上和EJB的interception相似,但实现得更简单。)
BeanFactories能在一个层次结构中选择性地参与,继承ancestor(祖先)的定义。这使得在整个应用中公共配置的共享成为可能,虽然个别资源,如controller servlets,还拥有他们自己的独立的对象集合。
这种使用JavaBeans的动机在《Expert One-on-One J2EE Design and Development》的第四章中有描述,在TheServerSide网站上的有免费的PDF版本(http://www.theserverside.com/resources/article.jsp?l=RodJohnsonInterview)。
通过BeanFactory概念,Spring成为一个Inversion of Control的容器。(我不怎么喜欢container这个词,因为它使人联想到重量级容器,如EJB容器。Spring的BeanFactory是一个可通过一行代码创建的容器,并且不需要特殊的部署步骤。)
Inversion of Control背后的概念经常表述为Hollywood原则的:“Don’t call me, I’ll call you。” IoC将控制创建的职责搬进了框架中,并把它从应用代码脱离开来。涉及到配置的地方,意思是说在传统的容器体系结构中,如EJB,一个组件可以调用容器并问“我需要它给我做工作的对象X在哪里?”;使用IoC容器则只需指出组件需要X对象,在运行时容器会提供给它。容器是通过查看方法的参数表(例如JavaBean的属性)做到的,也可能根据配置数据如XML。
IoC有几个重要的好处,例如:
因为组件不需要在运行时间寻找合作者,所以他们可以更简单的编写和维护。在Spring版的IoC里,组件通过暴露JavaBean的setter方法表达他们依赖的其他组件。这相当于EJB通过JNDI来查找,EJB查找需要开发人员编写代码。
同样原因,应用代码更容易测试。JavaBean属性是简单的,属于Java核心的,并且是容易测试的:仅编写一个自包含的Junit测试方法用来创建对象和设置相关属性即可。
一个好的IoC实现保留了强类型。如果你需要使用一个通用的factory来寻找合作者,你必须通过类型转换将返回结果转变为想要的类型。这不是一个大不了的问题,但是不雅观。使用IoC,你在你的代码中表达了强类型依赖,框架将负责类型转换。这意味着在框架配置应用时,类型不匹配将导致错误;在你的代码中,你无需担心类型转换异常。
大部分业务对象不依赖于IoC容器的APIs。这使得很容易使用遗留下来的代码,且很容易的使用对象无论在容器内或不在容器内。例如,Spring用户经常配置Jakarta Commons DBCP数据源为一个Spring bean:不需要些任何定制代码去做这件事。我们说一个IoC容器不是侵入性的:使用它并不会使你的代码依赖于它的APIs。任何JavaBean在Spring bean factory中都能成为一个组件。
最后应该强调的是,IoC 不同于传统的容器的体系结构,如EJB,应用代码最小程度地依靠于容器。这意味着你的业务对象可以潜在的被运行在不同的IoC 框架上——或者在任何框架之外——不需要任何代码的改动。
以我和其他Spring用户的经验来说,再怎么强调IoC给应用程序代码带来的好处也不为过。
IoC不是一个新概念,但是它在J2EE团体里面刚刚到达黄金时间。 有一些可供选择的IoC 容器: 例如 Apache Avalon, PicoContainer 和 HiveMind。Avalon 从没怎么流行,尽管它很强大而且有很长的历史。Avalon相当的重和复杂,并且看起来比新的IoC解决方案更具侵入性。 PicoContainer是一个轻量级而且更强调通过构造函数表达依赖性而不是JavaBean 属性。 与Spring不同,它的设计允许每个类型一个对象的定义(可能是因为它拒绝任何Java代码外的元数据导致的局限性)。在Spring, PicoContainer 和其他 IoC frameworks之间做比较,可参看文章Spring网站上的"The Spring Framework - A Lightweight Container"位于http://www.springframework.org/docs/lightweight_container.html。这个页面里面包含了PicoContainer站点的链接 。
Spring BeanFactories 是非常轻量级的。用户已经成功地将他们应用在applets和单独的Swing应用中。(它们也很好地工作在EJB容器中。) 没有特殊的部署步骤和察觉得到的启动时间。这个能力表明一个容器在应用的任何层面几乎立即可以发挥非常大的价值。
Spring BeanFactory 概念贯穿于Spring始终, 而且是Spring如此内在一致的关键原因。在IoC容器中,Spring也是唯一的,它使用IoC作为基础概念贯穿于整个功能丰富的框架。
对应用开发人员,最重要的是,一个或多个BeanFactory提供了一个定义明确的业务对象层。这类似于local session bean层,但比它更简单。与EJBs不同,在这个层中的对象可能是相关的,并且他们的关系被拥有它们的factory管理。有一个定义明确的业务对象层对于成功的体系结构是非常重要的。
Spring ApplicationContext 是BeanFactory的子接口,为下列东西提供支持:
信息查找,支持着国际化
事件机制,允许发布应用对象以及可选的注册以接收到事件
可移植的文件和资源访问
XmlBeanFactory 例子
Spring用户通常在XML的“bean定义”文件中配置他们的应用。Spring的XML bean定义文档的根是<beans> 元素。该元素包含一个或多个 <bean>定义。我们一般给每个bean定义的指定类和属性。我们还必须指定ID作为标识,这将成为在代码中使用该bean的名字。
让我们来看一个简单的例子,它配置了三个应用程序对象,之间的关系在J2EE应用中常常能够看到:
J2EE DataSource
使用DataSource的DAO
在处理过程中使用DAO的业务对象
在下面的例子中,我们使用一个来自Jakarta Commons DBCP项目的BasicDataSource。这个class(和其他许多已有的class一样)可以简单地被应用在Spring bean factory中,只要它提供了JavaBean格式的配置。需要在shutdown时被调用的Close方法可通过Spring的"destroy-method"属性被注册,以避免BasicDataSource需要实现任何Spring 的接口。
代码: |
<beans> <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName"><value>com.mysql.jdbc.Driver</value></property> <property name="url"><value>jdbc:mysql://localhost:3306/mydb</value></property> <property name="username"><value>root</value></property> </bean> |
BasicDataSource中我们感兴趣的所有属性都是String类型的,因此我们用<value>元素来指定他们的值。如果必要的话,Spring使用标准的 JavaBean属性编辑器机制来把String转换为其他的类型。
现在,我们定义DAO,它有一个对DataSource的bean引用。Bean间关系通过<ref>元素来指定:
代码: |
<bean id="exampleDataAccessObject" class="example.ExampleDataAccessObject"> <property name="dataSource"><ref bean="myDataSource"/></property> </bean> The business object has a reference to the DAO, and an int property (exampleParam): <bean id="exampleBusinessObject" class="example.ExampleBusinessObject"> <property name="dataAccessObject"><ref bean="exampleDataAccessObject"/></property> <property name="exampleParam"><value>10</value></property> </bean> </beans> |
对象间的关系一般在配置中明确地设置,象这个例子一样。我们认为这样做是件好事情。然而Spring还提供了我们称做"autowire"的支持, 一个la PicoContainer,其中它指出了bean间的依赖关系。这样做的局限性——PicoContainer也是如此——是如果有一个特殊类型的多个Bean,要确定那个类型所依赖的是哪个实例是不可能。好的方面是,不满足的依赖可以在factory初始化后被捕获到。(Spring 也为显式的配置提供了一种可选的依赖检查,它可以完成这个目的)
在上面的例子中,如果我们不想显式的编写他们的关系,可使用如下的autowire特性:
代码: |
<bean id="exampleBusinessObject" class="example.ExampleBusinessObject" autowire="byType"> <property name="exampleParam"><value>10</value></property> </bean> |
使用这个特性,Spring会找出exampleBusinessObject的dataSource属性应该被设置为在当前BeanFactory中找到的DataSource实现。在当前的BeanFactory中,如果所需要类型的bean不存在或多于一个,将产生一个错误。我们依然要设置exampleParam属性,因为它不是一个引用。
Autowire支持和依赖检查刚刚加入CVS并将在Spring 1.0 M2(到10/20,2003)中提供。本文中所讨论的所有其他特性都包含在当前1.0 M1版本中。
把管理从Java代码中移出来比硬编码有很大的好处,因为这样可以只改变XML文件而无需改变一行Java代码。例如,我们可以简单地改变myDataSource的bean定义引用不同的bean class以使用别的连接池,或者一个用于测试的数据源。 XML节变成另一种,我们可以用Spring的JNDI location FactoryBean从应用服务器获取一个数据源。
现在让我们来看看例子中业务对象的java 代码。注意下面列出的代码中没有对Spring的依赖。不像EJB容器,Spring BeanFactory不具有侵入性:在应用对象里面你通常不需要对Spring的存在硬编码。
代码: |
public class ExampleBusinessObject implements MyBusinessObject { private ExampleDataAccessObject dao; private int exampleParam; public void setDataAccessObject(ExampleDataAccessObject dao) { this.dao = dao; } public void setExampleParam(int exampleParam) { this.exampleParam = exampleParam; } public void myBusinessMethod() { // do stuff using dao } } |
注意那些property setter,它们对应于bean定义文档中的XML引用。这些将在对象被使用之前由Spring调用。
这些应用程序的bean不需要依赖于Spring:他们不需要实现任何Spring的接口或者继承Spring的类。他们只需要遵守JavaBeans的命名习惯。在Spring 应用环境之外重用它们是非常简单的,例如,在一个测试环境中。只需要用它们的缺省构造函数实例化它们,并且通过调用setDataSource()和setExampleParam()手工设置它的属性。如果你想以一行代码支持程序化的创建,只要你有一个无参数的构造器,你就可以自由定义其他需要多个属性的构造函数。
注意在业务接口中没有声明将会一起使用的JavaBean属性。 他们是一个实现细节。我们可以“插入”带有不同bean属性的不同的实现类而不影响连接着的对象或者调用的代码。
当然,Spring XML bean factories 有更多的功能没有在这里描述,但是,应当让你对基本使用有了一些感觉。以及,简单的属性,有JavaBean属性编辑器的属性,Spring可以自动处理lists,maps和java.util.Properties。
Bean factories 和application contexts 通常和J2EE server定义的一个范围相关联,例如:
Servlet context.:在spring 的MVC 框架里, 每一个包含common objects的web 应用都定义有一个应用程序的context。Spring提供了通过listener或者servlet实例化这样的context的能力而不需要依赖于Spring 的MVC 框架,因而它也可以用于Struts,WebWork 或者其他的web框架之中。
A Servlet:在Spring MVC 框架里每一个servlet控制器都有它自己的应用程序context,派生于根(全应用程序范围的)应用程序context。在Struts或者其他MVC框架中实现这些也很容意。
EJB:Spring 为EJB提供方便的超类,它们简化了EJB的创建并且提供了一个从EJB Jar 文件中的XML文档载入的BeanFactory。
这些J2EE规范提供的hook通常避免了使用Singleton来创造一个bean factory。
然而,如果我们愿意的话可以用代码创建一个BeanFactory,虽然是没有什么意义的。例如,我们在以下三行代码中可以创建bean factory并且得到一个业务对象的引用:
代码: |
InputStream is = getClass().getResourceAsStream("myFile.xml"); XmlBeanFactory bf = new XmlBeanFactory(is); MyBusinessObject mbo = (MyBusinessObject) bf.getBean("exampleBusinessObject"); |
这段代码将能工作在一个应用服务器之外:甚至不依赖J2EE,因为Spring 的IoC容器是纯java的。
JDBC 抽象和数据存储异常层次
数据访问是Spring 的另一个闪光点。
JDBC 提供了还算不错的数据库抽象,但是需要用痛苦的API。这些问题包括:
代码: |
JdbcTemplate template = new JdbcTemplate(dataSource); final List names = new LinkedList(); template.query("SELECT USER.NAME FROM USER", new RowCallbackHandler() { public void processRow(ResultSet rs) throws SQLException { names.add(rs.getString(1)); } }); |
注意回调中的程序代码是能够自由抛出SQLException的:Spring将会捕捉到这些异常并且用自己的类层次重新抛出。程序的开发者可以选择哪个异常,如果有的话,被捕捉然后处理。
JdbcTemplate提供许多支持不同情景包括prepared statements和批量更新的方法。Spring的JDBC抽象有比起标准JDBC来说性能损失非常小,甚至在当应用中需要的结果集数量很大的时候。
在org.springframework.jdbc.object包中是对JDBC的更高层次的抽象。这是建立在核心的JDBC回调功能基础纸上的,但是提供了一个能够对RDBMS操作——无论是查询,更新或者是存储过程——使用Java对象来建模的API。这个API部分是受到JDO查询API的影响,我发现它直观而且非常有用。
一个用于返回User对象的查询对象可能是这样的:
代码: |
class UserQuery extends MappingSqlQuery { public UserQuery(DataSource datasource) { super(datasource, "SELECT * FROM PUB_USER_ADDRESS WHERE USER_ID = ?"); declareParameter(new SqlParameter(Types.NUMERIC)); compile(); } // Map a result set row to a Java object protected Object mapRow(ResultSet rs, int rownum) throws SQLException { User user = new User(); user.setId(rs.getLong("USER_ID")); user.setForename(rs.getString("FORENAME")); return user; } public User findUser(long id) { // Use superclass convenience method to provide strong typing return (User) findObject(id); } } |
这个类可以在下面用上:
代码: |
User user = userQuery.findUser(25); |
这样的对象经常可以用作DAO的inner class。它们是线程安全的,除非子类作了一些超出常规的事情。
在org.springframework.jdbc.object包中另一个重要的类是StoredProcedure类。Spring让存储过程通过带有一个业务方法的Java类进行代理。如果你喜欢的话,你可以定义一个存储过程实现的接口,意味着你能够把你的程序代码从对存储过程的依赖中完全解脱出来。
Spring数据访问异常层次是基于unchecked(运行时)exception的。在几个工程中使用了Spring之后,我越来越确信这个决定是正确的。
数据访问异常一般是不可恢复的。例如,如果我们不能链接到数据库,某个业务对象很有可能就不能完成要解决的问题了。一个可能的异常是optimistic locking violation,但是不是所有的程序使用optimistic locking。强制编写捕捉其无法有效处理的致命的异常通常是不好的。让它们传播到上层的handler,比如servlet或者EJB 容器通常更加合适。所有的Spring对象访问异常都是DataAccessException的子类,因而如果我们确实选择了捕捉所有的Spring数据访问异常,我们可以很容易做到这点。
注意如果我们确实需要从unchecked数据访问异常中恢复,我们仍然可以这么做。我们可以编写代码仅仅处理可恢复的情况。例如,如果我们认为只有optimistic locking violation是可恢复的,我们可以在Spring的DAO中如下这么写:
代码: |
try { // do work } catch (OptimisticLockingFailureException ex) { // I'm interested in this } |
如果Spring的数据访问异常是checked的,我们需要编写如下的代码。注意我们还是可以选择这么写:
代码: |
try { // do work } catch (OptimisticLockingFailureException ex) { // I'm interested in this } catch (DataAccessException ex) { // Fatal; just rethrow it } |
第一个例子的潜在缺陷是——编译器不能强制处理可能的可恢复的异常——这对于第二个也是如此。因为我们被强制捕捉base exception(DataAccessException),编译器不会强制对子类(OptimisticLockingFailureException)的检查。因而编译器可能强制我们编写处理不可恢复问题的代码,但是对于强制我们处理可恢复的问题并未有任何帮助。
Spring对于数据访问异常的unchecked使用和许多——可能是大多数——成功的持久化框架是一致的。(确实,它部分是受到JDO的影响。)JDBC是少数几个使用checked exception的数据访问API之一。例如TopLink和JDO大量使用unchecked exception。Gavin King现在相信Hibernate也应该选择使用unchecked exception。
Spring的JDBC能够用以下办法帮助你:
O/R mapping 集成
当然你经常需要使用O/R mapping,而不是使用关系数据访问。你总体的应用程序框架也必须支持它。因而提供了对Hibernate 2.x和JDO的集成支持。它的数据访问架构使得它能和任何底层的数据访问技术集成。Spring和Hibernate集成得尤其好。
为什么你要使用Hibernate加Spring,而不是直接使用Hibernate?
Session 管理 Spring提供有效率的,简单的以并且是安全的处理Hibernate Session。使用Hibernate的相关代码为了效率和恰当的事务处理一般需要使用相同的Hibernate “Session”对象。Spring让它容易透明地创建和绑定Session到当前的线程,要么使用声明式,AOP的method interceptor方法,要么在Java代码层面使用显式的,“template”包装类。因而Spring解决了在Hibernate论坛上经常出现的用法问题。
资源管理 Spring的应用程序context能够处理Hiberante SessionFactories的位置和配置,JDBC数据源和其他相关资源。这使得这些值易于管理和改变。
集成的事务管理 Spring让你能够把你的Hibernate代码包装起来,要么使用声明式,AOP风格的method interceptor,要么在Java代码层面显式使用“template”包装类。在两种做法中,事务语义都为你处理了,并且在异常时也做好了恰当的事务处理(回滚,等)。如下面讨论的,你还获得了能够使用和替换不同transaction manager,而不会让你相关Hibernate代码受到影响的能力。额外的,JDBC相关的代码能够完全事务性的和Hibernate代码集成。这对于处理没有在Hibernate实现的功能很有用。
如上描述的异常包装 Spring能够包装Hibernate异常,把它们从私有的,checked异常转换为一套抽象的运行时异常。这使得你能够仅仅在恰当的层面处理大部分不可恢复的持久化异常,而不影响样板catch/throw,和异常声明。你仍然能够在任何你需要的地方捕捉和处理异常。记住JDBC异常(包括DB特有的方言)也被转换到相同的层次中,意味着你能在一致的编程模型中对JDBC执行相同的操作。
为了避免和厂商绑定 Hibernate是强大的,灵活的,开放源代码并且免费,但是它仍然使用私有的API。给出了一些选择,使用标准或者抽象API实现主要的程序功能通常是你想要的,当你需要因为功能,性能,或者其他考虑要转换到使用其他实现时。
让测试变简单 Spring的Inversion of Control方法使得改变Hibernate的session factories,数据源,transaction manager的实现和位置很容易,如果需要的话还能改变mapper object的实现。这使得更加容易分离和测试持久化相关的代码。
事务管理
抽象出一个数据访问的API是不够的;我们还需要考虑事务管理。JTA是显而易见的选择,但是它是一个直接用起来很笨重的API,因而许多J2EE开发者感到EJB CMT是对于事务管理唯一合理的选择。
Spring提供了它自己对事务管理的抽象。Spring提供了这些:
AOP
最近在应用AOP来解决企业关注点方面大家有了很大的兴趣,例如事务管理,这些都是EJB所要解决的。
Spring的AOP支持的首要目标是要给POJOs提供J2EE服务。这类似于JBoss 4的目标,Spring AOP由它能够在应用服务器之间移植的优势,因而没有绑死在厂商身上的风险。它既可以在web或者EJB容器中使用,也能够在WebLogic,Tomcat,JBoss,Resin,Jetty,Orion和许多其他应用服务器和web容器上使用。
Spring AOP支持method interception。所支持关键的AOP概念包括:
代码: |
<bean id="myTest" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces"> <value>org.springframework.beans.ITestBean</value> </property> <property name="interceptorNames"> <list> <value>txInterceptor</value> <value>target</value> </list> </property> </bean> |
注意bean类的定义总是AOP框架的ProxyFactoryBean,虽然bean的类型在引用中使用或者由BeanFactory的getBean()方法返回时依赖的是代理接口。(多个代理方法是被支持的。)ProxyFactoryBean的“interceptorNames”属性需要一个字符串列表。(因为如果代理是一个“prototype”而不是singleton,有状态interceptors可能需要创建新的实例,所以必须使用Bean的名字而不是bean的引用。)列表中的名字可以是interceptor或者pointcuts(interceptors和有关它们合适被使用的信息)。列表中的“target”值自动创建一个“invoker interceptor”封装target对象。实现代理接口的是在factory中的bean的名字。这个例子中的myTest可以和其他bean factory中的bean一样使用。例如,其他对象可以使用<ref>元素引用它而且这些引用是由Spring IoC设置的。
还可以不用BeanFactory,编程构建AOP代理,虽然这个很少用得上:
代码: |
TestBean target = new TestBean(); DebugInterceptor di = new DebugInterceptor(); MyInterceptor mi = new MyInterceptor(); ProxyFactory factory = new ProxyFactory(target); factory.addInterceptor(0, di); factory.addInterceptor(1, mi); // An "invoker interceptor" is automatically added to wrap the target ITestBean tb = (ITestBean) factory.getProxy(); |
我们相信最好把程序装配从Java代码中移出来,而AOP也不例外。
Spring在它的AOP能力方面的直接竞争者是Jon Tirsen的Nanning Aspects(http://nanning.codehaus.org)。
我觉得AOP作为EJB的替代无提供企业服务这个用法方面的进步是重要的。随着时间,这将成为Spring很重要的关注点。
MVC web 框架
Spring包括一个强大而且高度可配置的MVC web 框架。
Spring的MVC model类似于Struts。在多线程服务对象这点上,Spring的Controller类似于Struts Action,只有一个实例处理所有客户的请求。然而,我们相信Spring的MVC比起Struts有很多优点,例如:
代码: |
public class GoogleSearchController implements Controller { private IGoogleSearchPort google; private String googleKey; public void setGoogle(IGoogleSearchPort google) { this.google = google; } public void setGoogleKey(String googleKey) { this.googleKey = googleKey; } public ModelAndView handleRequest( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String query = request.getParameter("query"); GoogleSearchResult result = // Google property definitions omitted... // Use google business object google.doGoogleSearch(this.googleKey, query, start, maxResults, filter, restrict, safeSearch, lr, ie, oe); return new ModelAndView("googleResults", "result", result); } } |
这段代码使用的prototype中,IGoogleSearchPort是一个GLUE web services代理,由Spring FActoryBean返回。然而,Spring把controller从底层web service库中分离出来。接口可以使用普通的Java对象,test stub,mock对象或者如下面要讨论的EJB代理实现。这个contorller不包括资源查找;除了支持它的web交互的必要代码之外没有别的什么了。
Spring还提供了数据绑定,forms,wizards和更复杂的工作流的支持。
对Spring MVC 框架的优秀简介是Thomas Risberg的Spring MVC 教程(http://www.springframework.org/docs/MVC-step-by-step/Spring-MVC-step-by-step.html)。还可以参见“Web MVC with the Spring Framework”(http://www.springframework.org/docs/web_mvc.html)。
如果你乐于使用你钟情的MVC框架,Spring的分层架构使得你能够使用Spring的其他部分而不用MVC层。我们有使用Spring做中间层管理和数据访问,但是在web层使用Struts,WebWork或者Tapestry的用户。
实现EJB
如果你选择使用EJB,Spring能在EJB实现和客户端访问EJB两方面都提供很大的好处。
对业务逻辑进行重构,把它从EJB facades中取出到POJO已经得到了广泛的认同。(不讲别的,这使得业务逻辑更容易单元测试,因为EJB严重依赖于容器而难于分离测试。)Spring为session bean和message driver bean提供了方便的超类,使得通过自动载入基于包含在EJB Jar文件中的XML文档BeanFactory让这变得很容易。
这意味着stateless session EJB可以这么获得和使用所需对象:
代码: |
import org.springframework.ejb.support.AbstractStatelessSessionBean; public class MyEJB extends AbstractStatelessSessionBean implements MyBusinessInterface { private MyPOJO myPOJO; protected void onEjbCreate() { this.myPOJO = getBeanFactory().getBean("myPOJO"); } public void myBusinessMethod() { this.myPOJO.invokeMethod(); } } |
假定MyPOJO是一个接口,它的实现类——以及任何它需要的配置,注入基本的属性和更多的合作者——在XML bean factory 定义中隐藏。
我们通过在ejb-jar.xmldeployment descriptor中名为ejb/BeanFactoryPath的环境变量定义告诉Spring去哪儿装载XML文档。如下:
代码: |
<session> <ejb-name>myComponent</ejb-name> <local-home>com.test.ejb.myEjbBeanLocalHome</local-home> <local>com.mycom.MyComponentLocal</local> <ejb-class>com.mycom.MyComponentEJB</ejb-class> <session-type>Stateless</session-type> <transaction-type>Container</transaction-type> <env-entry> <env-entry-name>ejb/BeanFactoryPath</env-entry-name> <env-entry-type>java.lang.String</env-entry-type> <env-entry-value>/myComponent-ejb-beans.xml</env-entry-value></env-entry> </env-entry> </session> |
myComponent-ejb-beans.xml 文件将会从classpath装载:在本例中,是EJB Jar文件的根目录。每个EJB都能指定自己的XML文档,因而这个机制能在每个EJB Jar文件中使用多次。
Spring 的超类实现了EJB中诸如setSessionContext()和ejbCreate()的生命周期管理的方法,让应用程序开发者只需选择是否实现Spring的onEjbCreate()方法。
使用EJB
Spring还让实现EJB变得更加容易。许多EJB程序使用Service Locator和Business Delegate模式。这些比在客户代码中遍布JNDI查找强多了,但是它们常见的实现方式有显著的缺点,例如:
代码: |
private MyComponent myComponent; public void setMyComponent(MyComponent myComponent) { this.myComponent = myComponent; } |
我们随后在任何业务方法中使用这个实例变量。
Spring自动完称剩下的工作,通过像这样的XML bean定义。LocalStatelessSessionProxyFactoryBean是一个可以用于任何EJB的通用factory bean。它创建的对象能够自动被Spring转型为MyComponent类型。
代码: |
<bean id="myComponent" class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean"> <property name="jndiName"><value>myComponent</value></property> <property name="businessInterface"><value>com.mycom.MyComponent</value></property> </bean> <bean id="myController" class = "com.mycom.myController" > <property name="myComponent"><ref bean="myComponent"/></property> </bean> |
在幕后有许多魔法般的事情发生,Spring AOP framework的殷勤,虽然不强迫你使用AOP的概念享受这些结果。“myComponent”bean定义为EJB创建一个代理,它实现了业务方法的接口。EJB local home在启动的时候被缓存,因而只需要一次JNDI查找。每次EJB被调用的时候,代理调用local EJB中的create()方法并且调用EJB中对应的业务方法。
myController bean定义为这个代理设置controller类的myController属性。
这个EJB访问机制极大简化了应用程序的代码:
- 需要冗长的错误处理代码来确保ResultSets,Statements以及(最重要的)Connections在使用后关闭。这意味着对JDBC的正确使用可以快速地导致大量的代码量。它还是一个常见的错误来源。Connection leak可以在有负载的情况下快速宕掉应用程序。
SQLException相对来说不能说明任何问题。JDBC不提供异常的层次,而是用抛出SQLException来响应所有的错误。找出到底哪里出错了——例如,问题是死锁还是无效的SQL?——要去检查SQLState或错误代码。这意味着这些值在数据库之间是变化的。
Spring用两种方法解决这些问题:
- 提供API,把冗长乏味和容易出错的异常处理从程序代码移到框架之中。框架处理所有的异常处理;程序代码能够集中精力于编写恰当的SQL和提取结果上。
为你本要处理SQLException程序代码提供有意义的异常层次。当Spring第一次从数据源取得一个连接时,它检查元数据以确定数据库。它使用这些信息把SQLException映射为自己从org.springframework.dao.DataAccessException派生下来的类层次中正确的异常。因而你的代码可以与有意义的异常打交道,并且不需要为私有的SQLState或者错误码担心。Spring的数据访问异常不是JDBC特有的,因而你的DAO并不一定会因为它们可能抛出的异常而绑死在JDBC上。
Spring提供两层JDBC API。第一个时,在org.springframework.jdbc.core包中,使用回调机制移动控制权——并且因而把错误处理和连接获取和释放——从程序的代码移到了框架之中。这是一种不同的Inversion of Control,但是和用于配置管理的几乎有同等重要的意义。
Spring使用类似的回调机制关注其他包含特殊获取和清理资源步骤的API,例如JDO(获取和释放是由PersistenceManager完成的),事务管理(使用JTA)和JNDI。Spring中完成这些回调的类被称作template。
例如,Spring的JdbcTemplate对象能够用于执行SQL查询并且在如下的列表中保存结果:
你决不需要在使用JDBC时再编写finally block。
总的来说你需要编写的代码更少了
你再也不需要挖掘你的RDBMS的文档以找出它为错误的列名称返回的某个罕见的错误代码。你的程序不再依赖于RDBMS特有的错误处理代码。
无论使用的是什么持久化技术,你都会发现容易实现DAO模式,让业务代码无需依赖于任何特定的数据访问API。
在实践中,我们发现所有这些都确实有助于生产力的提高和更少的bug。我过去常常厌恶编写JDBC代码;现在我发现我能够集中精力于我要执行的SQL,而不是烦杂的JDBC资源管理。
如果需要的话Spring的JDBC抽象可以独立使用——不强迫你把它们用作Spring的一部分。
通过类似于JdbcTemplate的回调模板编程管理事务,比起直接使用JTA要容易多了
类似于EJB CMT的声明式事务管理,但是不需要EJB容器
Spring的事务抽象式唯一的,它不绑定到JTA或者任何其他事务管理技术。Spring使用事务策略的概念把程序代码和底层的事务架构(例如JDBC)解藕。
为什么你要关心这些?JTA不是所有事务管理的最好答案吗?如果你正在编写仅仅使用一个数据库的程序,你不需要JTA的复杂度。你不关心XA事务或者两阶段提交。你甚至不需要提供这些东西的高端应用服务器。但是另一方面,你不会希望在需要和多个数据源打交道的时候重写你的代码。
假定你决定通过直接使用JDBC或者Hibernate的事务以避免JTA带来的额外负担。一旦你需要处理多个数据源,你必须剥开所有的事务管理代码并且使用JTA事务来替代。这不是非常有吸引力的并且导致大部分J2EE程序员,包括我自己,推荐只使用全局JTA事务。然而使用Spring事务抽象,你只需要重新配置Spring让它使用JTA,而不是JDBC或者Hibernate的事务策略,就一切OK了。这是一个配置上的改变,而不是代码的改动。因而,Spring使得你能够自由缩放应用。
Interception:自定义行为能够在对接口和类的调用之前和之后插入。这类似于AspectJ术语中类似的“around advice”。
Introduction:指定advice会导致对象实现额外的接口。这混乱了继承。
静态和动态的pointcuts:在interception发生的程序执行处指定points。静态pointcuts concern函数签名;动态pointcuts也可以在point被求值的地方考虑函数的参数。Pointcuts独立interceptors单独定义,使得标准interceptor可以应用于不同应用程序和代码上下文。
Spring既支持有状态(一个advised对象一个实例)也支持无状态的interceptors(所有advice使用一个实例)。
Spring不支持field interception。这是一个经过深思熟虑的设计决定。我总是感觉field interception违反了封装。我比较倾向于把AOP作为补全物,而不是与OOP冲突的东西。如果在5年或者10年后,我们在AOP学习曲线上走得更远了并且觉得应该在程序设计的桌面上给AOP一个位置,我不会惊讶的。(然而在那个时候基于语言的解决方案例如AspectJ可能比它们今天看来更加具有吸引力。)
Spring使用动态代理实现AOP(其中存在一个接口)或者在运行时使用CGLIB生成字节码(这使得能够代理类)。两种方法都能够在任何应用服务器中使用。
Spring是第一个实现AOP Alliance interfaces的AOP 框架(www.sourceforge.net/projects/aopalliance)。这些是定义在不同AOP框架中能够互操作interceptors的尝试。
在TheServerSide和其他地方有一个正在进行但是不是那么引人注目的争论,就是这种interception是不是“true AOP”。我倒不怎么在意它叫什么;仅仅需要知道它是否在实践中有用就好了。我也乐于称它为“declarative middleware”(声明式中间件)。把Spring AOP认做简单,轻量级的无状态beans的替代物,这样就不需要monolithic EJB容器了,而这些仅仅是让你能够构建有你需要的服务的容器。我不推荐advising任何一个POJO,对local SLSBs的类比有助于你理解推荐的粒度。(然而,与EJB不同的是,在恰当但是少见的情况下,你可以自由地把Spring的AOP应用到粒度更好的对象上。)
因为Spring在实例上advises 对象,而不是在class loader层面上,使用有不同advice的同一个类的多个实例是可能的,或者与advised实例一道使用unadvised 实例。
可能Spring AOP最常见的应用是声明式事务管理。这是基于前面描述的TansactionTemplate抽象上的,并且可以给任何POJO提供声明式事务管理。取决于事务策略,底层的机制可以是JTA,JDBC,Hibernate或者任何其他提供事务管理的API。
Spring的声明式事务管理类似于EJB CMT,在以下方面有些不同:
事务管理能够应用于任何POJO。我们推荐业务对象实现接口,但是这只是一个好的编程习惯的问题,而不是由框架强制的。
通过使用Spring的事务API能够在事务性POJO中实现编程回调。我们为此提供静态的方法,使用ThreadLoacal变量,因而你不需要传播诸如EJBContext这样的context对象来确保回滚。
你可以声明式地定义“回滚规则”。EJB不会在未捕捉程序异常的时候自动回滚(仅仅在unchecked exceptions和其他Throwables的时候),应用程序开发者经常需要在任何异常发生时回滚。Spring事务管理让你能够声明式地指定什么异常什么子类能够导致自动回滚。缺省的行为和EJB是一致的,但是你能够在checked和unchecked异常时自动回滚。这个在最少化自编程回调代码方面有很大好处,而回调依赖于Spring的事务API(因为EJB的编程回调时在EJBContext中完成的)。
事务管理不绑定于JTA。如前面解释过的,Spring的事务管理能够在不同事务策略中使用。
当然还可以使用Spring AOP实现程序特有的aspects。取决于你对AOP概念的接受程度,决定你是否选择这么做,而不是Spring的能力,但是它确实非常有用。我们所见过的成功例子包括:
自定义的security interception,当安全检查的复杂度超出了J2EE安全架构的能力的时候
在开发中使用的调试和profiling aspects
发送email通知管理员用户不寻常的举动的Interceptors
程序自定的aspects能够成为消除需要许多函数的样板代码的有利武器。
Spring AOP透明地与Spring BeanFactory概念集成。包含一个来自Spring BeanFactory对象地代码不需要知道它是还是不是advised。和任何对象一样,契约实在接口和对象实现中定义的。
下面的XML片断展示了如何定义一个AOP代理:
Spring在controllers,JavaBean,models和views提供了一个非常清晰的划分。
Spring的MVC是非常灵活的。不像Struts,它强制你的Action和Form对象进入固化的层次之中(因而你迫使你使用Java的实体继承),Spring MVC完全是基于接口的。而且,通过插入你自己的接口几乎Spring MVC 框架的所有部分都是可配置的。当然我们也提供了方便的类作为实现选择。
Spring MVC是真正的view无关的。你不会被强制使用JSP,如果你不想那么做的话。你可以使用Velocity,XSLT或其他view技术。如果你想要使用自定义的view机制——例如,你自己的模板语言——你可以简单实现Spring的View接口并且把它集成进来。
和其他对象一样,Spring的Controllers是通过IoC配置的。着使得它们易于测试,并且完美地和其他由Spring管理的对象集成。
Web层变成了业务对象层之上的薄薄一层。这鼓励了好的习惯。Struts和其他专门的web框架让你去实现你自己的业务对象;Spring提供了你应用程序所有层的集成。
如在Struts 1.1中所见的,你可以有和你在Spring MVC 应用程序中所需要的一样多的dispatcher servlets。
下面的例子展示了一个简单的Spring Controller如何能够访问定义在应用程序context中的业务对象。这个controller在它的handleRequest()方法中执行了Google搜索:
使用EJB的典型代码依赖Service Locator或者Business Delegate singletons,使得测试难于进行。
在Service Locator模式没有使用Business Delegate的情况下,程序代码还要在EJB home中调用create()方法,并且处理可能导致的异常。因而仍然绑定在EJB API身上,忍受着EJB 编程模型的复杂度。
实现Business Delegate模式通常导致显著的代码重复,其中我们必须编写大量仅仅是调用EJB同等方法的方法。
基于这些和其他原因,传统的EJB访问,如在Sun Adventure Builder和OTN J2EE Virtual Shopping Mall中展示的那样,会降低生产率并且带来显著的复杂度。
Spring通过引入codeless business delegate前进了一步。有了Spring,你不再需要再编写另一个Service Locator,另一个JNDI查找,或者在硬编码的Business Delegate中重复代码,除非你肯定这增加了价值。
例如,假定我们有使用local EJB的web controller。我们将遵循最佳实践,使用EJB Business Methods Interface模式,EJB的local interface extend非EJB专有的业务方法接口。(这么做的主要的一个原因是确保在本地接口和bean实现类中方法签名的自动同步。)让我们调用这个业务方法接口MyComponent。当然我们还需要实现local home接口并且提供实现SessionBean和MyComponent业务方法的bean的实现类。
用了Spring EJB 访问,我们把我们的web层controller和EJB实现挂接上所需要进行的Java编码仅仅是在我们的controller中暴露一个类型MyComponent的setter方法。这将如下保存作为实例变量的引用:
Web层的代码不依赖于EJB的使用。如果你想要使用POJO,mock object或者其他test stub替代EJB引用,我们可以简单地改动一下myComponent bean定义而不影响一行Java代码
我们还不需要写一行JNDI查找或者其他EJB plumbing code。
在实际程序中的性能测试和经验标明这种方法(包括对目标EJB的反射调用)的性能影响是很小的,在典型的应用中检测不出。记住无论如何我们都不希望使用fine-grained的EJB调用,因为会有有关应用服务器上的EJB的底层架构方面的代价。
我们可以把相同方法应用于远程EJB,通过类似org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean factory bean的方法。然而我们无法隐藏远程EJB的业务方法接口中的RemoteException。
- 测试
如你可能已经注意到的,我和其他Spring开发这是全面单元测试重要性的坚定支持者。我们相信框架被彻底单元测试过的是非常重要的,而且我们框架设计的主要目标是让建立在框架之上的程序易于单元测试。
Spring自身有一个极好的单元测试包。我们的1.0 M1的单元测试覆盖率是75%,而且我们希望在1.0 RC1的时候能够达到80%的单元测试覆盖率。我们发现在这个项目中测试优先的开发带来的好处是实实在在的。例如,它使得作为国际化分布式开发团队的工作极端有效率,而且用户评论CVS snapshots趋向于稳定和使用安全。
因为以下理由,我们相信用Spring构建的应用程序是非常易于测试的:
IoC推动了单元测试
应用程序不包括直接使用注入JNDI的J2EE服务的plumbing code,这些代码一般让测试难于进行
Spring bean factories和contexts能够在容器外设置
在容器外可以设置Spring bean factory的能力提供了对开发过程有趣的可选项。在几个使用Spring的web应用中,工作是从定义业务接口和在web容器外集成测试开始的。在业务功能已经足够完整之后,web接口不过是添加在其上的薄薄一层。
- 谁在使用Spring
虽然相对来说Spring还是一个新的项目,但是我们已经有了一个令人印象深刻并且不断增长的用户群。它们已经有许多产品使用着Spring。用户包括一个主要的全球投资银行(做大型项目的),一些知名的网络公司,几个web开发顾问机构,卫生保健公司,以及学院机构。
许多用户完整地使用Spring,但是一些只单独使用一些组件。例如,大量用户使用我们地JDBC或者其他数据访问功能。
Roadmap
在今年晚些时候我们主要要做的是让Spring发布release 1.0。然而,我们还有一些更长远的目标。
为1.0 final规划地主要改进式源代码级地元数据支持,它主要用于(但不局限于)AOP框架。这将使得C#风格的attribute驱动的事务管理,并且让声明式企业服务在典型应用情况下非常容易配置。Attribute支持将会在Spring的1.0 final release支持中加入,并且设计的是在发布的那个时候能与JSR-175集成。
1.0之后,一些可能的改进地方包括:
通过对我们的JDBC和事务支持的一个相当抽象来支持JMS
支持bean factories的动态重配置
提供web services的能力
IDE和其他工具支持
作为一个敏捷项目,我们主要是受到用户需求的驱动。因而我们不会开发没有一个用户需要的特性,并且我们会仔细倾听来自用户群的声音。
- 总结
Spring是一个解决了许多在J2EE开发中常见的问题的强大框架。
Spring提供了管理业务对象的一致方法并且鼓励了注入对接口编程而不是对类编程的良好习惯。Spring的架构基础是基于使用JavaBean属性的Inversion of Control容器。然而,这仅仅是完整图景中的一部分:Spring在使用IoC容器作为构建完关注所有架构层的完整解决方案方面是独一无二的。
Spring提供了唯一的数据访问抽象,包括简单和有效率的JDBC框架,极大的改进了效率并且减少了可能的错误。Spring的数据访问架构还集成了Hibernate和其他O/R mapping解决方案。
Spring还提供了唯一的事务管理抽象,它能够在各种底层事务管理技术,例如JTA或者JDBC纸上提供一个一致的编程模型。
Spring提供了一个用标准Java语言编写的AOP框架,它给POJOs提供了声明式的事务管理和其他企业事务——如果你需要——还能实现你自己的aspects。这个框架足够强大,使得应用程序能够抛开EJB的复杂性,同时享受着和传统EJB相关的关键服务。
Spring还提供了可以和总体的IoC容器集成的强大而灵活的MVC web框架。
- 更多信息
参见以下资源获得关于Spring的更多信息:
Expert One-on-One J2EE Design and Development(Rod Johnson,Wrox,2002)。虽然Spring在书出版之后已经极大地进步和改进了,它仍然是理解Spring动机的极佳途径。
Spring的主页:http://www.springframework.org。这里包括Javadoc和几个教程。
在Sourceforge上的论坛和下载
Spring用户和Spring开发者的邮件列表
我们正在尽我们可能去改进Spring的文档和示例。我们还为在信件和邮件列表中极好的回复率自豪。我们希望你能快速融入我们的社区!
- 关于作者
Rod Johnson 作为Java开发者和架构师已经有了7年的经验了并且在J2EE平台出现之初就在其上进行开发了。他是《Expert One-on-One J2EE Design and Development》(Wrox,2002)的作者并且贡献了其他好几本关于J2EE的书。他当前正在为Wiley撰写另外一本有关J2EE架构的书。Rod在两个Java标准委员会服务并且经常师大会发言人。现在他在UK做一个咨询顾
大家谁能提供点权限管理的好的方法和例子,
在下多选了。
线性表,链表,哈希表是常用的数据结构,在进行Java开发时,JDK已经为我们提供了一系列相应的类来实现基本的数据结构。这些类均在java.util包中。本文试图通过简单的描述,向读者阐述各个类的作用以及如何正确使用这些类。
Collection
├List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack
└Set
Map
├Hashtable
├HashMap
└WeakHashMap
Collection接口
Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。一些Collection允许相同的元素而另一些不行。一些能排序而另一些不行。Java SDK不提供直接继承自Collection的类,Java SDK提供的类都是继承自Collection的“子接口”如List和Set。
所有实现Collection接口的类都必须提供两个标准的构造函数:无参数的构造函数用于创建一个空的Collection,有一个Collection参数的构造函数用于创建一个新的Collection,这个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个Collection。
如何遍历Collection中的每一个元素?不论Collection的实际类型如何,它都支持一个iterator()的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:
Iterator it = collection.iterator(); // 获得一个迭代子
while(it.hasNext()) {
Object obj = it.next(); // 得到下一个元素
}
由Collection接口派生的两个接口是List和Set。
List接口
List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。
和下面要提到的Set不同,List允许有相同的元素。
除了具有Collection接口必备的iterator()方法外,List还提供一个listIterator()方法,返回一个ListIterator接口,和标准的Iterator接口相比,ListIterator多了一些add()之类的方法,允许添加,删除,设定元素,还能向前或向后遍历。
实现List接口的常用类有LinkedList,ArrayList,Vector和Stack。
LinkedList类
LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。
注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:
List list = Collections.synchronizedList(new LinkedList(...));
ArrayList类
ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。
size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。
每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法并没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。
和LinkedList一样,ArrayList也是非同步的(unsynchronized)。
Vector类
Vector非常类似ArrayList,但是Vector是同步的。由Vector创建的Iterator,虽然和ArrayList创建的Iterator是同一接口,但是,因为Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。
Stack 类
Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。
Set接口
Set是一种不包含重复的元素的Collection,即任意的两个元素e1和e2都有e1.equals(e2)=false,Set最多有一个null元素。
很明显,Set的构造函数有一个约束条件,传入的Collection参数不能包含重复的元素。
请注意:必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。
Map接口
请注意,Map没有继承Collection接口,Map提供key到value的映射。一个Map中不能包含相同的key,每个key只能映射一个value。Map接口提供3种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。
Hashtable类
Hashtable继承Map接口,实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value。
添加数据使用put(key, value),取出数据使用get(key),这两个基本操作的时间开销为常数。
Hashtable通过initial capacity和load factor两个参数调整性能。通常缺省的load factor 0.75较好地实现了时间和空间的均衡。增大load factor可以节省空间但相应的查找时间将增大,这会影响像get和put这样的操作。
使用Hashtable的简单示例如下,将1,2,3放到Hashtable中,他们的key分别是”one”,”two”,”three”:
Hashtable numbers = new Hashtable();
numbers.put(“one”, new Integer(1));
numbers.put(“two”, new Integer(2));
numbers.put(“three”, new Integer(3));
要取出一个数,比如2,用相应的key:
Integer n = (Integer)numbers.get(“two”);
System.out.println(“two = ” + n);
由于作为key的对象将通过计算其散列函数来确定与之对应的value的位置,因此任何作为key的对象都必须实现hashCode和equals方法。hashCode和equals方法继承自根类Object,如果你用自定义的类当作key的话,要相当小心,按照散列函数的定义,如果两个对象相同,即obj1.equals(obj2)=true,则它们的hashCode必须相同,但如果两个对象不同,则它们的hashCode不一定不同,如果两个不同对象的hashCode相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,所以尽量定义好的hashCode()方法,能加快哈希表的操作。
如果相同的对象有不同的hashCode,对哈希表的操作会出现意想不到的结果(期待的get方法返回null),要避免这种问题,只需要牢记一条:要同时复写equals方法和hashCode方法,而不要只写其中一个。
Hashtable是同步的。
HashMap类
HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和null key。,但是将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低。
WeakHashMap类
WeakHashMap是一种改进的HashMap,它对key实行“弱引用”,如果一个key不再被外部所引用,那么该key可以被GC回收。
总结
如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。
如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。
要特别注意对哈希表的操作,作为key的对象要正确复写equals和hashCode方法。
尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程。
|