本文选自迷你书《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 { <strong>InitialContext ctx = new InitialContext(); </strong> <strong>UserTransaction txn = (UserTransaction) </strong> <strong>ctx.lookup("java:comp/UserTransaction"); </strong> try { <strong>txn.begin(); </strong> ... Placement placement = placementService.placeTrade(trade); executionService.executeTrade(placement); <strong>txn.commit(); </strong> } catch (TradeUpdateException e) { <strong>txn.rollback(); </strong> 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 { <strong>@TransactionAttribute( </strong> <strong>TransactionAttributeType.SUPPORTS) </strong> public BaseCommand executeRead(BaseCommand command) throws Exception { CommandImpl implementationClass = getCommandImpl(command); return implementationClass.execute(command); } <strong>@TransactionAttribute( </strong><strong>TransactionAttributeType.REQUIRED) </strong> public BaseCommand executeUpdate(BaseCommand command) throws Exception { try { CommandImpl implementationClass = getCommandImpl(command); return implementationClass.execute(command); } catch (Exception e) { <strong>sessionCtx.setRollbackOnly(); </strong> throw e; } } }
上面例子中的 getCommandImpl() 方法使用反射(reflection)来装载和实例化命令实现对象。而后它被执行,然后通过命令对象向客户端返回结果。注意,该实现与“领域服务拥有事务的设计模式”非常相似,因为我们对读取操作附加了 Supports 的事务属性,并且对更新相关操作附加了 Required 的事务属性,并辅以 setRollbackOnly() 方法。
Spring 框架
在 Spring 框架下,这个模式完全通过 Spring 的 XML 配置文件实现。在 EJB 实现中需要调用 setRollbackOnly() 方法,而在 Spring 中,通过配置文件中的回滚规则指令就可以处理了。以下的配置代码展示了本模式如何设置服务器端代理组件(命令处理器)去处理更新和读取操作(事务逻辑加用粗体):
<p><!-- 定义服务器端代理命令处理器 --></p><bean id="commandProcessorTarget" class="com.commandframework.server.commandProcessorImpl"></bean> <bean id="commandProcessor" <b>class="org.springframework.transaction.interceptor. TransactionProxyFactoryBean"></b> <b><property name="transactionManager" ref="txnMgr"/></b> <property name="target" ref="tradingServiceTarget"/> <property name="transactionAttributes"> <props> <b><prop key="executeUpdate"></b> <b>PROPAGATION_REQUIRED,-Exception </b> <b></prop></b> <b><prop key="executeRead">PROPAGATION_SUPPORTS </prop></b> </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 。
感谢张龙对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论