JEST:用 OpenJPA 实现 REST

统一多层 Web 应用程序的 REST 和 JPA 的架构风格

JEST 结合了两种体架构风格 Representational State Transfer (REST) 和 Java Persistence API (JPA),使语言无关的远程客户端能够根据 REST 方法与基于 JPA 的应用程序进行交互。JEST 模型将可定制的管理实体持久化闭包建模成 REST 资源,并用 XML 或增强的 JavaScript Notation (JSON) 格式表现。本文将介绍 JEST 的概念。然后再介绍一个演示实现:一个普通 Web 客户端,它可以与服务器通信来查询持久化对象并以未知域(domain-agnostic)的方式浏览一个持久化域模型。

编缉注语:由于本文涉及了很多最新的技术术语和概念,首先需要您对相关的技术,如 JPA、ORM、持久化、REST 等概念有一定的了解,同时,对于文章中的一些专业术语和名称的翻译,业界并无统一标准,如果在阅读过程中对于某些词语的翻译有疑议,可以将意见通过“添加评论”提交给我们,同时可以参考英文原文的内容。

Pinaki Poddar, 高级软件工程师, IBM

Pinaki Poddar 的照片Pinaki Poddar 研究中间件技术,关注的焦点是对象持久化。他是 Java Persistence API (JSR 317) 规范的专家组成员,并且是 Apache OpenJPA 项目的参与者。他曾经为一家全球性投资银行构建面向组件的集成中间件,并为医疗行业构建医学图像处理平台。他在自己的博士论文中开发了一个基于神经网络的自动语音识别系统。



2011 年 5 月 23 日

Representational State Transfer (REST) 架构风格定义了客户和服务器是如何通信以便将多层系统扩展到一个实际上数量无限的客户端上。Java Persistence API (JPA) 定义了一个服务器端应用程序是如何查询存储在一个关系数据库中的数据的。REST 和 JPA 具有共同的架构特征。本文将介绍和演示 JEST,这是一种将这两种风格整合为一种健壮的、端到端的多层 Web 应用程序架构基础的解决方案。JEST 属于 Apache Software Foundation 的 OpenJPA 项目,是一个 Java Persistence 2.0 规范的实现(见 参考资料)。

JPA:不仅仅是一个 API

JPA 通常会被误解为仅仅是一种对象关系映射(ORM)解决方案,或者仅仅是 Java Database Connectivity (JDBC) 的替代技术。当然,JPA 也是表示为一种 Java 语言 API。但是,这个 API 底层是一种 “模型-视图-控制器(MVC)” 架构风格的面向对象的事务处理应用程序。

本文先通过介绍 REST 和 JPA 的主要特性来突出强调它们的共同特征,然后详细说明了两种风格之间协作的技术和概念问题,并阐述了 JEST 是如何解决这些问题的。最后一部分是介绍一个具体的实现:一个您可以部署到 Servlet 或应用程序容器的 Java Servlet,然后您可以在一个 Web 浏览器上通过 JEST 的预先包装好的 Dojo/Ajax 客户端访问它。

REST 原理

REST 原理在万维网得到广泛的应用,其主要原因是它优越的可扩展性(见 参考资料)。我将通过图 1 所示的典型交互场景对 REST 的一些主要概念进行详细介绍:

