Chinaunix首页 | 论坛 | 博客
  • 博客访问: 821197
  • 博文数量: 754
  • 博客积分: 7000
  • 博客等级: 少将
  • 技术积分: 5005
  • 用 户 组: 普通用户
  • 注册时间: 2008-09-12 12:54
文章分类

全部博文(754)

文章存档

2011年(1)

2008年(753)

我的朋友
最近访客

分类:

2008-09-12 12:58:38

  本文主要探讨如何利用Spring来装配组件,包括其事务上下文。从J2EE应用程序内部连接到单个的数据库并不是什么难事。但是,如果要装配或者集成企业级的组件,情况就复杂了。一个组件可以有一个或多个支持它的数据库,因此,当装配两个或更多的组件时,我们希望能够保持在跨组件的多个数据库中进行的操作的原子性。J2EE为这些组件提供了一个容器来保证事务原子性和跨组件独立性。如果使用的不是J2EE,则可以利用Spring来帮助我们。Spring基于Inversion of Control(控制反转)模式(也称为依赖注入),它不仅可以连接组件服务,还可以连接关联的事务上下文。在本文中,我们将Hibernate用作对象/关系持久性和查询服务。  

  装配组件事务

  假设在企业组件库里,我们已经有一个审计组件,里面有可以被客户端调用的服务方法。然后,当我们想要构建一个订单处理系统时,我们发现存在这样的设计要求:OrderListManager组件服务同样需要审计组件服务。OrderListManager创建和管理订单,因此所有的OrderListManager服务都有自己的事务属性。当我们从OrderListManager服务内调用审计组件时,我们实际上是在把OrderListManager服务的事务上下文传播给审计服务。也许将来新的业务服务组件同样需要审计组件,但那时将在一个不同的事务上下文中调用它。实际结果就是,即使审计组件的功能保持不变,它也可能是由别的业务服务功能组成,包含了混搭的(mix-and-match)事务属性来提供不同的运行时事务性行为。

  在图1中有两个独立的调用上下文流程。在流程1里,如果客户端有TX上下文,那么OrderListManager既可以参与其中,也可以启动一个新的TX,这取决于客户端是否在TX中,以及为OrderListManager方法指定了什么样的TX属性。这同样适用于OrderListManager服务依次调用AuditManager方法的情况。

Assembling Component Transactions

  图1 装配组件事务

  EJB架构允许组件装配者声明式地给出正确的事务属性,从而为他们提供这种灵活性。我们不探讨声明式事务管理的替代方案(即所谓的编程式事务控制),因为这会牵涉到代码更改,从而产生不同的运行时事务行为。几乎所有的J2EE应用服务器都按照X/Open XA规范提供了服从两阶段提交的分布式事务管理器。现在的问题是,我们能不能利用EJB服务器来实现相同的功能?Spring就是其中的一种解决方案。让我们来看一下Spring如何帮助我们解决事务组装的问题:

  使用Spring进行事务管理

  我们将看到一个轻量级的事务基础架构,它实际上可以管理组件级的事务装配。Spring是其中的一个解决方案。它的优点在于,我们不会被捆绑到J2EE容器服务(如JNDI DataSource)上。最棒的一点是,如果我们想把这个轻量级事务基础架构关联到一个已可用的J2EE容器基础架构,将不会有任何问题。看起来我们可以利用两者的优点。

  另一方面,Spring这个轻量级事务基础架构使用了一个面向方面编程(Aspect-Oriented Programming,AOP)框架。Spring AOP框架使用了一个支持AOP的Spring bean工厂。在特定于Spring的配置文件applicationContext.xml中,通过在组件服务级指定事务特性来划分事务。

        class="org.springframework.transaction
        .interceptor.TransactionProxyFactoryBean">

       


       


       
               
                        PROPAGATION_REQUIRED
               

               
                        PROPAGATION_REQUIRED
               

               
                        PROPAGATION_REQUIRED
               

               
                   PROPAGATION_REQUIRED,
                   -com.example.exception.FacadeException
               

               
                        PROPAGATION_REQUIRED,readOnly
               

               
                        PROPAGATION_REQUIRED,readOnly
               

       


  一旦我们在服务级指定了事务属性,org.springframework.transaction.PlatformTransactionManager接口的一个特定实现就会截获并解释它们。该接口如下:

public interface PlatformTransactionManager{
        TransactionStatus getTransaction
                (TransactionDefinition definition);
        void commit(TransactionStatus status);
        void rollback(TransactionStatus status);
}

  Hibernate事务管理器

  由于我们已决定使用Hibernate作为ORM工具,下一步要做的就是配置一个特定于Hibernate的事务管理器实现。

        class="org.springframework.orm.hibernate.
                HibernateTransactionManager">
       
               
       

  设计多个组件中的事务的管理

  现在,我们来讨论什么是“装配组件事务”。您也许注意到了为域中的服务级组件OrderListManager所指定的各种TX属性。图2所示的业务域对象模型(Business Domain Object Model,BDOM)显示了我们的域所确定的主要对象:

Business Domain Object Model (BDOM)
  图2 业务域对象模型(BDOM)

  图字:Order:订单;Audit:审计

  为了更好的说明,我们来列出我们的域中的一些非功能性需求(Non-Functional Requirement,NFR):

  业务对象需要保存在一个数据库中(appfuse1)。
  审计时要登录到另一个数据库中(appfuse2),出于的考虑,数据库要有保护。
  业务组件应该可以重用。
  必须尽一切努力审计业务服务层的所有活动。

  考虑了以上要求之后,我们决定,OrderListManager服务会将所有的审计日志调用委托给已经可用的AuditManager组件。这样就得出了详细设计,如图3所示:

Design of Component Services
图3 组件服务的设计

  这里值得注意的一点是,由于我们的NFR,我们要将与OrderListManager相关的对象映射到appfuse1数据库,而将与审计相关的对象映射到appfuse2。这样,无论要审计什么,OrderListManager组件都会调用AuditManager组件。我们会看到,OrderListManager组件中的所有方法都应该是事务性的,因为我们通过服务来创建订单和线项目(line item)。那么AuditManager组件中的服务呢?因为它做的是审计跟踪,我们关心的是尽可能维持长时间的审计跟踪,并针对系统中所有可能的业务活动。这就产生了如下的需求:“即使主要的业务活动失败了,也要进行审计跟踪记录”。AuditManager组件同样要有自己的事务,因为它也与自己的数据库进行交互。如下所示:


        class="org.springframework.transaction.
         interceptor.TransactionProxyFactoryBean">
       
               
       

       
               
       

       
               
                       
                                PROPAGATION_REQUIRES_NEW
                       

               

       

  现在,为了演示,我们把注意力放到createOrderList和addLineItem这两个业务服务上。同时请注意,我们并没有要求最佳设计策略——你可能注意到了,addLineItem方法抛出了FacadeException异常,而createOrderList却没有。在生产设计中,您也许希望每一个服务方法都可以处理异常场景。

