WebSphere Process Server 中业务流程和业务数据的解耦:MVC 模式的新转折

本文描述了将业务流程与相应业务数据的生命周期分隔开的一种开发模式。这种方法提供了对相应生命周期的解耦 (decoupling),从而增强了应对变化的能力。业务流程只包含尽可能少的信息,实际的业务数据是通过逻辑层中的托管 Java Persistence API 实体来表示的。相同实体是以数据传输对象的形式作为逻辑层和用户界面层之间的独立实体被交换的。本文先介绍这个架构的各层,然后讨论在 WebSphere® Integration Developer V7.0 和 WebSphere Process Server V7.0 中实现基于这个模式的基本应用程序的步骤。

Walter M. Jenny, 业务解决方案架构师, IBM

Walter Jenny 是 Business Performance and Services Optimization (BPMO) 团队的业务解决方案架构师,有超过 20 年从业经验。他已经在 IBM Software Group 工作了三年。他专长的领域包括业务集成和业务过程管理。Walter 拥有计算机学科硕士学位、企业管理硕士学位和企业管理博士学位。



2011 年 9 月 01 日

简介

将业务流程与其数据分隔开是业务流程管理领域的长期主题。这种方法提供了对相应生命周期的解耦,从而增强它们应对变化的能力:可以使用成熟的技术修改流程,同时还可以利用持久化特性独立地操纵数据。因此,消息模型(它们定义流程与其支持服务之间的合约)占用的数据非常少,只使用了少量关键值。这让版本化相当简便。

这种方法还避免传输流程很可能不需要的过多的数据,因此让流程可以更高效地运行。在长时间运行的流程中,WebSphere Process Server(后面简称为 Process Server)在对 Business Process Choreographer (BPC) 数据库执行每个事务之后存储业务流程的状态。每当对数据库执行写操作后,对 Service Data Object (SDO) 进行序列化,在执行读操作后,则进行反序列化。如果在外部保存数据,则不需要这些步骤,而且可以根据需要装载数据。

需要做的只是维护流程和数据之间的引用,并通过维护状态组合业务流程。有一些文章讨论了分隔流程与数据的方法(例如 Claim Check pattern)。但是,本文更进了一步,讲解如何在单独的数据库中持久保存所有业务数据,并通过 Java™ Persistence API (JPA) 访问它们。可以使用独立的 JPA 实体作为架构的各层之间的数据传输对象 (DTO)。

为了创建与数据相关的工件,我们要借用模型驱动的开发 (MDD) 的一些概念。这种方法会显著加快开发过程,因为可以通过共用的模型更好地控制业务数据,所以可以跨所有主要架构层重用这个模型。

在本文中,我们使用一个示例场景解释了如何实现采用这个模式的应用程序。“下载” 中的 示例应用程序 演示这种方法,并提供了所有基本构建块,您可以以它为基础开发自己的解决方案。这个应用程序包含基于 ZK Open Source Framework 的用户界面和一种查询任务列表的高效方法。


主要架构层

假设您正在开发一个银行应用程序,它在前端办公室中创建交易,在后端办公室中审查并批准这些交易,并最终执行它们。关于这个应用程序的主要架构层和构建块,请参见图 1。

图 1. 主要架构层
主要架构层
  • 因为我们使用了 ZK 框架,所以浏览器仅仅显示用户界面 (UI) 组件,并通过 Asynchronous JavaScript and XML (Ajax) 监听用户的活动。换句话说,它仅仅反映用户界面层的状态。可以通过 ZK 自动地执行同步,这些对于应用程序是透明的。
  • 用户界面层在 Web 层中运行。它控制浏览器中的 UI 组件,并通过客户机外观 Enterprise JavaBeans (EJB) 与 WebSphere Process Server V7.0 Human Task Manager (HTM) 和逻辑层进行通信。UI 提供创建新交易、查询 Process Server 任务列表以及声明和完成任务的基本特性。
  • 流程层组合服务并创建人工任务。流程在 WebSphere Business Modeler V7(后面简称为 Business Modeler)中建模,然后将其导入 WebSphere Integration Developer V7.0(后面简称为 Integration Developer)。在逻辑层中,可以将实际的服务实现作为 Service Component Architecture (SCA) 组件调用。
  • 逻辑层包含实际的应用程序逻辑,逻辑实现为 EJB 3.0 无状态会话 bean。这些 EJB 是作为 SCA 公开的,流程可以调用它们。UI 使用独立的 JPA 实体与客户机外观 EJB 进行通信,提供了足以让 UI 层能够完成其任务的、粗粒度的对业务逻辑和托管 JPA 实体的访问。外观 EJB 接收到独立的实体后会提取一个托管版本,无状态服务将使用该版本执行业务逻辑。