图 1. REST 风格交互示例
图片显示客户端和服务器端之间的 REST 风格交互
  • 客户端和服务器之间的交互完全基于双向传输的资源。这是与基于特定语言 API 的交互完全不同的,在基于 API 的交互中,客户端是通过传递特定编程语言的可变参数到远程代理来调用服务器函数的。
  • 所谓资源 是一种可识别的内聚(coherent)概念。可识别的 表示每一个资源都可以通过明确定义的语法进行惟一识别。这种身份的最常用表现方式是 Uniform Resource Identifier (URI),例如一个网址。
  • 客户端请求一个服务器资源(例如,Google Maps) — 一个抽象符号,如附近一个咖啡店的街道地址。服务器会通过发送一个资源表现 响应这个请求 — 例如,带有 JavaScript 和咖啡店地理位置地图的超链接的一个 HTML 文档。
  • 如果一个表现包含了足够的信息来解析表现中的所有引用,那么这个表现就是内聚的
  • 一收到这个表现,客户端通常就会将它渲染到浏览器上。其中值得注意的方面有:
    • 一个资源的抽象符号会被实现为一个具体表现。
    • 服务能够基于许多条件 — 例如,用户的自然语言 — 为同一个资源响应不同的表现。这个请求本身规定了一种特殊的可接受表单,如通过一个典型 HTTP 请求的 Accept 头部请求的 MIME 类型。
    • 一个客户端有可能能够解析表现中的所有引用,也有可能无法解析。例如,一个用户可能决定使用或者不使用这个超链接来显示咖啡店地址附近的地理位置地图,这取决于界面上的“建筑物”。
  • 除了渲染,客户端也允许用户修改表现(representation)或者创建新的表现 — 例如,通过一个 HTML 表单渲染。在用户交互完成之后,客户端就会发送一个携带修改表现(modified representation)的新请求。
  • 服务器接收到该修改表现方式后,会基于它的内容修改原始资源或者创建新的资源。

如果 这是最重要的条件 — 服务器在发送第一个响应之后而它接收下一个请求之前是空闲的(rest),那么客户端与服务器之间通过交换资源表现的交互就是符合 REST 风格的。即,服务器不会消耗任何计算容量或其他资源来保存客户端在两个连接请求之间的会话内存。(同样,服务器中存储会话数据的应用程序也 符合 REST 风格的。)因为服务器并没有保存请求之间的任何客户端环境,那么服务器响应可以扩展到实际上数量无限的客户端。

ACID 事务和 REST

在客户端仅仅请求资源时,采用无状态服务器实际上是更现实的。如果客户端通过多个请求发送携带修改表现回服务器,服务器必须自动 将这些请求应用到资源上即,通过一个 ACID(原子性、一致性、隔离性和持久性)事务保证 — 那么采用符合 REST 风格的方法就会遇到一定难度。这是因为 ACID 事务由于其特性决定,需要短期和长期的存储。

ACID 事务:

  • 将一段时间内的多个客户端操作分组到一个原子(不可分割)单元。
  • 保证这些操作组合在一起的效果是一致的。
  • 保证组合在一起的效果与其他客户端同时发生的所有操作相隔离。
  • 保证这个组合效果永远有效。

短期存储保存了当前操作系列(也称为工作单元瞬间任务)。长期存储通常是持久化和共享的 — 如硬盘上的数据库 — 则用来使这个组合效果的持久性。

所以设计一个 REST 风格的客户端服务器应用程序的主要问题有:

  • 一个服务器所提供的可识别资源是什么?
  • 这个服务器产生的资源表现是什么?
  • 一个无状态服务器是如何支持 ACID 事务的?

这些问题的答案可以在一个基于 JPA 且与运行语言无关的 Web 客户端或 Web 服务而不违反核心 REST 原则的服务器应用程序环境中找到。


JPA:支持对象持久化的 “模型-视图-控制器”

作为一种架构风格,JPA 符合持久化 Java 对象典型的 “模型-视图-控制器” 架构:

  • 模型是一个关系数据库。
  • 视图是严格编写且不带任何强加 接口或超类(如之前的 Enterprise JavaBeans 规范)的 Plain Old Java Object (POJO)。
  • 控制器是这个 API 本身 — 即 javax.persistence.EntityManager

图 2 所示是 JPA 的对象持久化 MVC 架构:

图 2. 作为对象持久化 MVC 的 JPA
图片说明了 JPA 的对象持久化 MVC 架构

图 2 说明了一个基于 JPA 的应用程序只通过视图(POJO)来访问模型(关系数据库)。所有数据库修改操作,如插入新记录或修改现有记录,都只能够通过控制器 — 例如,EntityManager.persist()merge()— 完成。