public class OrderListManagerImpl
        implements OrderListManager{

private AuditManager auditManager;

public Long createOrderList
(OrderList orderList){
        Long orderId = orderListDAO.createOrderList(orderList);
       auditManager.log(new AuditObject(ORDER + orderId, CREATE));

        return orderId;
}

public void addLineItem
(Long orderId, LineItem lineItem)
        throws FacadeException{

        Long lineItemId = orderListDAO.addLineItem(orderId, lineItem);
       auditManager.log(new AuditObject(LINE_ITEM + lineItemId, CREATE));

        int numberOfLineItems = orderListDAO.
                queryNumberOfLineItems(orderId);
        if(numberOfLineItems > 2){
                log("Added LineItem " + lineItemId +" to Order " + orderId + ";
                        But rolling back *** !");
                throw new FacadeException("Make a new Order for this line item");
        }
        else{
                log("Added LineItem " + lineItemId +" to Order " + orderId + ".");
        }
}

//Other code goes here...

}

  为了创建一个异常场景来进行演示,我们引入了另一种业务规则,它规定一个特定的订单不能包含多于两个的线项目。现在应该注意,我们是从createOrderList和addLineItem中调用auditManager.log()方法的。您应该也注意到了为上述方法所指定的事务属性。

        class="org.springframework.transaction.
         interceptor.TransactionProxyFactoryBean">
       
               

                                PROPAGATION_REQUIRED
                       

                       
                                PROPAGATION_REQUIRED,-com.
                                example.exception.FacadeException
                       


               
       




       
               

                                PROPAGATION_REQUIRES_NEW
                       


               

       

  PROPAGATION_REQUIRED等效于TX_REQUIRED,而PROPAGATION_REQUIRES_NEW等效于EJB中的TX_REQUIRES_NEW。如果我们想让服务方法始终在事务中运行,我们可以使用PROPAGATION_REQUIRED。当使用PROPAGATION_REQUIRED时,如果已经运行了一个TX,bean方法就会加入到该TX中;否则的话,Spring的轻量级TX管理器就会启动一个TX。如果在调用组件服务时我们总是希望开始新的事务,那么可以利用PROPAGATION_REQUIRES_NEW属性。

  我们还指定,当方法抛出FacadeException类型的异常时,addLineItem就总是回滚事务。这就达到了另一个粒度级别:在异常场景中,我们的控制可以精细到TX的具体结束方式。前缀符号“-”指定回滚TX,而前缀符号“+”指定提交TX。

  接下来的问题是,为什么我们要为log方法设置PROPAGATION_REQUIRES_NEW属性呢?这是由我们的以下需求决定的:无论主服务方法发生什么情况,对所有创建订单以及向系统添加线项目的尝试都要记录审计跟踪。也就是说,即使在createOrderList和addLineItem的实现过程中出现了异常也要记录审计跟踪。这仅在启动一个新的TX并在这个新的TX上下文中调用log的时候起作用。这就是为什么要为log设置PROPAGATION_REQUIRES_NEW TX属性的原因:如果对下述方法的调用成功了

auditManager.log(new AuditObject
(LINE_ITEM +lineItemId, CREATE));

  ,auditManager.log()就将在新的TX上下文中执行,而且只要auditManager.log()本身成功(即,没有抛出异常),新的上下文就会被提交。

  设置演示环境

  准备演示环境时,我参考了这本书的流程:

  1. 并安装以下组件,这时请注意使用准确的版本,不然就会引起版本不兼容问题。
    • 或更高版本
  2. 在系统中设置以下环境变量:
    • JAVA_HOME
    • CATALINA_HOME
    • ANT_HOME
  3. 把下列目录添加到您的PATH环境变量中,或者使用完全路径来执行脚本:
    • JAVA_HOMEin
    • CATALINA_HOMEin
    • ANT_HOMEin
  4. 要设置Tomcat,在文本编辑器中打开/conf/tomcat-users.xml文件,验证以下各行是否存在。如果不存在,必须手动添加进去:

  password="admin" roles="manager"/> 
  1. 要创建基于Struts、Spring和Hibernate的Web应用程序,必须用Equinox来构建一个基本的框架程序(bare-bones starter application),它将包含预定义的文件夹结构、所有需要用到的.jar文件以及Ant构建脚本。把Equinox解压到一个文件夹中,它将创建一个equinox文件夹。将目录更改为equinox文件夹,输入命令ANT_HOMEinnt new -Dapp.name=myusers。这样就会创建一个与equinox同级的文件夹myusers。该文件夹的具体内容如下:
    Equinox Myusers Application Folder Template

  图4 Equinox的myusers应用程序文件夹模板

  删除myuserswebWEB-INF文件夹下的所有。xml文件。

  复制equinoxxtrasstrutswebWEB-INFibstruts*.jar文件至myuserswebWEB-INFib文件夹下,这样,这个示例应用程序就可以利用struts了。

  从参考资料小节的示例代码中,解压myusersextra.zip到一个合适的位置。将目录更改为新创建的myusersextra文件夹,复制myusersextra文件夹中的所有内容,并将它们粘贴到myusers文件夹。

  打开命令提示符,将目录转至myusers目录下。执行CATALINA_HOMEinstartup.要从myusers文件夹启动Tomcat,这一点非常重要,否则数据库将不会创建在myusers文件夹中,从而导致在执行一些定义在build.xml中的任务时出现错误。

  再次打开命令提示符并将目录转至myusers目录下。执行ANT_HOMEinnt install.这将构建应用程序并把它部署到Tomcat中。这时,我们可以看到myusers中多了一个db目录,以便存放数据库appfuse1和appfuse2.

  打开浏览器并验证myusers应用程序已经部署在:8080/myusers/上了。

  要重新安装应用程序,执行ANT_HOMEinnt remove,然后执行CATALINA_HOMEinshutdown关闭Tomcat.现在,从CATALINA_HOMEwebapps文件夹删除所有的myusers文件夹。然后执行CATALINA_HOMEinstartup重新启动Tomcat,并通过执行ANT_HOMEinnt install重新安装应用程序。

  运行演示

  为了运行测试用例,myusers estmxampleservice中提供了一个JUnit测试类,OrderListManagerTest.要执行它,可以在构建应用程序的命令提示符中输入以下命令:

CATALINA_HOMEinnt test -Dtestcase=OrderListManager  测试用例分为两个主要部分:第一部分创建一个由两个线项目组成的订单,然后把这两个线项目链接到订单中。它可以成功运行,如下所示:

OrderList orderList1 = new OrderList();
Long orderId1 = orderListManager.
        createOrderList(orderList1);
log("Created OrderList with id '"
        + orderId1 + "'...");
orderListManager.addLineItem(orderId1,lineItem1);
orderListManager.addLineItem(orderId1,lineItem2);

  第二部分执行类似的操作,但是这次我们试图向订单添加三个线项目,这将产生一个异常:
OrderList orderList2 = new OrderList();
Long orderId2 = orderListManager.
        createOrderList(orderList2);
log("Created OrderList with id '" + orderId2 + "'...");
orderListManager.addLineItem(orderId2,lineItem3);
orderListManager.addLineItem(orderId2,lineItem4);
//We know, we will have an exception here,still want to proceed
try{
  orderListManager.addLineItem
        (orderId2,lineItem5);
}
catch(FacadeException facadeException){
  log("ERROR : " + facadeException.getMessage());

}

  控制台的输出如图5所示:

Client Side Console Output
图5 客户端控制台输出

  我们创建了Order1,并向其添加了两个ID为1和2的线项目。然后我们创建Order2,并尝试添加3个项目,前两个(ID为3和4)添加成功,但是图5显示,添加第三个项目(ID为5)时业务方法遇到了异常。因此,业务方法TX被回滚,数据库中没有ID为5的线项目。从控制台执行以下命令,就可以通过图6和图7进行验证:

CATALINA_HOMEinnt browse1


Orders Created in appfuse1 DB

图6 appfuse1数据库中创建的订单

Line Items Created in appfuse1 DB

图7 appfuse1数据库中创建的线项目

  在接下来的也是最重要的演示部分中可以看出,订单和线项目保存在appfuse1数据库中,而审计对象保存在appfuse2数据库中。实际上,OrderListManager中的服务方法可以与多个数据库交互。启动appfuse2数据库,查看审计跟踪,如下所示:

CATALINA_HOMEinnt browse2

Audit trail created in the appfuse2 database, with entry for failed TX

图8 创建到appfuse2数据库中的审计跟踪,包括失败TX的记录项

  表8最后一行尤其值得注意,RESOURCE列显示这一行对应的是LineItem5。但是当我们回过来看图7时,却发现并没有对应于LineItem5的线项目。哪里出错了呢?事实上并没有出错,图7没有的那一行其实正是这篇文章的关键所在,让我们来看看是怎么回事。

  我们知道,addLineItem()方法包含PROPAGATION_REQUIRED属性,而log()方法有PROPAGATION_REQUIRES_NEW属性。而且addLineItem()在内部调用了log()方法。因此,当我们试图向Order2添加第三个线项目时,就(按照我们的业务规则)引发了异常,于是这个线项目的创建以及将其链接到Order2的操作都被回滚了。但是,因为还从addLineItem()中调用了log(),还因为log()具有PROPAGATION_REQUIRES_NEW TX属性,回滚addLineItem()将不会造成回滚log(),因为log()是在一个新的TX中执行。

  让我们对log()的TX属性做一下改动,把PROPAGATION_REQUIRES_NEW替换为PROPAGATION_SUPPORTS。PROPAGATION_SUPPORTS属性允许服务方法在客户端有TX上下文时在客户端TX中运行;如果客户端没有TX,就不用TX而直接运行。您可能必须重新安装应用程序,以便数据库中已经可用的数据可以自动刷新。重新安装请按照“设置演示环境”中的步骤12进行。

  如果再次运行,我们会发现一点不同。这次,在试图向Order2添加第三个线项目时依然有异常,这将回滚试图添加第三个线项目的事务。而这个方法又调用了log()方法。但是由于它的PROPAGATION_SUPPORTS TX属性,log()将在与addLineItem()方法相同的TX上下文中调用。由于addLineItem()回滚,log()也回滚了,没有留下回滚的TX的审计跟踪。所以在图9中没有对应于失败TX的审计跟踪记录项!

Audit Trail Created In appfuse2 DB, Without Entry For Failed TX

图9 创建在appfuse2数据库中的审计跟踪,没有失败TX的记录项

  我们改动的仅仅是Spring配置文件中的TX属性,就产生了这样不同的事务行为,如下所示:

   
       

           
                PROPAGATION_SUPPORTS
           


       

   

  这就是声明式事务管理的效果。自从EJB出现以来我们就一直在使用这种方法,但是我们需要一个高端的应用服务器来驻留EJB组件。现在,我们可以看到,利用Spring,没有EJB服务器也可以达到类似的结果。

  结束语

  这篇文章重点介绍了J2EE领域的强大组合之一:Spring和Hibernate。利用二者的功能,现在对于容器管理持久性(Container-Managed Persistence,CMP)、容器管理关系(Container-Managed Relationships,CMR)和声明式事务管理,我们多了一种技术选择。虽然Spring不能视为EJB的替代方案,但是它提供的许多功能,例如普通对象的声明式事务管理,使得在许多项目中没有EJB也完全可以。

  本文的目的不是要寻找EJB的替代方案,而是为当前的问题找出一个最可行的技术方案。至于Spring和Hibernate的轻量级组合的更多功能,就留给我们的读者去探索了。

【责编:Amy】

--------------------next---------------------

阅读(302) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~