《Java事务设计策略》服务器端代理拥有事务的设计模式
当您在应用架构中用到命令模式(Command Pattern)或服务器端代理设计模式(Server Delegate Design pattern)时,本章描述的事务设计模式就比较适合了。在本模式中,服务器端代理组件,作为对服务器的远程接入点,拥有事务并负责对事务实施全面的管理。其他任何组件,包括客户端组件、领域服务组件、或是持久化组件都不负责管理事务,它们甚至不会察觉到它们正在使用到了事务。
命令模式是一种非常有用的设计模式,它解决了关于客户端事务管理以及EJB中的很多常见问题。这种设计模式背后最基本的原则是,客户端功能被包装在所谓”命令(command)“中,提交给服务器端以便执行。该命令可能包含了一个或多个对领域服务方法的调用。然而,这些领域服务方法的调用都是通过服务器端的被称为“命令实现(Command Implementation)”的对象去执行的,而不是在客户端执行。使用命令模式,使得用户可以从客户端对领域服务组件发起单一请求,并同时允许服务器端而不是客户端来管理事务处理过程。
服务器端代理模式的机制类似,唯一不同的是,它没有使用类似于命令模式的框架,而是简单地将客户侧的业务代理逻辑放置在服务器端的代理对象中去了。最终结果是一样的,事务处理过程被移到了服务器端,并且客户端对服务器的请求从多个减少到一个。
“服务器端代理拥有事务的设计模式”是“领域服务拥有事务模式”的特例。两者的主要区别在于,如果使用命令模式,会有“一个独立的对象”拥有事务,而不是由“一类组件”拥有事务。例如,如果您的应用包含40个不同的领域服务,在“领域服务拥有事务模式”下,可能由40个bean去管理事务。然而,在“服务器端代理拥有事务的设计模式”下,您只需要一个bean(命令执行者,Command Processor)去管理事务。
背景(Context)
下面的代码示例展示了在EJB环境下,为了完成一个业务请求,客户端对象必须形成多次对服务器调用的场景。
public class ClientModel { public void placeFixedIncomeTrade(TradeData trade) throws Exception { InitialContext ctx = new InitialContext(); UserTransaction txn = (UserTransaction) ctx.lookup("java:comp/UserTransaction"); try { txn.begin(); ... Placement placement = placementService.placeTrade(trade); executionService.executeTrade(placement); txn.commit(); } catch (TradeUpdateException e) { txn.rollback(); log.fatal(e); throw e; } } }
在这个实例中,客户端必须使用 编程式事务来确保一个独立的事务性工作单元中满足ACID特性。在此情形下,不仅仅是客户端要负责事务管理,而且因为多次对(远端)领域服务的调用,性能也受到影响。还有,在这个例子中,领域服务由无状态会话Bean的EJB实现,更大程度上使得架构复杂化。
为了简化此类情景,我们需要将无状态会话Bean实现的领域服务重构为POJO,从客户端移除事务逻辑,而后为所涉及的所有客户端请求构造一个单一的对服务器端的调用。我们可以使用命令模式或是服务器端代理设计模式去达到部分或全部的目标。当应用这两个模式中任何一个时,本来位于客户层的事务处理将被搬移到服务器层了。下面的代码示例展示了利用命令模式后,改动过的客户端代码:
public class ClientModel { public void placeFixedIncomeTrade(TradeData trade) throws Exception { PlaceFITradeCommand command = new PlaceFITradeCommand(); command.setTrade(trade); CommandHandler.execute(command); } }
可以看出,这是对之前代码做了非常大简化的版本。然而,有一个问题留下来了,“事务处理的逻辑在哪里呢”?在这个场景中,可以使用“服务器端代理者拥有事务”设计模式,甄别由哪个组件负责事务管理,以及对这些组件声明式事务该如何设定。
制约条件(Forces)
- 使用命令模式和服务器端代理设计模式设计应用架构和客户端与服务器的通信。
- 客户端总是与服务器端代理进行单一交互以完成业务请求。
- 必须保证ACID特性,意味着需要进行事务处理以维护数据完整性。
- 领域服务对象使用POJO实现,可以位于远程或本地。
- 命令处理器或服务器端代理是从客户端到领域服务的单一访问点。
解决方案(Solution)
当使用命令模式或服务器端代理设计模式时,服务器端代理者拥有事务设计模式能够作为此类应用架构的整体事务设计策略。该模式使用将整个事务管理的责任放在服务器端代理组件上的责任模型。当使用命令模式时,服务器端代理组件实现为单一的命令处理器(Command Processor)组件,该组件定位有关的命令实现对象并执行命令。在EJB中,该组件通常被实现为无状态会话Bean。在Spring中,它可以被实现为Spring管理的bean(POJO)。当使用服务器端代理设计模式时,每个客户端请求的功能集合可以实现为彼此独立的服务器端代理(Spring中的POJO或是EJB中的SLSB)。
下图说明了在EJB和Spring框架两种情况下本模式的实现细节:
服务器端代理组件使用声明式事务,有关的更新方法被赋予Required的事务属性,而所有的读取方法被赋予Supports属性。服务器端代理组件在处理应用异常时,也会负责调用setRollbackOnly()方法。
这种模式适用的应用架构非常单一。在命令模式的用例下,服务器端代理被实现为命令处理器,简单地接收客户端发送来的命令对象,执行这些命令。在命令模式框架下自始至终使用接口(interface)的编程风格则保证了命令接收(Command Handler)组件、命令处理(Command Processor)组件、命令实现接口(Command Implementation Interface)、以及命令接口(Command Interface)本身都保持通用和应用无关性。服务器端代理建立的事务上下文被传播到它调用到的所有对象。
在服务器端代理设计模式中,服务器端代理组件是作为对应用架构中领域服务组件的客户端门面存在的。在这种设计模式中,客户端的逻辑实际上被搬到了服务器,并放在客户端代理组件中。
这些设计模式的主要缺点是,服务器端代理组件包含了客户端的逻辑,而不是纯的服务器逻辑。并且,本模式在很多情况下比较难于实现,因为客户端业务代理常常是与使用的web框架紧密绑定的。这种情况直接的例子是struts,在struts框架中Action类具备扮演客户端业务代理角色的能力(事实上很多时候它也是这么用的)。此外,也许将客户端逻辑搬走很难,因为其代码包含了对完全基于客户端的对象,例如HTTPSesssion、HTTPRequest,以及HTTPResponse的引用,这些对象要搬到服务器侧去是比较困难的。
然而,这两种设计模式最为明显的一个优势在于,包含处理请求业务逻辑主体的领域服务组件实现为POJO(简单Java对象),而不是EJB。因此领域服务组件从EJB框架中解耦合,使得它们易于测试。服务器端代理拥有事务的设计模式另一个独特之处在于,由于服务器端代理常常实现为一个无状态会话Bean的单例(singleton)组件,整个应用的事务逻辑位于单一的对象内。因此,从实现和维护的观点讲,它是最简单的事务设计模式。并且,该事务设计模式将事务管理的责任重担放在了领域服务组件的上面一层,将应用中服务器端的核心功能从事务管理之类的基础架构方面中解放了。通过使用本模式,领域服务组件可用POJO编写,由于它们不包含事务逻辑,在容器外的环境中测试就十分方便。
后果(Consequences)
- 客户端(无论哪种类型)不包含任何事务逻辑,不管理事务处理的方方面面。
- 由于服务器端代理组件开启和管理事务,更新方法在碰到应用异常时必须调用setRollbackOnly()方法。
- 服务器端代理建立的事务被传播到基于POJO的领域服务对象,同时也传播到领域服务用到的持久化对象(无论使用哪一种持久化框架)。而且,这些组件不包含任何事务或回滚的逻辑。
- 服务器代理对象使用声明式事务,对更新相关的方法应用Required的事务属性,对读取操作应用Supports属性。
- 为了维护ACID特性,客户端对象绝不开启事务、提交事务,或将事务标记为回滚。
- 持久化对象和领域服务不包含任何事务或回滚逻辑。
- 如果使用EJB2.1的实体Bean,更新操作的事务属性必须设置为Mandatory,并不要使用任何回滚逻辑。对读取操作而言,如果使用容器管理持久化(Container-managed Persistence,CMP),需要为实体Bean设置Required的事务属性;如果使用Bean管理的持久化(Bean-managed Persistence,BMP),则需要设置事务属性为Supports。
实现(Implementation)
下面的代码分别展示了EJB和Spring下这种模式的实现。为举例方便的需要,我们假设使用命令模式。对EJB,我假设命令处理器用无状态会话Bean实现;对Spring,我假设命令处理器为Spring框架所管理。
EJB
在命令模式下,该事务设计模式只有一个组件包含事务代码(即事务处理器组件)。因此,由于客户端不包含事务代码,我们仅仅有必要展示服务器端代理(事务处理器)关于这个模式的代码实现。EJB中的领域服务组件,针对更新和读取操作的代码如下所示(事务逻辑用粗体表示):
@Stateless public class CommandProcessorImpl implements CommandProcessor { @TransactionAttribute( TransactionAttributeType.SUPPORTS) public BaseCommand executeRead(BaseCommand command) throws Exception { CommandImpl implementationClass = getCommandImpl(command); return implementationClass.execute(command); } @TransactionAttribute( TransactionAttributeType.REQUIRED) public BaseCommand executeUpdate(BaseCommand command) throws Exception { try { CommandImpl implementationClass = getCommandImpl(command); return implementationClass.execute(command); } catch (Exception e) { sessionCtx.setRollbackOnly(); throw e; } } }
上面例子中的getCommandImpl()方法使用反射(reflection)来装载和实例化命令实现对象。而后它被执行,然后通过命令对象向客户端返回结果。注意,该实现与“领域服务拥有事务的设计模式”非常相似,因为我们对读取操作附加了Supports的事务属性,并且对更新相关操作附加了Required的事务属性,并辅以setRollbackOnly()方法。
Spring框架
在Spring框架下,这个模式完全通过Spring的XML配置文件实现。在EJB实现中需要调用setRollbackOnly()方法,而在Spring中,通过配置文件中的回滚规则指令就可以处理了。以下的配置代码展示了本模式如何设置服务器端代理组件(命令处理器)去处理更新和读取操作(事务逻辑加用粗体):
<!-- 定义服务器端代理命令处理器 --><bean id="commandProcessorTarget" class="com.commandframework.server.commandProcessorImpl"></bean> <bean id="commandProcessor" class="org.springframework.transaction.interceptor. TransactionProxyFactoryBean"> <property name="transactionManager" ref="txnMgr"/> <property name="target" ref="tradingServiceTarget"/> <property name="transactionAttributes"> <props> <prop key="executeUpdate"> PROPAGATION_REQUIRED,-Exception </prop> <prop key="executeRead">PROPAGATION_SUPPORTS </prop> </props> </property> </bean>
因为这个模式针对服务器端代理组件确定了声明式事务的使用,在领域服务组件中无论是更新还是读取的代码都不包含任何事务逻辑。如之前讲到的,setRollbackOnly()的逻辑被Spring自动处理了,我们可以从上面XML代码中-Exception那一段看出端倪。下面则演示了在Spring中实现该模式不需要任何事务逻辑的Java代码:
public class CommandProcessorImpl implements CommandProcessor { public BaseCommand executeRead(BaseCommand command) throws Exception { CommandImpl implementationClass = getCommandImpl(command); return implementationClass.execute(command); } public BaseCommand executeUpdate(BaseCommand command) throws Exception { CommandImpl implementationClass = getCommandImpl(command); return implementationClass.execute(command); } }
注意,在本例中,Spring在两个方法中的实现实质上做了同样的事情。不像EJB实现,异常处理并不必要,因为setRollbackOnly逻辑已经包含在XML的回滚策略设置中了。
关于作者
Mark Richards是IBM认证的高级IT架构师,他在IBM公司从事大型系统面向服务架构的设计和架构工作,使用J2EE与其他技术,主要为金融行业服务。作者早在1984年起就加入软件行业,从开发人员做起,直至设计师、架构师。他经常在著名论坛“No Fluff Just Stuff”演讲,他从波士顿大学获取了计算机科学硕士学位,持有SUN、IBM、BEA的多个Java与架构师认证。如有关于本书的评论或疑问,尽请 联系Mark。
本文选自迷你书 《Java事务设计策略》 的第九章,译者翟静。
[ comments ]