转而采用 JPA 实现持久化服务的应用程序可以得到的一个直接好处就是将所谓 ORM 问题 — 由关系模式和 SQL 控制的数据库领域与强类型和单向引用所控制的面向对象 Java 语言领域之间的复杂转换问题 — 交由 JPA 提供者处理。

为什么 JPA >> ORM + JDBC

但是 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 客户端。

然而,采用这种 “访问-分离-修改-合并” 编程模式的客户端-服务器交互必须满足一定的要求:

  • 客户端必须使用 Java 语言编写,通过一个远程会话 Bean 或类似的方法来调用 JPA。
  • 服务器将以服务器端序列化字节的强类型 Java 对象发送分离对象图。在解析这些字节时,客户端需要有这个持久化 Java 类型的类定义。
  • 实际上,大多数 JPA 供应商都会给用户定义的 POJO 加入他们的实现类依赖(如标准 Java 集合类型的代理)来对它们进行更有效的管理。这使得客户端还必须使用供应商运行时程序库。

相反,JEST 的目标是语言无关的客户端,并且很少有客户端计算环境要求,其最低要求就是一个最简单的 Web 浏览器。因此,JEST 必须生成一个可供语言无关的客户端使用的分离对象表现,从而使 REST 原理与 JPA “访问-分离-修改-合并” 编程框架能够统一在一起。


JEST 是如何统一 REST 和 JPA 的

统一 REST 和 JPA 的主要技术难点是:

  • 如何将分离的持久化对象表现为语言无关客户端可使用的资源
  • 如何在不牺牲 RESTful 服务器的无状态特性的前提下支持 ACID 事务

JEST 资源:可定制的持久化闭包

在 JEST 环境中,资源 — RESTful 架构的中心概念 — 是一个可识别和管理的实体的一种可定制的持久化闭包,如图 3 所示:

图 3. 可定制的持久化闭包
图片说明了可定制的持久化闭包的概念

管理根实体 x 的一个持久化闭包被定义为一个管理实体集 C(x) ={e},其中 e 可以直接或间接通过一个持久化关系从 x 访问。此外,从定义上一个实体总是可以从本身访问的。

自定义持久化闭包指的是能够配置哪些实体可以在运行时从一个根实体动态访问。

JPA 规定对象关系是用持久化特性装饰的。例如,Department 及其 Employee 之间的多值关系 — 即,Department 类的一个私有域 List<Employee> 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.LAZYFetchType.EAGER。这种注释是在一个根实体实例从数据库加载到一个持久化上下文时生效的,用来判断有哪些属性或其他实例还需要加载。当前 JPA 规范的主要限制是这种自定义是在设计时静态定义的。我们还无法在运行时进行配置。

OpenJPA 通过它的 FetchPlan 接口(见 参考资料)支持持久化闭包的动态配置。FetchPlan 的丰富语法和语义允许应用程序为 find() 操作结果的任何根实例或查询得到的实例配置闭包。JEST 利用这些机制访问相关的实例及其属性。查询计划是一个非常有用的结构,因为客户端可以控制相同逻辑资源在每次使用时的请求表现内容。例如,对接收到的同一个咖啡店信息,客户端可以根据在移动电话或高端桌面显示器上渲染自定义不同的表现方式。

一致的 JEST 表现:XML 和 JSON

JEST 联合模式的下一个方面是资源表现方式。这个表现是针对与语言和域无关的客户端的。JEST 规定了以下的资源表现特性:

JEST 与 JAX-RS

