原文:http://tech.ddvip.com/2012-03/1332301817173960.html 留此备用
Representational State Transfer (REST) 架构风格定义了客户和服务器是如何通信以便将多层系统扩展到一个实际上数量无限的客户端上。Java Persistence API (JPA) 定义了一个服务器端应用程序是如何查询存储在一个关系数据库中的数据的。REST 和 JPA 具有共同的架构特征。本文将介绍和演示 JEST,这是一种将这两种风格整合为一种健壮的、端到端的多层 Web 应用程序架构基础的解决方案。JEST 属于 Apache Software Foundation 的 OpenJPA 项目,是一个 Java Persistence 2.0 规范的实现。
JPA 通常会被误解为仅仅是一种对象关系映射(ORM)解决方案,或者仅仅是 Java Database Connectivity (JDBC) 的替代技术。当然,JPA 也是表示为一种 Java 语言 API。但是,这个 API 底层是一种 “模型-视图-控制器(MVC)” 架构风格的面向对象的事务处理应用程序。
本文先通过介绍 REST 和 JPA 的主要特性来突出强调它们的共同特征,然后详细说明了两种风格之间协作的技术和概念问题,并阐述了 JEST 是如何解决这些问题的。最后一部分是介绍一个具体的实现:一个您可以部署到 Servlet 或应用程序容器的 Java Servlet,然后您可以在一个 Web 浏览器上通过 JEST 的预先包装好的 Dojo/Ajax 客户端访问它。
REST 原理在万维网得到广泛的应用,其主要原因是它优越的可扩展性。我将通过图 1 所示的典型交互场景对 REST 的一些主要概念进行详细介绍:
如果 这是最重要的条件 — 服务器在发送第一个响应之后而它接收下一个请求之前是空闲的(rest),那么客户端与服务器之间通过交换资源表现的交互就是符合 REST 风格的。即,服务器不会消耗任何计算容量或其他资源来保存客户端在两个连接请求之间的会话内存。(同样,服务器中存储会话数据的应用程序也不 符合 REST 风格的。)因为服务器并没有保存请求之间的任何客户端环境,那么服务器响应可以扩展到实际上数量无限的客户端。
在客户端仅仅请求资源时,采用无状态服务器实际上是更现实的。如果客户端通过多个请求发送携带修改表现回服务器,服务器必须自动 将这些请求应用到资源上即,通过一个 ACID(原子性、一致性、隔离性和持久性)事务保证 — 那么采用符合 REST 风格的方法就会遇到一定难度。这是因为 ACID 事务由于其特性决定,需要短期和长期的存储。
ACID 事务:
短期存储保存了当前操作系列(也称为工作单元 或瞬间任务)。长期存储通常是持久化和共享的 — 如硬盘上的数据库 — 则用来使这个组合效果的持久性。
所以设计一个 REST 风格的客户端服务器应用程序的主要问题有:
这些问题的答案可以在一个基于 JPA 且与运行语言无关的 Web 客户端或 Web 服务而不违反核心 REST 原则的服务器应用程序环境中找到。
作为一种架构风格,JPA 符合持久化 Java 对象典型的 “模型-视图-控制器” 架构:
图 2 说明了一个基于 JPA 的应用程序只通过视图(POJO)来访问模型(关系数据库)。所有数据库修改操作,如插入新记录或修改现有记录,都只能够通过控制器 — 例如,EntityManager.persist() 或 merge() — 完成。
转而采用 JPA 实现持久化服务的应用程序可以得到的一个直接好处就是将所谓 ORM 问题 — 由关系模式和 SQL 控制的数据库领域与强类型和单向引用所控制的面向对象 Java 语言领域之间的复杂转换问题 — 交由 JPA 提供者处理。
但是 JPA 的核心目标是成为一个远远不仅限于解决 ORM 问题的中间件组件。本文的大部分相关讨论是针对 JPA 最强大功能之一:支持分离事务(detached transactions)。
JPA 提供者保证在一段时间 T 中由一系列持久化操作构成的 ACID 事务不会在整个 T 时期内都占用一个数据库连接。通过在内存中保存即时事务,并且减少数据库连接占用时间,JPA 使应用程序更具并发用户事务扩展性。(它也可能降低成本,因为一个商业数据库服务器的费用是与 cn 成正比的,其中 c 是数据库所支持的并发连接数,而 n 通常大于 1。)
但是 JPA 中的分离事务并不局限于此。JPA 支持远程客户端的 “访问-分离-修改-合并” 风格的事务。一个客户端可以向服务器请求持久化对象,然后服务器以分离对象图的方式将它们返回。客户端可以修改这个分离对象图的内容或结构,然后在另一个 请求中将修改后的对象图发送回服务器。服务器就可以通过一个保证符合 ACID 事务将修改提交到数据库中。JPA 应用程序在第一个访问请求和后续修改请求之间并没有保持分离对象的引用。
对于典型的用户事务时间间隔可能远远大于实际数据库事务的基于 Web 客户端,或者当大多数事务都是只读时(即,从不会执行提交操作,许多流行网站所谓操作浏览比(buy-to-browse) 为 1:200),这种分离事务方法尤为重要。这种 “访问-分离-修改-合并” 模式使基于 JPA 的应用程序变得可扩展且高效。这样一个应用程序就可以在不丢失任何事务或延缓响应时间的前提下只用 10 个数据库连接就能够支持 5,000 个并发 Web 客户端。
然而,采用这种 “访问-分离-修改-合并” 编程模式的客户端-服务器交互必须满足一定的要求:
相反,JEST 的目标是语言无关的客户端,并且很少有客户端计算环境要求,其最低要求就是一个最简单的 Web 浏览器。因此,JEST 必须生成一个可供语言无关的客户端使用的分离对象表现,从而使 REST 原理与 JPA “访问-分离-修改-合并” 编程框架能够统一在一起。
统一 REST 和 JPA 的主要技术难点是:
在 JEST 环境中,资源 — RESTful 架构的中心概念 — 是一个可识别和管理的实体的一种可定制的持久化闭包,如图 3 所示:
管理根实体 x 的一个持久化闭包被定义为一个管理实体集 C(x) ={e},其中 e 可以直接或间接通过一个持久化关系从 x 访问。此外,从定义上一个实体总是可以从本身访问的。
自定义持久化闭包指的是能够配置哪些实体可以在运行时从一个根实体动态访问。
JPA 规定对象关系是用持久化特性装饰的。例如,Department 及其 Employee 之间的多值关系 — 即,Department 类的一个私有域 List employees — 可以通过 @OneToMany 注释或一个伴随映射描述符装饰为一种一对多关系。在这种情况下,Department d 的所有 Employee 实例都可以从 d 访问到。这种闭包也可以包含间接关联路径。如果每一个 Employee e 具有用 @OneToOne 注释的 Address 实例 a 的引用,那么 Address 实例 a 可以在 Department 实例中通过 Employee e 间接访问,因此 d 闭包也包含了一个 Address 实例 a。
然而,一定要注意持久化闭包只应用于受管理 实体。所以,Department 实例 d 可能逻辑上与 20 个 Employee 实例关联 — 但是这个关联可能并没有加载。即,Employee 可能还未从数据库获得并保存到包含 d 的持久化上下文中。在这种情况中,d 闭包可能不包含 Employee 实例,这些实例也不会作为这个闭包比较的副作用从数据库加载。
当前的 JPA 规范提供了一些基本的自定义闭包工具。任何持久化属性(关联及基本域)都可以注释为 FetchType.LAZY 或 FetchType.EAGER。这种注释是在一个根实体实例从数据库加载到一个持久化上下文时生效的,用来判断有哪些属性或其他实例还需要加载。当前 JPA 规范的主要限制是这种自定义是在设计时静态定义的。我们还无法在运行时进行配置。
OpenJPA 通过它的 FetchPlan 接口支持持久化闭包的动态配置。FetchPlan 的丰富语法和语义允许应用程序为 find() 操作结果的任何根实例或查询得到的实例配置闭包。JEST 利用这些机制访问相关的实例及其属性。查询计划是一个非常有用的结构,因为客户端可以控制相同逻辑资源在每次使用时的请求表现内容。例如,对接收到的同一 个咖啡店信息,客户端可以根据在移动电话或高端桌面显示器上渲染自定义不同的表现方式。
JEST 联合模式的下一个方面是资源表现方式。这个表现是针对与语言和域无关的客户端的。JEST 规定了以下的资源表现特性:
比较 Java API for RESTful Web Services (JAX-RS) 的第一个约束。在 JAX-RS 中,持久化 POJO 必须注释才能够表现。因为持久化 POJO 类是编译单元之一,用户必须重新编译他们的应用程序。而且,这些注释(和 JPA 一样)是要静态地定义为 Fetch.LAZY 或 Fetch.EAGER,并且不能用于为每次使用自定义不同的闭包概要。
语言无关的表现是一组具有隐含格式且按顺序排列的字节或字符。常见的例子是 XML 和 JSON。这种特点的基于字符串的表现适用于广泛的客户端,不要求客户端具有任何特殊功能,只需要它能够从一个字符集中解析字节或字符序列。JEST 生成 XML 和 JSON 表现,其中 JSON 表现可以扩展许多重要的功能。
JEST 表现的核心主题是,它是由模型驱动的,而不是域驱动的。Java 类型的域驱动 XML 表现是很常见的。例如,清单 1 中的简单持久化 Java 类型:
@javax.persistence.Entity public class Actor { @javax.persistence.Id private long id; private String name; private java.util.Date dob; } |
1234 Robert De Niro Aug 15, 1940 |
清单 2 的 XML 表现是域驱动的,因为它的隐含或显式的模式是由持久化域类(Actor)的类型结构管理的。相反,清单 3 显示了相同实例的一个模型驱动表现:
模型驱动表现具有一个域不变模式:它足够普通,可表示任何持久化类型。在模型驱动表现中,Actor 的模式是不变的,而在域驱动表现中,它是会变化的。模型驱动表现的优点是它允许接收者采用与所接收信息真实结构无关的普通方法来解析这个表现。
模型驱动模式 jest-instance.xsd 是从 JPA 2.0的 Metamodel API 直接派生的。Metamodel API 类似于 Java 语言类型的 Java Reflection API,它可以扩展持久化类型以表现更丰富的内容。JEST 能够在一个 XML 模式中表达 JPA Metamodel 结构,如(所谓元数据元)javax.persistence.metamodel.Entity 或 SingularAttribute。所有持久化实体的所有 JEST 响应都符合相同的 jest-instance.xsd 模式。
持久化闭包是一个封闭集合。即,对于 C(x) 闭包中的所有 e1:
如果 e1 引用实体 e2,那么 e2 必须在同一个闭包 C(x) 中。
这个封闭 属性保证一个 JEST 资源是一致的 — 这是 REST 风格表现的一个重要方面。客户端接收到一个表现,其中所有引用都会在同一个表现中解析,而客户端不需要再发出新的服务器请求就可以解析所有引用。
对于 XML 表现,JEST 定义了域无关的 XML 模式进行标识,并且分类作为 xsd:ID 和 xsd:IDREF 类型。因此,一个标准 XML 解析器就可以解析对应 Document Object Model (DOM) 元素的引用。
然而,一个与 JSON 一致的表现会遇到一个技术问题。Web 浏览器环境经常使用的标准 JSON 编码库并不支持循环引用。(这很让人意外,因为循环引用几乎出现在所有的持久化对象图中。)为了解决现有 JSON 编码器的这个限制,而且更重要的是为了支持一致表现的核心前提以防止出现不当的客户端-服务器会话,JEST 提供了一个支持循环引用的 JSON 编码器。
JEST 的 JSON 编码器引入了两个额外属性:$id 和 $ref,它们使用受管理实体的持久化标识来解析对象图可能有的循环。Dojo 或 Gson 中包含的标准 JSON 解析器可以直接忽略额外的 $id 和 $ref 属性来解析这个增强表现。只有 JEST 可以将增强的 JSON 表现重新解析而创建一个具有循环引用的对象图。
JPA 的分离事务模型是满足在事务 Web 应用程序中两次请求之间服务器休息(server-at-rest-between-requests)的 REST 惯例要求的关键。JEST 将这两种架构风格整合在一起,以实现如图 4 所示的事务:
在这个 REST-JPA 统一模式中,当客户端修改 JPA 应用程序的表现时,JPA 并没有在短期内存(持久化上下文)中保存分离的持久化对象。在后一时刻 t2 中,它也不需要保持一个数据库连接来保证事务完整性。服务器在数据库连接和短期内存方面保持了无状态性。服务在短于 t1 到 t2 的间隔里以按需方式占用这些资源,而 t1 到 t2 的间隔是客户端方面的事务总持续时间。
这个统一模式解决了一个无状态服务器如何在既不保存任何客户环境又不牺牲 ACID 事务完整性的前提下支持客户端事务的重要问题。
按照 HTTP 协议规定,一个请求要么是幂等的,要么是非幂等的。在 JEST 环境中,HTTP 请求的一个有用的分类方法是最终的 JPA 操作是否需要使用数据库事务。幂等操作指的是一个为相同输入执行多次均产生相同结果的操作。HTTP 的 GET 操作就是一个例子。请求一个资源(如一个静态 HTML 页面)的多个客户端会得到相同的页面。在 JEST 环境中 — 其中 HTTP 请求实际上会转换成 JPA 操作 — 严格地说没有一个请求是幂等操作。即使是两个连续的 PurchaseOrder 2345 请求也不能保证产生相同的结果,因为在两个请求之间,数据库中 PurchaseOrder 2345 的状态可能会发生变化。HTTP 的 PUT 操作也是一个幂等操作。在 JEST 环境中,具有相同内容的两个连续 PUT 将会产生复制主键异常,这会导致第二个请求失败。
JEST 的最后一个概念是理解它是如何指定一个通信协议和 URI 模式的。REST 从不直接在客户端和服务器之间引用任何具体的通信协议。但是,实际上 REST 与 HTTP 具有紧密的关系(并且已经对 HTTP 发展产生重大影响)。因此,JEST 项目选择 HTTP 作为 JEST 的通信协议。(根据 REST 原理,JEST 的核心概念 — 资源成为受管理实体的持久化闭包而表现是与语言和域无关的 — 是与 HTTP 无关的。)
JEST 是通过解析标准 URI 来确定持久化资源的。标准 URI 定义包括 4 个部分:模式、权限、路径和查询。JEST 会对路径和查询部分进行特殊解析,将它们转换成一个正确的持久化操作。
下面是一些典型的 JEST URI,它们可以说明哪些信息可以编码到一个 URI 中供 JEST 处理。
这个 URI 会获取一个带有主键 m1 的 Actor:
http://openjpa.com:8080/jest/find/plan=basic?type=Actor&m1 |
这个 URI 通过 Java Persistence Query Language (JPQL) 查询获取一个名称为 John 的 Actor:
JEST URI 必须包含足够信息来提供以下的详细信息:
EntityManager 也是从一个 JPQL 查询字符串创建可执行 javax.persistence.Query 实例的工厂。
理想情况下,目标持久化操作应该隐含了一个 HTTP 操作。HTTP 操作 PUT、POST 和 DELETE 可以直接转换到对应的 JPA 操作(分别是 merge()、persist() 和 remove()),如图 5 所示:
然而,HTTP GET 必须在一个查找或查询操作进行多重复用。除了这些操作,HTTP GET 也被用于获取持久化单元的元模型(/domain)和配置属性(/properties)。为了区分 GET 请求的多种资源类型,JEST 要求在 URI 中指定操作名称,并且要求操作必须位于调用上下文的第一段路径。
限定符在路径部分中紧跟操作名。操作可以有 0 个或多个限定符。限定符的顺序是可以任意的。限定符是以 = 字符分隔的键-值对出现的。对于表现逻辑值的限定词,值部分可以忽略以节省字符。例如,表示逻辑值的限定词可以是 single(表示这个查询必须得到惟一一个实体)或 named(表示指定的查询参数引用了声明的 NamedQuery 的名称,而不是 JPQL 字符串)。
JPA 操作的参数是位于 HTTP GET 请求的 URI 查询部分中,或者位于 PUT、POST 和 DELETE 请求的有效内容部分中。而且,GET 请求中 URI 的查询部分包含的参数取决于操作。例如,find() 操作有两个参数:所查找实体的 Java 类及其持久化标识符。
JEST 将 JPA 操作参数编码到一个 GET 请求中 URI 的查询部分中,是因为 JEST URI 可能被用于指定一个 JPQL 查询字符串。而 JPQL 查询字符串可能包含 URI 路径部分不允许的字符。
URI 的查询部分使用和号(&)来分隔每个参数,而每个参数都是以等号(=)分隔的键-值对出现的。与限定词不同,这些参数是按顺序排列的,而且必须按顺序排列。
将一个 URI 转换成一个可执行 JPA 操作的关键方面是将基于字符串的参数转换成各种 JPA 操作所需要的强类型参数。例如,参数 type=Actor 必须转换为 Actor.class。或者 POST 请求的 XML/JSON 有效内容必须在合并到持久化上下文之前转换成一个强类型 Java 对象图。
现在您已经理解了关于 JEST 统一模式的所有基础概念和原理,您现在可以看一个具体的实现:JESTServlet。JESTServlet 是一个标准的 javax.sevlet.HttpServlet,它从一个远程客户端访问 JEST 组件。JESTServlet 可部署到任何标准的 Servlet 容器中,如 Tomcat、WebSphere® 或 Glassfish。
JESTServlet 是使用 OpenJPA 打包的。OpenJAP 网站文档包含了下载、构建和部署说明,所以这里我不准备重复这些步骤。但是,我会说明 JESTServlet 支持的两个部署模式:主模式 和副模式,如图 6 所示:
在主模式中,JESTServlet 本身会实例化一个持久化单元:EntityManagerFactory。这个持久化单元是通过一个标准持久化描述符(META-INF/persistence.xml)描述的,它也会打包到 Web 存档文件中。
在副模式中,JESTServlet 没有自己的持久化单元,但是能够发现一个同级组件 的持久化单元:一个部署制品,如同一个部署模块的另一个 Servlet。在副部署模式中,JESTServlet 需要知道同级组件所使用的持久化单元的名称。目前,同级组件必须激活 OpenJPA 的 EntityManagerFactory 的原生池,这样 JESTServlet 就可以从池中获得同一个单元的句柄。不同的应用程序和 Servlet 容器可能会采用私有机制将持久化单元(或者它们的代理)保存在池中,而容器通常是通过依赖注入将它们注入到运行组件中。在本文撰写 时,JESTServlet 还不能够从这种容器管理的池中查找持久化单元的普遍机制,但是这个项目正在研究增加这样一个功能。
预先包装的示例程序演示了副模式,它使用另一个简单的 Servlet 来实例化一个持久化单元,并将它与 JESTServlet 部署到同一个 Web 存档文件中。这个示例显示了 JESTServlet 是如何在不知道它的同级持久化单元的情况下浏览持久化域或通过它通用的与域无关的功能来表现这些持久化实例。
JEST 本身并没有解决资源表现的渲染问题。例如,它没有提供 HTML 表现。然而,JESTServlet 提供了一个包含 JavaScript 的 HTML 页面。这个页面实际上是一个访问 JEST 工具的 Ajax 客户端。这个客户端使用 Dojo JavaScript 库提交采用 JEST URI 语法 的异步资源请求到 JESTServlet。它会在接收到响应(XML 或 JSON)时渲染这个响应,或者在 Dojo 可视化小部件的形式渲染 — 验证我之前提到的可由标准 JSON 解析器使用的支持循环图的特殊 JSON 编码器输出。图 7 显示了这个页面:
这个交互式网页可以向用户提供所有可用的 JEST 资源:持久化域模型、持久化实体和持久化单元配置。它向用户提供发起请求的 HTML 表单。这些表单通过用户指定的限定词和参数按部就班地显示了 JEST URI 语法是如何形成的。
JESTServlet 在将 HTTP 请求-响应绑定到一个持久化上下文的环境中处理每一个客户端请求。这个环境的生命周期与请求-响应周期的生命周期相同 — 因此符合 REST 在两个请求之间的无状态特性。
JEST 利用了 JPA 架构的丰富功能概念:
JEST 方法是实现持久化资源的无侵害方法,它与其他具有类似目的方面是完全不同的,如 JAX-RS。因为 JEST 完全是由元数据驱动的,所以它是一个可以应用到任何持久化域模型而不需要预先知道模型细节的通用工具。
JEST 可以扩展以表现 OpenJPA 的内部运行时状态 — 例如,查询的运行统计,或者第二层缓存或缓存实体的点击率。访问这类内部信息可能对于创建基于 Web 浏览器的监控控制台是非常有用的。
JEST 的一个公认问题是细粒度的数据安全性。向一个远程客户端提供持久化数据访问可能会引起数据保密和安全问题。这个项目正在研究一个 JEST 响应的内容可以如何基于请求客户端的证书进行验证或细粒度数据层的控制。既然 JEST 运行环境知道一个持久化上下文和可执行查询的表达树的存在,基于客户端角色对查询表达式树节点进行访问规则判断就是一种可行的方法。(目前的 JESTServlet 已经通过重写部署的 JavaScript 客户端的基本 URL 防止出现跨浏览器脚本处理。