图 2 显示 Human Task Manager 的远程接口,以此说明总体通信。

图 2. 使用 JPA 作为 DTO
使用 JPA 作为 DTO
  • JPA 持久化层有两个用途:作为托管 JPA 实体为逻辑层提供对象关系映射 (ORM) ,作为逻辑层和 UI 层之间的独立实体提供 DTO。

    独立的实体作为完整的对象或通过惰性 (lazy) 装载传输到和传输出 UI。在实现该操作时,我们选用了 WebSphere Application Server 附带的 OpenJPA。JPA 类是根据 Unified Modeling Language (UML) 模型生成的。

  • 底层的数据层说明了如何在应用程序的完全控制下,在单独的数据库实例中保存业务数据。

所需的项目

四个主要的层都有自己的 Integration Developer 项目,它们实现了图 3 所示的集成解决方案。

图 3. WebSphere Integration Developer 集成解决方案
WebSphere Integration Developer 集成解决方案
  • Trading_UI 项目使用 Trading_LogicClient 项目中的远程接口与 Trading_Logic EJB 通信。通过独立的 JPA 实体交换数据。
  • Trading_Process 项目也使用远程接口与 EJB 通信。但是,它不使用 JPA 对象,因此可以确保流程和业务数据之间的严格隔离。
  • Trading_Logic 项目实现由 EJB 3.0 无状态会话 bean 组成的后端逻辑。这些 EJB 创建并操纵业务数据,通过 Trading_Persistence 项目保存数据。
  • 最后,Trading_Persistence 项目包含用于持久化的带注解的 Java 类。

我们来仔细看看每个项目,讨论如何把它们集成为一个一致的应用程序。

用户界面层

对于用户界面,我们使用了 ZK Open Source Framework,它是一个用 Java 编写的开放源码 Asynchronous JavaScript and XML (Ajax) 框架。使您能够开发 Web 2.0 风格的富 Internet 应用程序,无需编写任何 JavaScript 代码。它是开发便于维护的富用户界面的出色工具。用 Java 编写所有代码,无需了解 JavaScript。ZK Studio 是 Eclipse 和 NetBeans 的插件。它让在 Integration Developer 中创建 UI 项目变得非常方便和直观。

使用 ZK 框架的富 Internet 应用程序 已经介绍了基本的 ZK 特性,所以我们只讨论我们的场景需要的方面。

ZK 中支持的 MVC 模式有助于清晰地分隔各个应用程序层。在 ZK 中,在 ZUL 文件中用 ZUML 声明 UI 组件,在 Java 控制器类中实现使用组件的功能。控制器类扩展 GenericForwardComposer,所以可以在 Java 代码中引用 ZUL 中声明的组件。这允许将事件自动地转发给控制器类进行处理。我们来看一下主要页面的处理方式。首先,构建一个名为 IndexController 的控制器类,它扩展了 GenericForwardComposer(参见清单 1)。

清单 1. 主控制器的 Java 代码
package com.bigbank.trading.ui.controller;
public class IndexController extends GenericForwardComposer

创建这个类之后,构建 UI ZUL 文件,然后引用相关联的控制器。通过指定 apply 属性引用控制器(参见清单 2 中的粗体代码)。

清单 2. 主控制器的 ZUL 代码
<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" ?>
<window id="main" border="normal"apply="com.bigbank.trading.ui.controller.IndexController">
    <borderlayout>
        <north>
            <include src="menu.zul" />
        </north>
        <west title="Lists" size="39%" flex="true" splittable="true" 
             collapsible="true">
            <tabbox>
                <tabs id="listTabHeader" ></tabs>
                <tabpanels id="listTabBody"></tabpanels>
            </tabbox>
        </west>
        <center title="Todos" flex="true">
            <tabbox>
                <tabs id="todoTabHeader" ></tabs>
                <tabpanels id="todoTabBody"></tabpanels>
            </tabbox>
        </center>
    </borderlayout>
</window>

通过应用 GenericForwardComposer,可以在控制器类中把组件声明为私有变量。通过定义匹配的 ID 值,自动地连接到相应的 ZUL 组件,参见清单 3。

清单 3. 自动连接