比较 Java API for RESTful Web Services (JAX-RS) 的第一个约束。在 JAX-RS 中,持久化 POJO 必须注释才能够表现。因为持久化 POJO 类是编译单元之一,用户必须重新编译他们的应用程序。而且,这些注释(和 JPA 一样)是要静态地定义为 Fetch.LAZYFetch.EAGER,并且不能用于为每次使用自定义不同的闭包概要。

  • 非侵害的(Noninvasive):资源不要求进行任何类型的注释或装饰就可以表现(见侧栏的 JEST 与 JAX-RS)。
  • 语言无关的:客户可以使用任何编程语言使用这个表现。因为交互是基于一个 URI 而不是 API 的,所以用任何语言编写的客户端都可以通过 HTTP 请求接收一个表现。这个表现还采用常见的格式,如 XML 或 JSON,使任何客户端都能够解析它的内容。所有客户都不需要安装插件就能够解析 JEST 生成的表现。
  • 与域无关:即使原始资源是一种强类型持久化 Java 对象,客户端也不需要持久化类定义就能够使用 JEST 生成的表现。在 REST 的概念里,这个表现是自我描述的

域驱动与域不变表现

语言无关的表现是一组具有隐含格式且按顺序排列的字节或字符。常见的例子是 XML 和 JSON。这种特点的基于字符串的表现适用于广泛的客户端,不要求客户端具有任何特殊功能,只需要它能够从一个字符集中解析字节或字符序列。JEST 生成 XML 和 JSON 表现,其中 JSON 表现可以扩展许多重要的功能。

JEST 表现的核心主题是,它是由模型驱动的,而不是域驱动的。Java 类型的域驱动 XML 表现是很常见的。例如,清单 1 中的简单持久化 Java 类型:

清单 1. 简单的持久化 Java 类型
@javax.persistence.Entity
public class Actor {
  @javax.persistence.Id
  private long id;
  private String name;
  private java.util.Date dob;
}

清单 2 显示了一个 Actor 实例的相应 XML 表现:

清单 2. Actor 实例的域驱动 XML 模式表现
<?xml version="1.0" encoding="UTF-8"?>
<Actor>
  <id type="long">1234</id>
  <name type="string">Robert De Niro</name>
  <dob type="date">Aug 15, 1940</dob>
</Actor>

清单 2 的 XML 表现是域驱动的,因为它的隐含或显式的模式是由持久化域类(Actor)的类型结构管理的。相反,清单 3 显示了相同实例的一个模型驱动表现:

清单 3. 以模型驱动 XML 模式表现的简单实体
<?xml version="1.0" encoding="UTF-8"?>
<instances>
  <instance type="Actor" id="Actor-1234">
    <id name="id" type="long">1234</id>
    <basic name="name" type="string">Robert De Niro</name>
    <basic name="dob" type="date">Aug 15, 1940</dob>
  </instance>
</instances>

模型驱动表现具有一个域不变模式:它足够普通,可表示任何持久化类型。在模型驱动表现中,Actor 的模式是不变的,而在域驱动表现中,它是会变化的。模型驱动表现的优点是它允许接收者采用与所接收信息真实结构无关的普通方法来解析这个表现。

模型驱动模式 jest-instance.xsd 是从 JPA 2.0(见 参考资料)的 Metamodel API 直接派生的。Metamodel API 类似于 Java 语言类型的 Java Reflection API,它可以扩展持久化类型以表现更丰富的内容。JEST 能够在一个 XML 模式中表达 JPA Metamodel 结构,如(所谓元数据元)javax.persistence.metamodel.EntitySingularAttribute。所有持久化实体的所有 JEST 响应都符合相同的 jest-instance.xsd 模式。

通过持久化闭包实现一致表现

持久化闭包是一个封闭集合。即,对于 C(x) 闭包中的所有 e1

如果 e1 引用实体 e2,那么 e2 必须在同一个闭包 C(x) 中。

这个封闭 属性保证一个 JEST 资源是一致的 — 这是 REST 风格表现的一个重要方面。客户端接收到一个表现,其中所有引用都会在同一个表现中解析,而客户端不需要再发出新的服务器请求就可以解析所有引用。

为什么 JEST 要扩展 JSON?

