作者将在这篇文章中分享他在这个试验中的想法和发现。作者将清晰的说明他是怎样用Spring组件替换参考应用中的单态注册,JDBC代码,和web的前后端层。作者也会描述他遇到的一个问题还有他是怎样来解决它的。
叫我来参加会议晚了,就是这次的Spring框架会议。对于你们后来的人来讲,Spring已经是一个在Apache 2.0许可下发布的基础构造代码库。它的核心是一个反转控制容器,在容器的外部开发组已经创建了JDBC和JMS代码的样板文件模板,一个web MVC框架,和其他组件。
我之所以参加这个会议晚是因为我不管Spring的成熟度和公开度,我都对它花了很长的时间进行了测试。我的问题是:"Spring到底能给我带来什么?",为了找到答案,我用Spring的组件替换了现有的一个应用的内脏。通过这次替换我懂得了:我在很早以前就应该用Spring了;应用程序的代码已经没有以前那么混乱了,调试和扩展也更加容易,并且变的更轻因为我能够开发一些通用的辅助代码来配合Spring。
我将在这篇文章中分享我在这个试验中的想法和发现。我将清晰的说明我是怎样用Spring组件替换参考应用中的单态注册,JDBC代码,和web的前后端层。我也会描述我遇到的一个问题还有我是怎样来解决它的。
接下来的内容不需要你是一个Spring的专家,我会在后面给出Spring资源链接。样例代码在Linux上Spring 1.2.X和JDK1.5.0_07下已通过测试。
原始(旧的)代码
我不想通过制作一个鲜活的应用来进行试验,所以我从另外一篇文章中摘录下我的测试程序。这是一个有2个servlet页面控制器作为入口点的简单的java web应用。servlets通过DAO的方式访问数据库,DAO从本地数据源取得数据库链接。关联对象通过单态注册方式来相互取得。具体有:
SimpleDAO: 从数据库读出和写入信息对象
DbUtil: 方便程序使用JDBC ResultSets, Connections等等。
ObjectRegistry: 单态注册类,对象之间可以通过它彼此取得。
SetupDataSourceContextListener: 设置JDBC数据源
SetupDBContextListener: 预备(植入的)数据库
GetDataServlet: 页面控制器用来显示数据
PutDataServlet: 页面控制器用来存储数据
这是一个非常简单的应用程序,但它是完全独立的并且展现了一个多层应用程序的行为。这个小型试验的一些观测资料可用于由真实世界转换的工程应用中。
修改内脏:对象注册
首先来分析ObjectRegistry类,它是关系型对象粘合层。
package pool_test.util ;
public class ObjectRegistry {
private static ObjectRegistry _instance = new ObjectRegistry() ;
public static ObjectRegistry getInstance(){
return( _instance ) ;
}
private Map _singletons ;
public void put(final String key , final Object obj){
_singletons.put( key , obj ) ;
}
public Object get( final String key ){
return( _singletons.get( key ) ) ;
}
}
ObjectRegistry 实际上是一个String:Object键值对的一个Map.你可以将一个对象注册在一个地方(put()方法),然后在另外的地方得到这个对象(get()方法)。用注册可以削弱对象依赖因为代码可以仅仅通过它的通用类型(接口或超类)和查找关键字得到一个对象。而细节-实现,实例化和配置-就留给这段调用put()方法存储对象的代码。
它能工作,而且我发现它工作比较频繁,但它并不完美。缺少put()或者将它放错地方可能会引起空指针错误或栈溢出错误。你必须跟踪对象被存储注册的次序,为了确保你不会试图取的一个不存在的对象。像我在这做的一样,在小的应用中用ContextListener(一个监听器)处理实例化命令,但是在一个大的应用中可能就需要你努力去避免一些问题。
旧的单态注册的另一个问题是暴露的put()操作是java调用?意思是说:一个存储的对象实现的变更需要重新编译注册类-比如为了测试存根你想替换基于数据库的DAO。我以前犯过一个小错误,产品应用中用了DAO 的stub(这里不知道译成什么意思好?)。在一个比较大的应用中,它可能很难捕获到,因为它被隐藏在代码里了。
写点Spring代码就能处理这些缺点。这是新的注册类:
package pool_test.util ;
import org.springframework....ApplicationContext ;
import org.springframework....ClasspathXMLApplicationContext ;
public class ObjectRegistry {
private ApplicationContext _singletons ;
private ObjectRegistry(){
_singletons = new ClassPathXmlApplicationContext(
new String[] { "spring-objectregistry.xml" }
);
}
public Object get( final String key ){
return( _singletons.getBean( key ) ) ;
}
}
注意,我已经用一个Spring的ApplicationContext(应用上下文)替换了旧的Map。和Map类似,ApplicationContext保存对象并且让你通过名字来取得它们。比较起来,ApplicationContext是通过一个外部配置的Xml文件来读取所有的对象定义。修改spring-objectregistry.xml中的对象定义需要重启应用,但不是全编译。
摘自spring-objectregistry.xml:
<bean id="ObjectPool" class="org.apache...GenericObjectPool"
singleton="true"><-- omitted for brevity -->
</bean>
<bean id="DataSource" class="org.apache...PoolingDataSource"
singleton="true">
<property name="pool">
<ref local="ObjectPool" />
</property>
</bean>
<bean id="DAO" class="pool_test.data.jdbc.SimpleDAO" singleton="true">
<property name="dataSource">
<ref local="DataSource"/>
</property>
</bean>
XML元素符合反射调用:外部的<bean/>元素定义一个对象,里面的<property/>元素调用对象上的方法。例如,对于ID为Dao的bean来说,Spring首先实例化一个SimpleDAO类型的对象并且调用它的setDataSource()方法。setDataSource()参数是id为DataSource的bean,在文件的前面有定义。
在后台,Spring配置DataSource并且把它指派到DAO.其他的用Spring管理的对象要引用DAO只能通过它的bean名字"DAO",同样的,它们不知道这(就是"SimpleDAO")实现的改变。
既然Spring管理了对象,那ObjectRegistry对客户端代码来讲,只有只读。我可以移出ObjectRegistry类put()方法和其他的类中外部调用的put()方法。例如:SetupDataSourceContextListener现在仅用来组装初始化链接的池。
在web.xml部署描述下面也有一些固定负载。例如,一个用来指向本地jdbc和其他属性文件的上下文参数。Spring现在通过这些文件了来装配对象,并且自己分配这些值。
Spring也关心在spring-objectregistry.xml文件中对象间的依赖跟踪。我以前在代码中进行处理。因为在这个应用中我用了更多的依赖注入,现在Spring确认这些被引用的对象在客户端代码试图使用他们之前,按照属性顺序创建。也就是说我已经放下了笔记性的工作只剩下清理我的代码了。
有人可能在讨论关于一个完美的排除外在的需要的Ioc实现,可调用的ObjectRegistry类,并且让Spring在运行时管理对象间的关系。考虑到将来重构,以后它将会引起一点麻烦,但是现在我还是需要注册。
修改数据层:Spring JDBC
配置通用的数据源大概是一个装饰XML的工作。除了模板JDBC代码外,Spring也提供基于DAO的类。也就是把对Connection的管理以及ResultSet和PreparedStatement的关闭交给Spring框架。而只留下我应用的一些特殊代码。
新的DAO和旧接口一样
package pool_test.data.jdbc ;
public class SimpleDAO {
public void setupTable() ;
public void putData() ;
public Collection getData() ;
public void destroyTable() ;
}
实际上,它是个截然不同的东西。老版本的DAO里面有很多内嵌的JDBC代码,而新版本中把那个烦心的工作交给了Spring。
package pool_test.data.jdbc ;
public class SimpleDAO extends JdbcDaoSupport {
private GetDataWorker _getDataWorker ;
private PutDataWorker _putDataWorker ;
private CreateTableWorker _createTableWorker ;
private DestroyTableWorker _destroyTableWorker ;
// constructor is now empty
protected void initDao() throws Exception {
super.initDao() ;
_getDataWorker =
new GetDataWorker( getDataSource() ) ;
_putDataWorker =
new PutDataWorker( getDataSource() ) ;
_createTableWorker =
new CreateTableWorker( getDataSource() ) ;
_destroyTableWorker =
new DestroyTableWorker( getDataSource() ) ;
return ;
} // initDao()
public void setupTable() {
_createTableWorker.update() ;
}
public Collection getData() {
return( _getDataWorker.execute() ) ;
}
// ... destroyTable() and getData()
// follow similar conventions ...
}
首先的变更是父类:SimpleDAO。它现在继承了JdbcDaoSupport,JdbcDaoSupport有一些方法和内部类用来进行数据库的工作。第一个方法是setDataSource(),它用来给这个对象指派一个DataSource。子类通过调用getDataSource()得到数据源。
initDao()是另一个从JdbcDaoSupport继承来的方法。父类通过调用该方法以让子类进行以前的初始化方法。在这里,SimpleDAO给它的成员变量赋值。
成员变量也是新的:移除Spring JDBC也就是移出SimpleDAO的数据存取功能,而是指定像GetDateWorker和PutDataWorker.这样的内部类。每个DAO动作都有一个这样的内部类。例如,用来存储数据的PutDataWorker类
package pool_test.data.jdbc ;
import org.springframework ... SqlUpdate ;
public class SimpleDAO {
...
private class PutDataWorker extends SqlUpdate {
public PutDataWorker( final DataSource ds ){
super( ds , SQL_PUT_DATA ) ;
declareParameter(
new SqlParameter( Types.VARCHAR ) ) ;
declareParameter(
new SqlParameter( Types.INTEGER ) ) ;
}
// a real app would load the SQL statements
// from an external source...
private static final String SQL_PUT_DATA =
"INSERT INTO info VALUES( ? , ? )" ;
}
...
}
PutDataWorker继承了SqlUpdate类,SqlUpdate是一个用来处理繁重的SQL插入和修改调用的Spring模板类。declareParameter()方法是用来告诉Spring,这个SQL statement中的数据类型分别是字符串和数字类型。
注意,PutDataWorker是一个非常瘦的类。它调用super()方法传递一个DataSource和
SQL statement给它的父类,用declareParameter()来描述这个查询。SqlUpdate通过几个JDBC关联对象和闭合连接(closing connections,就是打开一个链接,处理完毕,关闭链接)来处理真正的工作。SimpleDAO.putDate()方法也是相当简洁的:
public class SimpleDAO {
public void putData() {
for( ... ){
// ... "nameParam" and "numberParam" are
// local loop variables ...
Object[] params = {
nameParam , // variable is a Java String
numberParam // some Java numeric type
} ;
_putDataWorker.update( params ) ;
}
}
}
putData()方法用来把数据存储到数据库。注意这个方法是把工作委派给了它的worker类(也就是_putDataWorker),实际上是它的worker类继承SqlUpdate而来的方法。SqlUpdate.update()方法负责取数据,关闭JDBC连接和其他相关的Statement对象。也就是我可以丢弃一些我写的通用的JDBC代码类甚至是整个DbUtil类,DbUtil类提供了一些方法,可以用来很方便的关闭Connection,Statement和ResultSet.
SqlUpdate是用来修改数据,Spring的MappingSqlQuery则用来查询。注意看GetDateWorker中的mapRow()方法:
package pool_test.data.jdbc ;
import org.springframework ... MappingSqlQuery ;
// inside class SimpleDAO ...
private class GetDataWorker
extends MappingSqlQuery {
// ...constructor similar to PutDataWorker...
protected Object mapRow( final ResultSet rs ,
final int rowNum ) throws SQLException
{
final SimpleDTO result = new SimpleDTO(
rs.getString( "names" ) ,
rs.getInt( "numbers" )
) ;
return( result ) ;
} // mapRow()
}
这个方法负责把ResultSet的表列数据转换为可用的SimpleDTO对象,一条数据对应一个对象。对结果集中的每一行Spring都调用此方法。当GetDataWorker.mapRow()和ResultSet两者相结合时,它不再负责关闭记录集或者去检查是否在处理过程中
拉下了某行。
用SpringMVC修改Web层
既然我有了Spring的基础结构和数据层代码,接下来就该处理web层了。SpringMVC的控制器接口是web层中很重要的类:
package org.springframework ...
interface Controller {
public ModelAndView handleRequest(
HttpServletRequest request ,
HttpServletResponse response
) throws Exception ;
}
handleRequest()方法是入口点,和Struts Action的情况类似,是自己的页面控制器:框架提供了servlet请求和响应对象,控制器的实现类则返回业务逻辑的结果(一个模型对象)和一个指示器,指示器用来说明怎样显示结果(也就是视图)。ModelAndView的模型是一个将要显示到视图层的一个业务对象或者其他数据的一个Map。在例子代码中,视图是要个JSP,但是Spring 的MVC也支持 Velocity templates 和 XSLT。
例子应用的servlet页面控制器太简单以致于没有看到Spring MVC的强大之处。因此,最新的页面控制器都藏在大多数的教科书的例子中。
旧的页面控制器也有类似的策略,将一个Map对象从servlets传递到jsps.结果是,我不需要对传递到jsp的工作做任何的改变。我只需要用一些spring特殊的标签库。
对于新的基于Spring的控制器,还有一些工作要做,尽管他们可以通过调用ObjectRegistry类查找DAO.但是在一个干净的spring中,DAO应该在一个特殊的web xml配置文件中指定,这里是通过WEB-INF/SpringMVC-servlet.xml来指定的。
听上去相当简单,对不对?我现在只需要将对象注册的Spring配置文件(spring-objectregistry.xml)添加到web.xml中,像这样:
<context-param>
<param-name>
contextConfigLocation
</param-name>
<param-value>
classpath*:spring-objectregistry.xml
</param-value>
<context-param>
现在在spring-objectregistry.xml文件中定义的对象,对于WEB-INF/SpringMVC-servlet.xml中定义的那些对象是可见的。
这样做就产生了另外一个问题:MVC层将spring-objectregistry.xml和SpringMVC-servlet.xml文件装载到一个ApplicationContext中。对象注册将spring-objectregistry.xml装载到一个不同的ApplicationContext中。这两个ApplicationContext分别在各自的环境中,默认情况下,它们彼此看不到bean的定义。
定义在spring-objectregistry.xml文件中的对象被依次注册2次:一次是通过MVC层,然后是对象注册。这样,spring-objectregistry.xml文件中单态对象实际上已经不在是单个的了。在例子代码中这个都是无状态对象,但在一个稍大的应用中一些单个对象可能确实要保持状态。如果我不仅仅只是装载一次这些对象的话,可能会有一个同步的问题。就像如果一个对象要执行一些以前的资源优化(resource-intensive)的操作,那我的应用的性能将有所损失(译者:因为2个对象不一致)。
我的第一个反应就是重构那些ObjectRegistry中不需要的代码。那是一个简单的事情,但这是一个学习的过程。假设这是一个大的工程,其中移除注册类不是个一次就能做好的任务,我决定留着它并且解决怎样让2个环境工作的问题。
简而言之,我需要一些途径把ObjectRegistry (spring-objectregistry.xml)中的对象暴露给那些在web层(WEB-INF/SpringMVC-servlet.xml)中的对象。Spring的解决方案是用BeanFactoryLocator,它是一个applicationContext的注册点。我可以告诉对象注册点和MVC层从BeanFactoryLocator中装载这些共用对象。
首先,我必须修改ObjectRegistry类,像这样,它不在明确装载spring-objectregistry.xml文件:
import org.springframework....BeanFactoryLocator ;
import org.springframework.
...ContextSingletonBeanFactoryLocator ;
import org.springframework.
...BeanFactoryReference ;
// this was an ApplicationContext before
private final BeanFactoryReference _singletons ;
private ObjectRegistry(){
// ContextSingletonBeanFactoryLocator loads
// contents of beanRefContext.xml
BeanFactoryLocator bfl =
ContextSingletonBeanFactoryLocator
.getInstance() ;
BeanFactoryReference bf =
bfl.useBeanFactory( "OBJ_REGISTRY_DEFS" );
_singletons = bf ;
}
public Object get( final String key ){
return( _singletons.getFactory().getBean( key ) ) ;
}
这个代码将ApplicationContext用一个BeanFactoryReference替换,从一个BeanFactoryLocator中取得名字为OBJ_REGISTRY_DEFS的对象。
接下来,将OBJ_REGISTRY_DEFS对象定义在一个叫beanRefContext.xml的文件中.
<beans>
<bean
id="OBJ_REGISTRY_DEFS"
class="...ClassPathXmlApplicationContext"
>
<constructor-arg>
<list>
<value>spring-objectregistry.xml</value>
</list>
</constructor-arg>
</bean>
</beans>
名字为OBJ_REGISTRY_DEFS的bean实际上是一个基于原来的对象注册配置文件spring-objectregistry.xml的ApplicationContext。在BeanFactoryReference上调用getBean()方法仅仅是传递了一个潜在的ApplicationContext。
那只是照顾了ObjectRegistry自己。为了让web层也能用OBJ_REGISTRY_DEFS,使得对象在web层的配置文件(SpringMVC-config.xml)可见,需要在web.xml中添加一些扩展点。
<context-param>
<param-name>
parentContextKey
</param-name>
<param-value>
OBJ_REGISTRY_DEFS
</param-value>
</context-param>
<context-param>
<param-name>
locatorFactorySelector
</param-name>
<param-value>
classpath*:beanRefContext.xml
</param-value>
</context-param>
第一个入口告诉web层的Spring配置,对于找不到的对象,它应该访问名字为OBJ_REGISTRY_DEFS的BeanFactoryReference.第二个告诉框架,装载classpath下的名字为beanRefContext.xml文件。
现在定义在spring-objectregistry.xml中对象对于web层(在SpringMVC-config.xml)的对象是可见的。意思是说我可以慢慢的淘汰ObjectRegistry,而不是一步就要做出很大的改动。
难看吗?是。耦合代码?是。当你已经有可自己的单态注册?(Absolutely)时,这是一种将你的应用移植到Spring的方法。现在这个应用从将来的重构中被屏蔽了,这个ObjectRegistry(和明确从ApplicationContext中加载的)的移出将仅仅影响ObjectRegistry的客户段代码。
然而,有一点警告:Spring的文档中也注明BeanFactoryLocator不是常用的,就像上面那样,它应该被用来移植对象。如果你打算在一个新的应用中用Spring,通过比较,你的设计从一开始应该说明合适的Ioc注入。
(总结) The Round-up
比较原来的app和它的新版本,我首先发现大小上的不同;我写的代码变少了,调试简单了。而且,让Spring来处理如此多的对象实例化和依赖跟踪意味着随着系统的增长我少了一些烦恼。通过Ioc的注册我的测试也变得简单了-我可以通过修改一个xml文件将一个DAO换成另一个进行测试。
取的更高层次的见解,我考查了我用过的Spring的特殊的组件。对象查找和JDBC templates很常见。在一个相当大小的工程中,框架处理象查找或数据库链接是共用的。那就是使得Spring如此有用的地方-感觉像是本身就有的。我们以前见过它,只是现在当我们从一个工程移动到另一个对象时,不必要(重写)写它。因为是一个附加的特性,Spring的全体人员能够专注于增强他们的产品,并且我们关心我们自己的。
采用Spring的另外一个好处是,不是要嘛全有要嘛全无的去努力。能够做到不彼此影响将Spring应用到每个应用层。因此我可以从前面走过的任何点上听下来。例如,我只想要Spring的JDBC模板部分,那我只修改我的DAO,而不触及到我应用的其他部分。
虽然Spring的会议我迟到了,但我还是非常高兴能够参加。