点击查看代码清单

清单 3. 自动连接

public class IndexController extends GenericForwardComposer
{
    // Auto wired ZUML elements
    private Tabs		listTabHeader;private Tabpanels	listTabBody;private Tabs		todoTabHeader;private Tabpanels	todoTabBody;
    ...

另一个出色的 ZK 特性是数据绑定。带注解的数据绑定(清单 2 中的第一行)允许将 bean 属性声明为组件属性值,这样视图和模型就会自动保持同步。在幕后,数据绑定器通过调用 bean 的获取方法或设置方法获取或设置 UI 组件中的值。

在以下 Java 类中实现 UI 的主要功能:

  • 中心元素 IndexController 负责创建和控制其他相关组件。
  • CreateController 允许输入一些用于开始新 Process Server 流程的数据。
  • ListController 包含取自 Human Task Manager 的当前可用任务集合,在一个 ZK Grid 中显示它们。另一种做法是,对数据库运行查询并显示结果。列表中的每个记录由一个 ListEntry 表示。
  • FormController 处理实际数据,它通过外观 EJB 获取和保存数据。对于我们的示例,它是一个简单的表单。在实际应用程序中,它可能是包含许多字段的复杂表单。在这种情况下,可以只读取初始屏幕需要的部分数据。当用户进入表单的其他部分时,再获取底层数据。
  • SearchController 是实现基本搜索功能的辅助类。

在图 4 中,可以看到使用 ZK 的 borderlayout 的 UI 设计。动态地创建各个列表并把它们添加在左边,实际的表单放在右边。

图 4. 基本的 UI
基本的 UI

在后面集成所有部分时,我们会再次讨论 UI。

流程层

对于我们的示例场景,使用一个简单的流程,它通过组合三个基本服务和两个人工任务批准和审查交易,参见图 5。

图 5. 业务流程
业务流程

银行前端办公室中的用户通过创建 Process Server 实例提出新的交易,交易要经历以下处理步骤:

  • Create 服务插入包含业务数据的数据库记录,返回一个状态,表示:
    • 需要审查
    • 不需要审查
  • 后端办公室中的用户通过 Review 人工任务审查交易,决定:
    • 需要批准
    • 不需要批准
  • 如果需要批准,担任另一个角色的用户必须做出最终决策:
    • Approved 导致直接执行(和批准)交易。
    • Declined 会发送一个通知。
    • Needs rework 意味着交易看起来是合理的,但是需要审查者做一些调整。
  • 最后,ExecuteNotify 是两个简单的组件,可以执行业务逻辑。

我们感兴趣的是在流程中传输的业务项。它只包含两个属性,刚好够将流程和相应的业务数据链接起来,参见图 6。

图 6. 业务项
业务项
  • tid 是一个包含交易 ID 的整数,交易 ID 是在数据库中插入初始记录时由 JPA 创建的唯一键。在后面的服务调用中,可以使用它获取特定 Process Server 流程实例的业务数据。
  • status 是服务和人工任务返回的字符串。流程根据它的值控制流程流。为了提高类型安全性,这也可以是一个枚举。

导入到 Integration Developer 中之后,相应的 SDO 如图 7 所示。

图 7. 相应的 SDO
相应的 SDO

在流程中如何使用这个 SDO?在调用服务或人工任务时,它提供流程与相关数据之间的链接。

但是,要想把 BPEL 流程与适当的业务数据集关联起来,还需要一个 “反向” 引用。该引用的自然选择是 Process Server 流程实例 ID (PIID)。它是由 Process Server 动态地分配的。可以在流程开始时通过 Java 片段获取它,把它作为属性存储在数据库表中,参见图 8(也见 图 13)。

图 8. 设置 PIID
设置 PIID

图 8 显示在 Integration Developer 中导入的流程模型,包括 "Assign PIID" 片段。在流程实例化之后,即可在交易 SDO 中设置 PIID 值,参见清单 4。

清单 4. 在 SDO 中设置 PIID
// Set the linkage from the process to the persistent data
com.ibm.bpe.api.ProcessInstanceData pid = processInstance();
String status = Trade.getString( "status" );
Trade.setString("status", "PIID=" + pid.getID().toString() + "|" + status);

Create EJB 会获取这个值,并将它作为属性存储在 Trade 表中。

逻辑层

这一层包含实际的业务逻辑,逻辑实现为无状态 Enterprise JavaBean (EJB)。为了从 Process Server 流程调用它们,这些 bean 作为 SCA 组件公开。作为 EJB 实现逻辑就可以利用依赖项注入,从而获得对其他 EJB 和 JPA 持久化管理器的引用。如果性能很重要,可以通过 EJB 的本地接口访问它们(按引用调用),除非拓扑要求进行远程访问。

EJB Import 使用 Java Naming and Directory Interface (JNDI) 远程接口进行通信,在组装图中通过 Java 桥连接,参见图 9。

图 9. 在组装图中连接 EJB
在组装图中连接 EJB

桥负责流程与服务之间的通信。图 10 所示的两个场景中使用它们。

图 10. Create EJB Import 的桥
Create EJB Import 的桥

作为流程的最初步骤之一,必须在数据库中创建相应的业务数据。图 10 显示 图 1 的一部分的细节,可以从中了解如何用参数调用 Create EJB。返回值是交易 ID (tid),这是在插入数据库记录时生成的键。为了保持示例简单,还返回用于控制流程流的状态值。

清单 5 给出经过简化的 Create EJB Import 代码。

清单 5. Create EJB Import 中的 Java 代码
package com.bigbank.trading.bridge;

public DataObject Create(DataObject input)
{
    int tid = locateService_CreateEJBRemotePartner().
      createTrade(input.getString("/status"));
    input.setInt("/tid", tid);
    return (input);
}
图 11. 后续 EJB Import 的桥
后续 EJB Import 的桥

用交易 ID (tid) 调用图 11 所示的服务。图 11 显示 图 1 的另一部分的细节:如何使用 tid 作为键调用 Execute EJB 来获取业务数据,从而执行所需的业务逻辑。同样,它返回用于控制流程流的状态值。

清单 6 给出了经过简化的后续 EJB Import 代码。

清单 6. 后续 EJB Import 中的 Java 代码
package com.bigbank.trading.bridge;

public DataObject Execute(DataObject input)
{
 String status = locateService_ExecuteEJBRemotePartner().executeTrade(input.getInt
  ("/tid"));
 input.setString("/status", status);
 return (input);
}

因为业务数据已通过 JPA 持久化,所以我们可以使用 EntityManager 创建和删除持久的实体实例、按实体的主键搜索实体以及运行查询。借助容器托管的实体管理器,通过容器将持久化上下文自动传播给单一 Java Transaction Architecture (JTA) 提交范围内使用 EntityManager 实例的所有应用程序组件。要获取 EntityManager 实例,只需将实体管理器注入应用程序组件即可,参见清单 7。

清单 7. 通过依赖项注入获得持久化上下文引用
@Stateless
public class CreateEJB implements CreateEJBRemote
{
    @PersistenceContext(unitName = "Trading-JPA")
    private EntityManager entityManager;
	...

右键单击 Trading_LogicEAR 项目并选择 Java EE > Open WebSphere Application Server Deployment 以配置数据源(图 12)。

图 12. WebSphere Application Server Deployment
WebSphere Application Server Deployment

这个示例使用了内置的 Derby 数据库,物理数据库文件位于临时目录中。关于详细的配置,请参阅 “下载” 中的应用程序

持久化层

这里还缺少一个重要的部分:业务数据,这是所有应用程序的血液。正如您可能经历过的,在开发周期内,数据模型的细节往往会频繁地发生变化。经常需要添加、删除或修改属性。为了更轻松地保持相应的模型和代码同步,我们借用模型驱动的开发 (MDD) 的一些概念来捕捉和交流高层需求,以此为基础创建组成解决方案的工件。

我们在 Rational® Software Architect for WebSphere Software, V7.5.5 (RSA) 中创建了一个 Unified Modeling Language (UML) 模型,然后通过转换来创建更详细的模型和实际代码。图 13 显示了概念性的数据模型(有时候也称为领域模型)。

图 13. 概念性数据模型
概念性数据模型

主要的实体是:

  • Trade:它代表对于一个客户执行的一个物理交易。TradeDetails 包含更详细的信息。
  • Customer:代表交易针对的客户。
  • Settlement:表示交易是否获得批准。

因为希望使用 RSA 内置的 UML 到 JPA 转换创建实际的 JPA 实体,我们用相应的详细信息和模板 (stereotype) 来充实这个概念性模型,参见图 14。

图 14. 逻辑数据模型
逻辑数据模型

在这个逻辑层上,我们添加了几个来自转换概要文件的 UML 到 JPA 模板:

  • 持久的 实体 由模板 <<Entity>> 表示,这是一个轻量的 Java 类,它的状态被存储到 关系数据库 中的表中。这种实体的实例对应于表中的行。
  • 模板 <<Id>> 声明一个或多个字段,它们一起构成实例的持久化标识。这成为物理模型中的主键。
  • 要让持久化实现自动地给标识字段分配唯一的值,请使用模板 <<GeneratedValue>>,将它设置为 GeneratorType.IDENTITY。数据库在执行插入操作时分配标识值。对于 Trade 实体,在流程中使用交易 ID 作为引用从数据库中获取相应的数据。交易 ID 也是图 6 所示的业务项中的属性之一。
  • <<RelationshipOptions>> 定义两个应用了 <<Entity>> 模板的类之间的关联,比如 CascadeFetch 细节。实体 TradeTradeDetails 之间的提取关系被设置为 Lazy,正如在下一节中会看到的,这有助于改进性能和内存占用量。

使用 javax.persistence 包中定义的 Java 5 注解、XML 映射文件或两者的组合指定持久化元数据。为了提高可读性,通过在实体类文件中直接指定的对象或关系元数据表达场景中的关系。在转换中配置这个设置。下面的 UML 到 JPA 转换根据 UML 模型元素生成 JPA 实体和 Java 代码(图 15)。

图 15. UML 到 JPA 转换
UML 到 JPA 转换

对于我们的应用程序,这个转换在 Trading_Persistence 项目中创建 JPA 实体。这包括用 UML 建模的所有 Java 类。

数据层

对于基本的应用程序,数据层非常简单。使用 JPA 工具在 Integration Developer 中创建一个物理数据模型:右键单击 Trading_Persistence 项目,然后选择 JPA Tools > Generate DDL,图 16 所示的向导会出现。

图 16. 物理数据模型:Generate DDL
物理数据模型:Generate DDL

这会在 META-INF 目录中的 Table.ddl 文件中创建物理数据模型。在 Integration Developer 中切换到 Data 透视图,创建与图 11 匹配的数据库连接,右键单击并选择 Run SQL,对数据库运行这个脚本。图 17 显示 Data 透视图中的结果。

图 17. 创建数据库模式
创建数据库模式

这种 “轻量的 MDD 方式” 让您能够控制开发周期并保持一致性。在不得不修改概念性或逻辑数据模型时,可以轻松地重新创建依赖的模型,让所有 JPA 实体保持同步。

现在,在 Integration Developer 中有了所有四个主要构建块,参见图 18。

图 18. 所需的所有项目
所需的所有项目

现在,我们来运行实际的应用程序。


运行应用程序

为了运行应用程序,在 Process Server 7.0 中部署代码并在浏览器中访问 http://localhost:9080/Trading_UI/index.zul(或安装服务器的任何端口)。为了执行使用 Process Server API 所需的身份验证和授权,必须创建一个用户,然后在 Integrated Solutions Console 中为这个 UI 应用程序指定安全角色 "TradingUsers"。

创建流程实例

在第一次启动应用程序时,还没有流程,因此没有要执行的任务。需要在 Process Server 中创建几个新流程。在 web 应用程序中,单击 Asset >Create Trade … 打开一个表单,可以在其中输入数据并创建新流程,参见图 19。

图 19. 创建流程实例
创建流程实例

在 "Todos" 部分中的新选项卡中打开创建表单。该表单由 CreateController 管理,连接到一个 ZK 表单并附带相应的 ZK 数据绑定。"Create" 按钮调用 CreateController 的 start() 方法,参见清单 8(为了简单,删除了错误处理代码)。

清单 8. 启动一个新流程
public void start()
{
    ...
    binder.saveAll();
    DataObject sdo = getStartDataObject();
    sdo.createDataObject(0);
    sdo.setInt("/Input/tid", 0);
    String input = "name=" + trade.getName() + …
    sdo.setString("/Input/status", input);
    // start business process asynchronously
    PIID piid = bfm.sendMessage(startActivity.getServiceTemplateID(),
                       startActivity.getActivityTemplateID(),
                       new ClientObjectWrapper(sdo));
    alert("New Trade created");
}

如清单 9 所示,只需把数据绑定中的字段值存储在一个连接的字符串中即可,在流程中调用 Create EJB 时,会使用这个字符串(在更稳健的实现中,这可以是序列化的 XML 文件)。

清单 9. 创建 EJB
@Stateless
public class CreateEJB implements CreateEJBRemote
{
    @PersistenceContext(unitName = "Trading-JPA")
    private EntityManager entityManager;