对于 XML 表现,JEST 定义了域无关的 XML 模式进行标识,并且分类作为 xsd:IDxsd:IDREF 类型。因此,一个标准 XML 解析器就可以解析对应 Document Object Model (DOM) 元素的引用。

然而,一个与 JSON 一致的表现会遇到一个技术问题。Web 浏览器环境经常使用的标准 JSON 编码库并不支持循环引用。(见 参考资料)。(这很让人意外,因为循环引用几乎出现在所有的持久化对象图中。)为了解决现有 JSON 编码器的这个限制,而且更重要的是为了支持一致表现的核心前提以防止出现不当的客户端-服务器会话,JEST 提供了一个支持循环引用的 JSON 编码器。

JEST 的 JSON 编码器引入了两个额外属性:$id$ref,它们使用受管理实体的持久化标识来解析对象图可能有的循环。Dojo 或 Gson 中包含的标准 JSON 解析器可以直接忽略额外的 $id$ref 属性来解析这个增强表现。只有 JEST 可以将增强的 JSON 表现重新解析而创建一个具有循环引用的对象图。

分离事务:使 REST 与 JPA 整合统一的关键

JPA 的分离事务模型是满足在事务 Web 应用程序中两次请求之间服务器休息(server-at-rest-between-requests)的 REST 惯例要求的关键。JEST 将这两种架构风格整合在一起,以实现如图 4 所示的事务:

图 4. 整合 REST 和 JPA 的分离事务
图片演示了 JEST 的分离事务序列
  1. t1 时刻,客户端的资源请求 R1 到达服务器。
  2. 服务器使用 JPA 以 Java 对象的方式访问数据库的资源。这个访问要求建立一个数据库连接。JPA 提供者会在访问完成之后马上释放这个数据库连接。
  3. 这个提供者一般会在一个 JPA 称之为持久化上下文 的短期内存区域中保存这些对象。然而,在这里提供者会立即将这些对象从持久化上下文中分离,然后将它们转换成客户端可解析的表现 A1,然后将 A1 作为响应发送给原始请求 R1。立即分离能够保证这个服务器应用程序在响应发送后不会占用任何资源。
  4. 客户端渲染 A1 并让用户将表现修改为 A1'
  5. 客户端可能会再发送请求 R2R3 以访问更多的资源,并分别接收到响应 A2A3
  6. 客户端可能还会让用户修改 A2A3
  7. 当用户完成对接收到的所有响应修改之后,客户端会在 t2 立刻发送一个包含所有修改表现 A1'A2'A3' 的请求 R4
  8. 服务器会将接收到的修改表现转换成 Java 对象,然后使用 JPA 将它们合并到一个持久化上下文中。这时,JPA 提供者可能需要建立数据库连接来验证修改是否一致 — 例如,保证在 t1t2 期间没有其他客户端提交对 A1'A2'A3' 所包含的相同对象的不相容修改。最后,如果修改是一致且独立的,那么服务器就会通过 JPA API 将修改提交到数据库中。

在这个 REST-JPA 统一模式中,当客户端修改 JPA 应用程序的表现时,JPA 并没有在短期内存(持久化上下文)中保存分离的持久化对象。在后一时刻 t2 中,它也不需要保持一个数据库连接来保证事务完整性。服务器在数据库连接和短期内存方面保持了无状态性。服务在短于 t1t2 的间隔里以按需方式占用这些资源,而 t1t2 的间隔是客户端方面的事务总持续时间。

这个统一模式解决了一个无状态服务器如何在既不保存任何客户环境又不牺牲 ACID 事务完整性的前提下支持客户端事务的重要问题。


采用 HTTP 的 JEST

幂等请求与事务请求

按照 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 语法

JEST 是通过解析标准 URI 来确定持久化资源的。标准 URI 定义包括 4 个部分:模式、权限、路径和查询(见 参考资料)。JEST 会对路径和查询部分进行特殊解析,将它们转换成一个正确的持久化操作。

下面是一些典型的 JEST URI,它们可以说明哪些信息可以编码到一个 URI 中供 JEST 处理。

