迁移到Spring

翻译|其它|编辑:郝浩|2007-08-31 10:19:07.000|阅读 791 次

概述:

# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>

我是这个团体的后来者。该团体就是  Spring framework  团体。对于后来者来说,Spring  是基于  Apache 2.0许可证发布的基础架构代码库。其核心是反转控制容器,开发团队围绕该容器为  JDBC  样板代码和  JMS  代码、web MVC  框架等创建模板。

  我之所以说自己是后来者,是因为,尽管  Spring  已非常成熟和公开,但我仍然花了一段时间来试用它。我的问题是,“Spring  可以为我做什么?为了找出答案,我将现有的参考应用程序换成了Spring组件。我认识到很早以前就应该开始使用  Spring,现在应用程序的代码没有以前那么混乱,应该更易于调试和扩展,并且它更为清楚,因为借助  Spring  对应的内容,可以丢弃一些自定义的  helper  代码。

  在本文中,我将和大家共享我在尝试中得到的想法和发现。具体地讲,我将解释如何使用  Spring组件替换参考应用程序的单例注册库(Singleton registry)、JDBC  代码和  web  前端层。还将描述遇到的障碍和我的解决方法。

  要阅读本文,您不必是  Spring  专家,但是我在后面的内容中提供了  Spring  资源的链接。

  样例代码使用  Sun    JDK 1.5.0_07 for Linux,在  Spring 1.2.x中进行了测试。

原(旧)代码

  我不想对实际的生产应用程序进行试验,因此从我编写的另一篇文章中摘出了一个测试应用程序。它是一个简单的  Java web  应用程序,以两个  servlet  页面控制器作为入口点。这些  servlet  通过数据访问对象(DAO)与数据库交互,而该  DAO  从本地  DataSource  获取数据库链接。相关对象调用单例注册库来相互查找。具体而言:

·                                 SimpleDAO:向数据库传输信息对象或从中传输信息对象

·                                 DbUtil:用于处理  JDBC ResultSetConnection  等内容的方便例程

·                                 ObjectRegistry:单例注册库,对象通过它进行相互查找

·                                 SetupDataSourceContextListener:设置  JDBC DataSource

·                                 SetupDBContextListener:准备(嵌入式)数据库

·                                 GetDataServlet:用于显示数据的页面控制器

·                                 PutDataServlet:用于存储数据的页面控制器

  这是一个非常简单的  web  应用程序,但它是独立的(self-contained)并且能展现出大型  N-层应用程序的行为。从这种超小规模的试验观察到的结果可以应用于实际的转换项目。

更改内部结构:对象注册库

  仔细观察的第一个类是  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  对的大规模映射。可以在该注册库的某个位置存储一个对象(put()),然后从另一个位置获取该对象(get())。使用注册库削弱了对象的依赖关系,因为获取对象的代码只需了解其常规类型(接口或超类)和查找键。具体的实现、实例化和配置由调用put()来存储该对象的代码实现。

  这可以正常工作,我曾经看到过这在更大规模的项目中正常工作,但它绝非完美。put()丢失或位置错误可以导致空指针错误或堆栈溢出。还必须跟踪对象在注册库中的存储顺序,以确保不会尝试获取不存在的对象。在小型应用程序中,可以使用  ContextListener(本文中我也是如此)来处理实例化顺序,但是在较大的应用程序中,需要更多工作才能避免出现问题。

  旧的单例注册库的另一个问题在于:显式的  put()操作是  Java  调用。这意味着对存储对象实现的任何更改(比如说,您希望为测试存根转入您的数据库支持的  DAO)都需要重新编译。一次错误的签入,我的实际生产应用程序使用了该  DAO  存根。同样,这在较大的应用程序中也难以跟踪,因为它在代码中隐藏得很深。

  只需一小段  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  文件中读取所有对象定义,而该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  元素对应于  Reflection  调用:外部的<bean/>元素定义了一个对象,内部的<property/>元素对该对象调用  mutator  方法。例如,对于  id    DAO    beanSpring首先实例化一个  SimpleDAO  类型的对象,然后对其调用  setDataSource()setDataSource()的参数是  bean DataSource,在这个文件前面的内容中定义了该  bean

  Spring  在幕后配置  DataSource,并将其指派给此  DAO。其他  Spring  管理的对象只需通  bean   名称(“DAO”)即可引用该  DAO,这样,它们不必了解其实现中的更改(此处为“SimpleDAO”)。

  既然  Spring  管理这些对象,ObjectRegistry  对于客户端代码就是只读的。我可以从ObjectRegistry  类中删除  put()方法,同样可以删除其他类中的显式  put()调用。例如,SetupDataSourceContextListener  现在只需使用其初始连接来填充该池。

  现在  web.xml  部署描述符中也只有一些必需内容了。例如,一些上下文参数指向  JDBC  之类的本地属性文件。Spring  现在收集使用这些属性文件的对象,自己为其赋值。

  Spring  还负责在  spring-objectregistry.xml  中跟踪这些对象之间的依赖关系。以前我在代码中自己处理这些事务。现在,随着在该应用程序中使用的依赖注入越来越多,Spring  将确保在客户端代码尝试使用这些引用对象之前,以正确的顺序创建这些对象。这意味着除了整理代码以外,Spring  还承担了一些簿记工作。

  有人可能会说,”IoC  方法可以消除对显式的、可调用  ObjectRegistry  的需求,而让Spring  在运行时管理对象关系。这将作为将来的重构考虑。这会在将来造成一点小问题,但是现在我仍然需要注册库。