    public String createTrade(String status)
    {
        Trade trade = new Trade();
        trade.setTradeDetails(new TradeDetails());
        // set values …

        // Simple business rule
        if (trade.getTradeDetails().getValue() > 1000)
        {
            trade.getTradeDetails().setReview("needs review");
        }
        else
        {
            trade.getTradeDetails().setReview("no review needed");
        }
        entityManager.persist(trade);
        int newKey = trade.getTid();
        return (newKey + "|" + trade.getTradeDetails().getReview());
    }
}

在这里,JPA 会简化开发。要将刚创建的实体持久化,只需调用 entityManagermerge() 方法即可。JPA 会在幕后运行所需的所有 JDBC 代码,并将修改提交给数据库。

注意,在创建流程时,通过简单的业务规则决定是否需要审查。这由流程组合中使用的状态字段值驱动。显然,在真实的场景中,可以使用外部业务规则引擎(比如 ILOG Rules)在外部实现这个业务逻辑。

实现任务列表

创建几个新流程之后,刷新 UI 以显示任务,并等待通过人工参与执行 "Review" 步骤(图 20)。

图 20. 任务列表
任务列表

构建包含大量任务或属性(例如网格中的列)的高效的任务列表可能相当困难。尽管 HTM 提供的 query() 方法可以获取相关的技术信息,但较难的部分是以高效的方式处理大量任务或属性。HTM 为此提供两个内置特性:定制的属性和查询表。

但是,这两种方法都有自己的问题。如果使用很多定制的属性,会对性能造成损害。将查询表与物化视图结合使用会带来很好的性能,但是它们使用本机数据库联接来检索相关记录。这意味着在同一数据库实例中必须有两组表格。

如果使技术数据库与业务数据库完全隔离,就可以解决问题。这使得实现高效的任务列表变得很容易。首先,使用 ZK 的 Grid 组件和数据绑定构建列表,参见清单 10。

清单 10. UI 层中任务列表的 ZUL 代码
<tabpanel apply="com.bigbank.trading.ui.controller.ListController">

<grid id="mainGrid" mold="paging" pageSize="6"
      sizedByContent="true" model="@{controller.gridModel}">
    <columns menupopup="auto" sizable="true">
        <column label="Status" sort="auto(Status)" />
                   ...
    </columns>
    <rows>
        <row self="@{each='list'}">
            <toolbarbutton
                style="font-weight:bold;color:blue;font-style_underline"
                label="@{list.status}" onClick="controller.addTab( self )">
                <custom-attributes listEntry="@{list}" />
            </toolbarbutton>
            <label value="@{list.action}" />
                   ...
        </row>
    </rows>
</grid>

Grid 控件提供几个出色的特性,比如分页、列的排序和隐藏以及定制呈现方式。如清单 10 所示,在 ZUL 文件中连接 ListController

ListController 的作用与 Process Server 中的查询表特性相似。但是,差异在于在 Java 代码中执行联接。另外,可以通过逐页抓取数据来实现基本的分页机制。首先,需要判断分配给当前用户的人工任务数量(清单 11)。

清单 11. UI 层中判断人工任务数量的 Java 代码
public void fillTaskList(int skipTuples)
{
    resultSet = htm.query("COUNT(*)",
            "PROCESS_TEMPLATE.NAME = 'Trade' AND " +
            "(ACTIVITY.STATE = ACTIVITY.STATE.STATE_READY OR " +
            "ACTIVITY.STATE = ACTIVITY.STATE.STATE_CLAIMED) " +
            "AND WORK_ITEM.REASON=WORK_ITEM.REASON.REASON_POTENTIAL_OWNER ",
            null, null, null, null);
    if (resultSet.next())
    {
        taskNumber = resultSet.getInteger(1);
    }
    ...

需要通过这个数值确定 Grid 中可用任务的数量。在下一步中,将要查询当前页面的 HTM 相关详细信息。为此,查询调用接受 "skipTuples" 和"threshold" 参数,参见清单 12。

清单 12. UI 层中查询任务详细信息的 Java 代码
    resultSet =
        htm.query("TASK.TKIID,TASK.STATE,TASK.NAME,PROCESS_INSTANCE.PIID",
          "PROCESS_TEMPLATE.NAME = 'Trade' AND " +
          "(ACTIVITY.STATE = ACTIVITY.STATE.STATE_READY OR " +
          "ACTIVITY.STATE = ACTIVITY.STATE.STATE_CLAIMED) " +
          "AND WORK_ITEM.REASON=WORK_ITEM.REASON.REASON_POTENTIAL_OWNER ",
          // orderByClause
          null,
          // skipTuplesskipTuples,// thresholdthreshold,
          // timeZone
          null);
    int location = skipTuples;
    while (resultSet.next())
    {
        ListEntry listEntry = gridModel.get(location++);
        // Assign the ResultSet details to the listEntry
        ...
        // Add the PIID to the list for the EJB call
        piidCollection.add(listEntry.getPiid());
    }

最后一步是调用外观 EJB 以获取必需的业务数据(例如列表中的列)。然后将这些属性赋值给 ListEntries,将后者作为 Grid 模型,参见清单 13。

清单 13. UI 层中查询业务数据的 Java 代码
    Map<String, Trade> piidMap =
            clientEJBRemote.findTradesByPiid(piidCollection);
    ListIterator<ListEntry> iterator = gridModel.listIterator(skipTuples);
    recordNumber = 0;
    // Set the retrieved trades to the list entries
    while (iterator.hasNext() && recordNumber++ < threshold)
    {
        ListEntry listEntry = iterator.next();
        listEntry.setTrade(piidMap.get(listEntry.getPiid().toString()));
    }
}

findTradesByPiid 方法通过程序代码模拟 “外部联接”。要改进这个查询的性能,请为 piid 属性添加索引,参见清单 14。

清单 14. 逻辑层中查询业务数据的 Java 代码
public Map<String, Trade> findTradesByPiid(Collection<PIID> piidCollection)
{
    Map<String, Trade> piidMap = new HashMap<String, Trade>();
    String queryText = "select trade from Trade trade where trade.piid in ( ";
    for (PIID piid : piidCollection)
    {
        queryText += "'" + piid.toString() + "',";
    }
    queryText += ") order by trade.tid";
    Query queryTrades = entityManager.createQuery(queryText);
    List<Trade> trades = queryTrades.getResultList();

    for (PIID piid : piidCollection)
    {
        for (Trade trade : trades)
        {
            if (trade.getPiid().equals(piid.toString()))
            {
                piidMap.put(trade.getPiid(), trade);
                break;
            }
        }
    }
    return (piidMap);
}

最初,在列表为空时触发这一系列步骤。当用户滚动到包含空的 com.bigbank.trading.ui.model.ListEntryGrid 页面时,触发对下一批任务的提取,参见清单 15。

清单 15. UI 层中触发查询业务数据的 Java 代码
public String getStatus()
{
    if (status == null)
    {
        listController.fetchNextTasks(this);
    }
    return status;
}

public void fetchNextTasks(ListEntry listEntry)
{
    int index = gridModel.indexOf(listEntry);
    fillTaskList(index);
}

这个任务列表实现高效很高,即使是在有大量属性(例如 Grid 中的列)或任务的情况下也是如此。在典型的场景中,即使给用户分配了几千个任务,该用户很可能只执行其中几个。我们实现上会根据需要惰性地提取流程和业务数据。这会显著减少响应时间和内存占用量。

打开 Todo 表单

单击第一列,这会在 "Todos" 部分中打开一个新表单,参见图 21。

图 21. Todo 表单
Todo 表单

这个表单由 FormController 管理,使用外观 EJB 通过独立的 JPA 实体获取最初的数据集。它通过 ZK 的数据绑定填充字段,参见清单 16。

清单 16. UI 层中 Todo 表单的 ZUL 代码
<tabpanel apply="com.bigbank.trading.ui.controller.FormController">
    <grid fixedLayout="false" width="98%" >
        <rows>
            <row>
                <label value="Name" />
                <textbox
                    constraint="no negative: please enter positive value"
                    value="@{controller.asset.name}"
                    style="font-weight:bold" />
                    …
            </row>
        </rows>
    </grid>
    <hbox width="100%" pack="center" spacing="10px">
        <button id="claimButton" label="Claim"
            onClick="controller.claim()" image="/images/cog.png"
            tooltiptext="Claims the Task" />
            …
    </hbox>
</tabpanel>

在大多数业务应用程序中,这个表单包含的信息比这个小应用程序多得多。只有在用户选择一个选项卡或打开另一个窗口时,才需要其中一部分信息,因此我们通过逻辑和 JPA 层惰性地装载它,参见清单 17。

清单 17. 逻辑层中 Todo 表单的 Java 代码
public TradeDetails findTradeDetails(Trade trade)
{
    trade = (Trade) entityManager.find(Trade.class, tid);
    return (trade.getTradeDetails());
}

对于更复杂的导航,Java Persistence Query Language 还支持路径表达式,可以使用多个单值的关系字段进行导航,比如 td.trade.tid(清单 18)。

清单 18. 逻辑层中使用路径表达式的 Java 代码
Query query = entityManager.createQuery("select td from TradeDetails td where 
 td.trade.tid = " + tid);
tradeDetails = (TradeDetails)query.getSingleResult();

使用 Todo 表单

最初,表单中的所有字段都是只读的。用户可以浏览数据并决定是否希望处理任务。为此,FormController 通过几个方法调用 HTM:

  • claim():声明一个准备好的任务实例以执行用户处理。
  • cancelClaim():取消对一个任务实例的声明。
  • complete():完成一个声明的任务实例。

用户声明一个任务之后,启用字段,用户可以处理业务数据。更新任务列表中的 "State" 列以反映 "Claimed" 状态,参见图 22。

图 22. 声明的 Todo 表单
声明的 Todo 表单

现在,用户还可以在没有完成任务的情况下保存修改。同样,实现方法是把独立的实体作为 DTO 发送给外观 EJB,从而作为托管 JPA 实体持久化它们。按 Complete 按钮通知 HTM 您已经完成了任务。保存相关联的数据并关闭表单,参见清单 19。

清单 19. 在 UI 层中调用 HTM complete()
public void complete()
{
    ...
    ClientObjectWrapper output = lhtm.createOutputMessage(tkiid);
    DataObject sdo = (DataObject) output.getObject();
    sdo.createDataObject(0);
    sdo.setInt("/Output/tid", listEntry.getAsset().getId());
    sdo.setString("/Output/status", "yes"); // asset.getStatus() );
    lhtm.complete(tkiid, output);
    System.out.println("# UI # FormController::complete successful");
    saveAsset();
    closeTab();
}

然后,业务流程经历流程逻辑的后续步骤,可能需要由担任另一个角色的用户执行额外的批准步骤。

图 23 回顾主要步骤(为了简单,删除了一些细节):创建任务列表,打开表单,处理业务数据,然后完成任务。

图 23. 主要步骤的序列图
主要步骤的序列图

搜索业务数据

我们在开始时提到的优点之一是,可以使用数据库功能搜索业务数据。本节提供一个使用数据库搜索功能的示例。单击 Asset >Search Trade 打开搜索面板并单击 Search,参见图 24。

图 24. 搜索业务数据
搜索业务数据

在外观 EJB 中使用搜索条件构建数据库查询,参见清单 20(同样为了可读性做了简化)。

清单 20. 逻辑层中搜索功能的 Java 代码
public Collection<Trade> searchTrade(Trade trade)
{
    Collection<Trade> trades = null;
    String queryText = "select trade from Trade trade where ";
    if (trade.getTid() != 0)
    {
        queryText += "trade.tid = " + trade.getTid();
    }
    if (trade.getName() != null && !trade.getName().equals(""))
    {
        queryText += " and trade.name like '" + trade.getName() + "'";
    }
    Query query = entityManager.createQuery(queryText);
    trades = query.getResultList();
    return (trades);
}

然后,使用记录集合作为新的搜索列表的模型。在界面的 "Lists" 部分中的一个新选项卡中显示它(并隐藏 "Refresh" 按钮)。当从这个列表打开一个交易时,会重用 Todo 表单,此时 "Save" 按钮是可用的,参见图 25。

图 25. 独立于流程处理业务数据
独立于流程处理业务数据

这展示了流程和业务数据隔离的另一个重要优点。


结束语

在本文中,您学习了如何将 WebSphere Process Server 业务流程与其业务数据分隔开。这种方法提供了对相应生命周期的解耦,从而提高性能,并充分利用 Java Persistence API 操纵托管实体和独立的实体。这种方法利用几个 MDD 概念加快开发过程。可以根据共用的 UML 模型生成物理 JPA 类,这使您能够控制解决方案工件并保持其一致性,从而加快整个开发周期。


下载

描述名字大小
示例场景Trading.zip3.4KB

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=WebSphere
ArticleID=755244
ArticleTitle=WebSphere Process Server 中业务流程和业务数据的解耦:MVC 模式的新转折
publish-date=09012011