这个 URI 会获取一个带有主键 m1Actor

http://openjpa.com:8080/jest/find/plan=basic?type=Actor&m1

这个 URI 通过 Java Persistence Query Language (JPQL) 查询获取一个名称为 JohnActor

http://openjpa.com:8080/jest/query/single?q=select p from Actor p where p.name=:n&n=John

JEST URI 必须包含足够信息来提供以下的详细信息:

  • JPA 操作:JEST 环境中的持久化操作大部分是 javax.persistence.EntityManager 接口的方法:
    • find():通过实体的主标识符查找实体。
    • persist():在数据库中创建一个指定的新实体。
    • merge():修改当前持久化环境的指定实体的当前状态。
    • remove():从数据库删除指定的实体。

    EntityManager 也是从一个 JPQL 查询字符串创建可执行 javax.persistence.Query 实例的工厂。

    理想情况下,目标持久化操作应该隐含了一个 HTTP 操作。HTTP 操作 PUTPOSTDELETE 可以直接转换到对应的 JPA 操作(分别是 merge()persist()remove()),如图 5 所示:

    图 5. 将 HTTP 操作映射到 JPA 持久化操作
    图片说明 HTTP 操作是如何映射到 JPA 持久化操作的

    然而,HTTP GET 必须在一个查找或查询操作进行多重复用。除了这些操作,HTTP GET 也被用于获取持久化单元的元模型(/domain)和配置属性(/properties)。为了区分 GET 请求的多种资源类型,JEST 要求在 URI 中指定操作名称,并且要求操作必须位于调用上下文的第一段路径。

  • 限定符:持久化操作可以进一步限定或修饰。例如,查询可以限定只返回一个且只有一个结果,或者只返回前 20 个查询结果。查找或查询操作还可以通过修饰后应用一个查询计划来自定义从数据库查询得到的实体集。

    限定符在路径部分中紧跟操作名。操作可以有 0 个或多个限定符。限定符的顺序是可以任意的。限定符是以 = 字符分隔的键-值对出现的。对于表现逻辑值的限定词,值部分可以忽略以节省字符。例如,表示逻辑值的限定词可以是 single(表示这个查询必须得到惟一一个实体)或 named(表示指定的查询参数引用了声明的 NamedQuery 的名称,而不是 JPQL 字符串)。

  • Arguments:每一个操作都需要有参数。例如,find() 操作的参数是实体类型及其主标识符。查询操作的参数是 JPQL 字符串或预定义名称查询的别名。一个带参数的查询也可以将一组数量不定的参数作为它的绑定参数。

    JPA 操作的参数是位于 HTTP GET 请求的 URI 查询部分中,或者位于 PUTPOSTDELETE 请求的有效内容部分中。而且,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 实践

现在您已经理解了关于 JEST 统一模式的所有基础概念和原理,您现在可以看一个具体的实现:JESTServletJESTServlet 是一个标准的 javax.sevlet.HttpServlet,它从一个远程客户端访问 JEST 组件。JESTServlet 可部署到任何标准的 Servlet 容器中,如 Tomcat、WebSphere® 或 Glassfish。

部署 JEST

JESTServlet 是使用 OpenJPA 打包的。OpenJAP 网站文档(见 参考资料)包含了下载、构建和部署说明,所以这里我不准备重复这些步骤。但是,我会说明 JESTServlet 支持的两个部署模式:主模式副模式,如图 6 所示:

图 6. JESTServlet 部署的主模式和副模式
图片显示了 JESTServlet 部署的主模式和副模式

在主模式中,JESTServlet 本身会实例化一个持久化单元:EntityManagerFactory。这个持久化单元是通过一个标准持久化描述符(META-INF/persistence.xml)描述的,它也会打包到 Web 存档文件中。