更改数据层:Spring JDBC

  配置自定义  DataSource    XML  可以完美实现的工作。Spring  还提供了  DAO  基类,用于消除  JDBC  样板代码。这意味着  Spring framework  负责管理连接和关闭  ResultSet    PreparedStatement。剩下的是我的应用程序特有的代码。

  新的  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  现在扩展  Spring    JdbcDaoSupportJdbcDaoSupport  有几个用于处理数据库工作的方法和内部类。其中第一个方法是  setDataSource(),它为该对象指派一个  JDBC DataSource。子类调用  getDataSource()来获取该对象。

  initDao()是从  JdbcDaoSupport  继承的另一个方法。父类调用该方法为其子类提供运行任何一次性初始化代码的机会。此处,SimpleDAO  为其成员变量赋值。

  成员变量也是新的:转移到  Spring JDBC  意味着将的  SimpleDAO  的功能(获取和存储数据)转变成专门的内部类(如  GetDataWorker    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    Spring  模板类,用于处理 SQL INSERT    UPDATE  调用的复杂工作。declareParameter()调用告知  Spring    SQL  语句使用的数据类型,分别是一个字符串和一个数字。

  注意,PutDataWorker  类是一个非常简单的类。它调用  super()向其父类传递  DataSource  SQL  语句,并调用  declareParameter()来描述该查询。SqlUpdate  处理实际与  JDBC  相关对象的交互工作,并关闭连接。SimpleDAO.putData()也同样被简化了:

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()使用一些无意义的数据填充该数据库。注意,该方法委托给其工作类。具体而言,委托给其工作类继承的方法。SqlUpdate.update()负责获取数据,并关闭  JDBC Connection  和相应的  Statement  对象。这意味着我可以丢弃很多自定义  JDBC  代码,甚至整个类:旧的  DbUtil  拥有方便的方法,用于关闭  ConnectionStatement    ResultSet

  SqlUpdate  对更新调用所作的工作,Spring    MappingSqlQuery  对查询也作同样处理。注意,GetDataWorker  中的方法  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  交互,但是它不另外负责  close(),或检查是否还有一些行需要处理

更改  Web  层:SpringMVC

  既然已经使基础架构和数据代码  Spring  化,下面该处理  web  层了。此处的中心类是Spring MVC    Controller  接口。

package org.springframework ...
interface Controller {
  public ModelAndView handleRequest(
    HttpServletRequest request ,
    HttpServletResponse response
  ) throws Exception ;      
}

  入口点  handleRequest()的签名与  Struts Action  类甚至与自制的页面控制器相似:该框架提供了  servlet  请求和响应对象,该控制器实现返回了一些业务逻辑的结果(模型),以及用于指导如何显示这些内容的指示器(视图)。ModelAndView  模型是将在视图层操作的业务对象或其他对象的  Map。在样例代码中,视图是  JSP,但  Spring MVC  还支持  Velocity  模板和  XSLT

  样例应用程序的  servlet  页面控制器非常简单,以至于无法表现  Spring MVC  的强大功能。因此,更新的页面控制器无法与大多数课本示例相比。

  旧的页面控制器在从  servlet    JSP  传递  Map  对象时使用类似的策略。因此,我没有为此对  JSP  进行任何更改。也不必使用任何  Spring  特有的标签库。

  在新的基于  Spring  的控制器中仍然需要做一些工作,尽管:它们调用  ObjectRegistry  来查找  DAO。在纯  Spring  世界中,应该在  web  特有的  XML  配置文件(此处为  WEB-INF/SpringMVC-servlet.xml)中将  DAO  指派给控制器。

  这听起来很简单,是不是?我向  web.xml  添加了该对象注册库的  Spring  配置文件spring-objectregistry.xml,如下所示:

<context-param>
  <param-name>
    contextConfigLocation
  </param-name>
  <param-value>
    classpath*:spring-objectregistry.xml
  </param-value>
<context-param>

  现在,在  WEB-INF/SpringMVC-servlet.xml  中就能看到  spring-objectregistry.xml  中指定的对象。

  这么作会造成另一个问题:MVC  层将文件  spring-objectregistry.xml  SpringMVC-servlet.xml  加载到一个  ApplicationContext  中。对象注册库将  spring-objectregistry.xml  加载到不同的  ApplicationContext  中。这两个  ApplicationContext  存在于自己的世界中,默认情况下,无法看到对方的  bean  定义。

  同样,spring-objectregistry.xml  中定义的对象也将被加载两次:一次由  MVC  层加载,另一次由对象注册库加载。然后  spring-objectregistry.xml  中的单例对象实际上不再是单例的。在样例代码中,这些都是无状态的对象。但是在较大的应用程序中,一些单例对象可能会有状态。如果不一次性加载这些对象,并且仅加载一次,就会遇到同步问题。如果这类对象要执行一些一次性的资源密集型操作,应用程序的性能就会降低。

  我的第一反应是重构  ObjectRegistry  之类的不必要代码。这项工作很简单,但这是一个学习练习。要模拟较大的项目(其中删除注册库不再是一次就能完成的工作),我决定坚持这样作,并找出如何才能使这两个世界工作。

  简而言之,我需要使用某种方式将  ObjectRegistryspring-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,名称为OBJ_REGISTRY_DEFS,是从  BeanFactoryLocator  中得到的。同样,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

  ApplicationContext  自己负责  ObjectRegistry。要使  web  层也使用OBJ_REGISTRY_DEFS(也就是,要使  web    Spring  配置  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  中定义的对象可以被  SpringMVC-config.xml  中的web  层对象看到。这意味着可以逐步淘汰  ObjectRegistry,而不是尝试一步实现这样影响深远的更改。

  不好看?是的。粘合代码?是的。这是当您已经拥有自己的单例注册库时,将应用程序迁移到Spring  的一种方式?毫无疑问。现在,此应用程序将来不再需要重构:删除  ObjectRegistry(及其显式加载的  ApplicationContext)只会影响  ObjectRegistry  的客户端代码。

  但是有一点警告需要注意:Spring  文档说明  BeanFactoryLocator  并非用于日常使用。它应该用于这类迁移项目。作为对照,如果您计划对新应用程序使用  Spring,您的设计应该从头考虑正确的  IoC  注入。

结束语

  将原应用程序与更改后的应用程序进行比较,我首先注意到的是规模上的差异,代码减少了,这更易于调试。此外,让  Spring  处理这么多的对象实例化和依赖关系跟踪,意味着在系统增长时不会遇到太多烦恼。通过  IoC  注入还能简化测试,通过更改  XML  文件可以转出  DAO  实现。

  从更高的角度来看,我仔细考虑了自己使用的特殊  Spring  组件。对象查找和  JDBC  模板看起来很熟悉。在大型项目中,通常拥有处理对象查找或数据库连接的框架代码。这自然会提高  Spring的实用价值。以前我看到过这种情况,现在我们从一个项目移动到另一项目时不必再编写这些内容。作为额外的优势,Spring  团队可以重点增强自己的产品,而我们则关注自己的产品。

  采用  Spring  的另一个优势是它不是要么完全成功要么完全失败的行为。可以在不影响其他层的情况下,对各个应用程序层应用  Spring,因此在此过程中可以随时停止。例如,如果我只希望利用  Spring    JDBC  模板,可以只更改  DAO,而不修改应用程序的其余部分。

  尽管我是  Spring  团体的后来者,但是我非常高兴能够成为其中的一员。


标签:

本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com

文章转载自:D2D

为你推荐

  • 推荐视频
  • 推荐活动
  • 推荐产品
  • 推荐文章
  • 慧都慧问
扫码咨询


添加微信 立即咨询

电话咨询

客服热线
023-68661681

TOP