在副模式中,JESTServlet 没有自己的持久化单元,但是能够发现一个同级组件 的持久化单元:一个部署制品,如同一个部署模块的另一个 Servlet。在副部署模式中,JESTServlet 需要知道同级组件所使用的持久化单元的名称。目前,同级组件必须激活 OpenJPA 的 EntityManagerFactory 的原生池,这样 JESTServlet 就可以从池中获得同一个单元的句柄。不同的应用程序和 Servlet 容器可能会采用私有机制将持久化单元(或者它们的代理)保存在池中,而容器通常是通过依赖注入将它们注入到运行组件中。在本文撰写时,JESTServlet 还不能够从这种容器管理的池中查找持久化单元的普遍机制,但是这个项目正在研究增加这样一个功能。

预先包装的示例程序演示了副模式,它使用另一个简单的 Servlet 来实例化一个持久化单元,并将它与 JESTServlet 部署到同一个 Web 存档文件中。这个示例显示了 JESTServlet 是如何在不知道它的同级持久化单元的情况下浏览持久化域或通过它通用的与域无关的功能来表现这些持久化实例。

JEST 的一个Dojo 客户端

JEST 本身并没有解决资源表现的渲染问题。例如,它没有提供 HTML 表现。然而,JESTServlet 提供了一个包含 JavaScript 的 HTML 页面。这个页面实际上是一个访问 JEST 工具的 Ajax 客户端。这个客户端使用 Dojo JavaScript 库提交采用 JEST URI 语法 的异步资源请求到 JESTServlet。它会在接收到响应(XML 或 JSON)时渲染这个响应,或者在 Dojo 可视化小部件的形式渲染 — 验证我之前提到的可由标准 JSON 解析器使用的支持循环图的特殊 JSON 编码器输出。图 7 显示了这个页面:

图 7. 用于访问 JEST 的基于 Dojo 的用户界面
截图显示了 Web 浏览器上用于访问 JEST 的基于 Dojo 的用户界面

单击此处查看 大图

这个交互式网页可以向用户提供所有可用的 JEST 资源:持久化域模型、持久化实体和持久化单元配置。它向用户提供发起请求的 HTML 表单。这些表单通过用户指定的限定词和参数按部就班地显示了 JEST URI 语法是如何形成的。

JESTServlet 在将 HTTP 请求-响应绑定到一个持久化上下文的环境中处理每一个客户端请求。这个环境的生命周期与请求-响应周期的生命周期相同 — 因此符合 REST 在两个请求之间的无状态特性。


结束语

JEST 利用了 JPA 架构的丰富功能概念:

  • 一个强大的查询语言
  • 自定义的闭包
  • 通用域模型
  • 提供富内容和为远程语言无关客户端动态配置表现的分离事务,以完全符合 REST 架构原理

JEST 方法是实现持久化资源的无侵害方法,它与其他具有类似目的方面是完全不同的,如 JAX-RS。因为 JEST 完全是由元数据驱动的,所以它是一个可以应用到任何持久化域模型而不需要预先知道模型细节的通用工具。

JEST 可以扩展以表现 OpenJPA 的内部运行时状态 — 例如,查询的运行统计,或者第二层缓存或缓存实体的点击率。访问这类内部信息可能对于创建基于 Web 浏览器的监控控制台是非常有用的。

JEST 的一个公认问题是细粒度的数据安全性。向一个远程客户端提供持久化数据访问可能会引起数据保密和安全问题。这个项目正在研究一个 JEST 响应的内容可以如何基于请求客户端的证书进行验证或细粒度数据层的控制。既然 JEST 运行环境知道一个持久化上下文和可执行查询的表达树的存在,基于客户端角色对查询表达式树节点进行访问规则判断就是一种可行的方法。(目前的 JESTServlet 已经通过重写部署的 JavaScript 客户端的基本 URL 防止出现跨浏览器脚本处理。)

参考资料

学习

获得产品和技术

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

条评论

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=Java technology
ArticleID=660136
ArticleTitle=JEST:用 OpenJPA 实现 REST
publish-date=05232011