JAX-RS2-REST-服务开发指南-全-

JAX-RS2 REST 服务开发指南(全)

原文:zh.annas-archive.org/md5/988d8353d798fb073449bc7e1d9af84d

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

几年来,我们见证了从主机到 x86 农场、从重型方法到轻量级敏捷方法、从桌面和厚客户端到瘦、丰富和高度可用的 Web 应用程序以及无处不在的计算的几次革命和范式转变。

随着技术领域向更小、更便携、更轻便的设备发展,以及这些设备在日常活动中被广泛使用,将计算从客户端机器推送到后端的需求变得更加突出。这也带来了在服务器和客户端之间开发具有近似实时或实时事件和数据传播的应用程序的机会和挑战;这正是 HTML 5 为开发者提供标准、API 和灵活性,以在 Web 应用程序中实现与厚客户端应用程序相同结果的地方。

客户端与服务器之间的通信在数量、内容、互操作性和可扩展性等方面已成为最基本的问题。XML 时代、长时间等待请求、单一浏览器和单一设备兼容性已经结束;取而代之的是设备、多个客户端和浏览器的时代,从小型设备(仅能通过 HTTP 处理文本)到巨型规模机器(几乎可以处理任何类型的内容)都有。因此,生成内容、接受内容以及能够在旧协议和新协议之间切换的能力已成为显而易见的必需。

Java EE 7 更加注重这些新兴(和主导)需求;支持 HTML5、更多异步通信/调用能力组件,以及支持 JSON 作为数据格式之一,这些都有助于开发者解决技术需求,并为开发者提供充足的时间来处理系统业务需求的实现。

本书试图为热衷于技术的读者提供一个概述,介绍 Java EE 在一般意义上以及 Java EE 7 在特定意义上作为技术平台,为开发基于 HTML5 的轻量级、交互式应用程序,并可在任何 Java EE 兼容容器中部署提供支持。

本书涵盖的内容

第一章, 使用 JAX-RS 构建 RESTful Web 服务,从构建 RESTful Web 服务的基本概念开始,涵盖了 JAX-RS 2.0 API,详细介绍了不同的注解、提供者、MessageBodyReaderMessageBodyWriter、客户端 API 和 JAX-RS 2.0 中的 Bean Validation 支持。

第二章,WebSockets 和服务器端事件,讨论了向客户端发送近实时更新的不同编程模型。它还涵盖了 WebSockets 和服务器端事件,即 WebSockets 和服务器端事件的 JavaScript 和 Java API。本章比较和对比了 WebSockets 和服务器端事件,并展示了 WebSockets 在减少不必要的网络流量和提高性能方面的优势。

第三章,详细理解 WebSockets 和服务器端事件,涵盖了 Java EE 7 API 用于 WebSockets、编码器和解码器、客户端 API,以及如何使用 blob 和ArrayBuffers通过 WebSockets 发送不同类型的消息。它教授如何确保基于 WebSockets 的应用程序的安全性。它概述了基于 WebSockets 和服务器端事件的最佳实践。

第四章,JSON 和异步处理,涵盖了 Java EE 7 JSON-P API 用于解析和操作 JSON 数据。它还讨论了 Servlet 3.1 规范中引入的新 NIO API。它教授如何使用 JAX-RS 2.0 API 进行异步请求处理以提高可伸缩性。

第五章,通过示例学习 RESTful Web 服务,涵盖了两个实际的 RESTful Web 服务示例。它涵盖了一个基于 Twitter 搜索 API 的事件通知示例,服务器如何根据事件发生的情况将数据推送到客户端。一个库应用程序将上述章节中涵盖的不同技术联系起来。

您需要为这本书准备什么

要能够构建和运行本书提供的示例,您将需要:

  1. Apache Maven 3.0 及以上版本。Maven 用于构建示例。您可以从maven.apache.org/download.cgi下载 Apache Maven。

  2. GlassFish Server Open Source Edition v4.0 是免费的、社区支持的提供 Java EE 7 规范实现的 Application Server。您可以从dlc.sun.com.edgesuite.net/glassfish/4.0/promoted/下载 GlassFish Server。

这本书适合谁阅读

这本书是针对熟悉 Java EE 并渴望了解 Java EE 7 中引入的新 HTML5 相关功能以提高生产力的应用开发者理想的阅读材料。为了充分利用这本书,您需要熟悉 Java EE 并具备一些使用 GlassFish 应用服务器的基本知识。

习惯用法

在这本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“发送给 JAX-RS 资源的请求是一个以app/library/book/为目标 URI 的POST请求。”

代码块如下所示:

@GET
@Path("browse")
public List<Book> browseCollection() {
  return bookService.getBooks();    }

当我们希望将您的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:

@GET
@Path("browse")
public List<Book> browseCollection() {
  return bookService.getBooks();    }

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“当用户点击 HTML 页面上的保持按钮时”。

注意

警告或重要注意事项如下所示。

提示

技巧和窍门如下所示。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者的反馈对我们开发您真正能从中受益的标题非常重要。

要向我们发送一般反馈,只需发送电子邮件到<feedback@packtpub.com>,并在邮件主题中提及书籍标题。

如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在,您已经成为 Packt 书籍的骄傲所有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您在www.packtpub.com的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

勘误

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata来报告它们,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过选择您的标题从www.packtpub.com/support查看任何现有勘误。

盗版

在互联网上侵犯版权材料是一个跨所有媒体持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果你在网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。

我们感谢你在保护我们作者和提供有价值内容的能力方面的帮助。

问题

如果你在本书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。

第一章:使用 JAX-RS 构建 RESTful Web 服务

实现异构应用程序之间的通信有多种方式。有基于 SOAPWSDL 和 WS* 规范的标准化解决方案;与此同时,还有一个基于纯 HTTP 的轻量级解决方案,被称为 表征状态转移REST)。

REST 通过可寻址资源、使用 HTTP 动词约束的接口、表示和无状态性等原则来识别。

REST 的关键原则是:

  • 将 ID 关联到资源

  • 使用标准 HTTP 方法

  • 资源发送的数据的多种格式

  • 无状态性

本章从使用 JAX-RS 2.0 API 构建 RESTful Web 服务 的基本概念开始,并涵盖以下部分:

  • 开始使用 JAX-RS 2.0

  • 使用 JAX-RS 2.0 注解将 POJO 转换为 RESTful 端点

  • @Produces@Consumes 注解

  • JAX-RS 2.0 的客户端 API

  • 展示所有动词的示例

  • 用于 序列化反序列化 用户定义类的自定义实体提供者使用 JAX-RS

  • 利用 Bean Validation API 对 JAX-RS 2.0 进行验证

理解 REST

REST 架构风格基于客户端和服务器之间传输的请求和响应消息,参与节点中没有任何节点跟踪先前会话的状态。

REST 使用名词和动词以提高可读性。资源在请求中标识。发送给客户端的资源表示取决于请求和服务器发送数据的方式。

RESTful Web 服务

RESTful Web 服务是一种接口和访问机制与 REST 原则一致的服务。URI 识别资源。例如,一个书籍的 RESTful 资源可以标识为 foo.org/book

一个通过 ISBN 标识的书籍资源可以是 foo.org/book/isbn/1234459。这显示了易于理解和识别的易读 URI。

客户端拥有足够资源的元数据来修改或删除它,只要它被授权这样做。为了获取资源,客户端会发送一个 HTTP GET 请求。为了更新资源,客户端会发送一个 PUT 请求。为了删除资源,客户端会发送一个 DELETE 请求。为了创建新的资源,以及进行任意处理,客户端会发送一个 HTTP POST 请求。下一节将更详细地介绍这些动词。

REST 中的动词

REST 中使用的一些请求如下:

  • GETGET 请求从服务器到客户端检索资源的表示

  • POSTPOST 请求用于根据客户端发送的表示在服务器上创建资源

  • PUTPUT 请求用于在服务器上更新或创建对资源的引用

  • DELETEDELETE 请求可以删除服务器上的资源

  • HEADHEAD 请求检查资源而不检索它

下一个部分将介绍安全性和幂等性的概念,这两个术语与 REST 紧密相关。

安全性和幂等性

当涉及到 REST 时,根据定义,一个安全的方法是一个不修改服务器上资源状态的 HTTP 方法。例如,在资源 URL 上调用 GETHEAD 方法不应改变服务器上的资源。PUT 被认为是不可安全的,因为它通常会在服务器上创建资源。DELETE 也被认为是不可安全的,因为它会删除服务器上的资源。POST 也不安全,因为它会改变服务器上的资源。

幂等方法是可以多次调用而结果不会改变的方法。

GETHEAD 是幂等的,这意味着即使相同的操作执行多次,结果也不会变化。PUT 是幂等的;多次调用 PUT 方法不会改变结果,资源状态保持完全相同。

DELETE 是幂等的,因为一旦资源被删除,它就消失了,多次调用相同的操作不会改变结果。

相反,POST 不是幂等的,多次调用 POST 可能会有不同的结果。

提示

HTTP 动词的幂等性和安全性是一种约定,意味着当有人使用您的 API 时,他们将假设 GET/PUT/POST/DELETE 具有之前描述的相同幂等特性;并且每个动词背后的业务逻辑实现应该支持这些特性。

服务器发送的响应可以是 XML、JSON 或任何其他 MIME 类型,只要服务器支持请求的格式。如果服务器无法支持请求的 MIME 类型,它可以返回状态码 406(不可接受)。

当我们以 RESTful 原则进行开发时,每条消息都应该包含足够的信息,以便服务器理解消息的目的以及如何处理该消息,以生成消息预期的响应,并最终确保可见性和无状态性。

总结,这些是 RESTful Web 服务的组成部分:

  • 基础 URI:Web 服务的基础 URI 为 http://foo.com/bar

  • 媒体类型:Web 服务支持的媒体类型

  • 方法:如 GETPUTPOSTDELETE 这样的 HTTP 方法

JAX-RS 简介

Java API for Representational State Transfer (JAX-RS) 规范定义了一套 Java API,用于构建符合 REST 风格的 Web 服务。

本规范定义了如何使用 HTTP 作为网络协议将 POJOs 公开为 Web 资源。使用这些 API 的应用程序可以以可移植的方式部署到应用程序服务器。

JAX-RS 2.0 规范中引入的一些关键特性如下:

  • 客户端 API

  • 服务器端异步支持

  • Bean Validation 支持

在接下来的章节中,我们将介绍与 JAX-RS 2.0 相关的以下主题:

  • 将 POJO 转换为 RESTful 资源

  • 关于 JAX-RS 注解的更多内容

  • JAX-RS 客户端 API

  • JAX-RS 中的实体

  • JAX-RS 中的自定义实体提供者

  • 在 JAX-RS 中使用 Bean 验证 API

将 POJO 转换为 RESTful 资源

资源类是一个使用 JAX-RS 注解的 POJO。资源类需要至少有一个被 @Path 或请求方法注解的方法。资源是我们所说的网络服务,传入的请求针对这些资源。

将 POJO 转换为 RESTful 端点的步骤:

  1. 定义一个由 URI 标识的根资源

  2. 定义资源的方法

  3. 定义 MIME 类型

  4. 定义应用程序子类

  5. 定义子资源

定义一个由 URI 标识的根资源

JAX-RS 提供了非常丰富的客户端和服务器 API,这些 API 在任何 Java EE 应用程序服务器上都可以工作。使用 JAX-RS API,任何 POJO 都可以注解以构建 RESTful 资源。从一个简单的 POJO BookResource 开始,并使用 JAX-RS API 注解它。

@Path("books")
public class BooksResource {
}

这是一个根资源类,它被 @Path 注解 标注。值 "books" 将表示资源将在类似于以下 URI 的位置可用:http://host:port/appname/books

之后,我们将向此资源添加方法,以便当带有 GETPUT 等请求击中此资源时,将调用类中的特定方法以生成响应。

定义资源的方法

要向此资源添加方法,我们需要使用 @GET@PUT@DELETE@HEAD 注解该方法。在以下示例中,我们选择使用 @GET 注解

@GET
public String getGreeting() {
  return "Hello from Book resource"
}

@GET 注解指定 getGreeting() 方法处理 HTTP GET 请求。

定义 MIME 类型

要指定资源可以处理的 MIME 类型,我们应该使用 @Produces@Consumes 注解资源方法:

@Produces("text/plain")
@GET
public String getGreeting() {
  return "Hello from Book resource"
}

@Produces 指定该方法将生成的媒体类型是 "text/plain"。其他媒体类型的支持以及如何从 Java 映射到特定格式以及相反的映射将在实体提供者部分详细说明。因此,这是对拥有第一个 JAX-RS 资源初识的介绍。下一节将介绍 Application 子类的详细信息。

定义应用程序子类

Application 类是一种配置应用程序级细节的可移植方式,例如指定名称,并注册 JAX-RS 应用程序的各种组件。这包括应用程序中的不同 JAX-RS 资源和 JAX-RS 提供者。

类似地,可以使用 Application 的子类设置应用程序范围内的属性。Application 子类应放置在 WAR 文件的 WEB-INF/classesWEB-INF/lib 中。应用程序类有以下可以重写的方法:

public Set<Class<?>> getClasses() ;
public Map<String, Object> getProperties();
public Set<Object> getSingletons();

这里是我们案例中应用程序子类的一个示例:

@ApplicationPath("/library/")
public class HelloWorldApplication extends Application {
@Override
  public Set<Class<?>> getClasses() {
    Set<Class<?>> classes = new HashSet<Class<?>>();
 classes.add(BooksResource.class);
    return classes;
  }
}

在此代码中,我们创建了一个 HelloWorldApplication,它是 javax.ws.rs.core.Application 的子类。使用 Servlet 3.0,不需要 web.xml 文件,并且 Servlet 容器使用 @ApplicationPath 中指定的值作为 servlet 映射。Application 类的 getClasses() 方法被重写以添加 BooksResource.class

一个基本的 JAX-RS 资源现在可以使用了。当示例部署到如 GlassFish 这样的应用服务器时,你可以使用 curl 发送请求。

这里是一个如何发送 curl -X GET 请求的示例:

curl -X GET http://localhost:8080/helloworld/books

终端窗口中的输出应该是:

来自书籍资源的问候

第五章,通过示例学习 RESTful Web 服务,将展示如何在 web.xml 文件中使用 Application 类。

定义子资源

资源类可以部分处理请求的一部分,并提供另一个子资源来处理请求的剩余部分。

例如,这里是一个根资源 Library 和另一个资源 Book 的片段。

@Path("/")
public class Library {

 @Path("/books/{isbn}")
 public Book getBook(@PathParam("isbn") String isbn){
    //return book
  }
}

public class Book {
 @Path("/author")
  public String getAuthor(){
  }
}

子资源定位器是具有 @Path 注解但没有 HTTP 方法的资源方法。

在前面的例子中,Library 是一个根资源,因为它被 @Path 注解。getBook() 方法是一个子资源定位器,其任务是提供一个可以处理请求的对象。

@PathParam 是一个允许你在方法调用中映射 URI 路径片段的注解。在这个例子中,isbn URI 参数被传递以提供关于书籍的信息。

如果客户端使用以下 URI 发送请求:

GET /books/123456789

将调用 Library.getBook() 方法。

如果客户端使用以下 URI 发送请求:

GET /books/123456789/author

Library.getBook() 方法将首先被调用。返回一个 Book 对象,然后调用 getAuthor() 方法。

更多关于 JAX-RS 注解的内容

@Produces 注解用于定义资源中方法产生的输出类型。@Consumes 注解用于定义输入类型,资源中的方法消耗。

这里是一个资源中用于 POST 请求的方法:

@POST
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
public Response addBook(Book book) {
  BooksCollection.addBook(book);
  return    Response.ok(book).
  type(MediaType.APPLICATION_XML_TYPE).build();
}

如此片段所示,我们有一个 @POST 注解,表示此方法接受 POST 请求。

@Produces(MediaType.APPLICATION_XML) 表示该资源的 addBook() 方法产生了 "application/xml" 媒体类型。

@Consumes(MediaType.APPLICATION_XML) 表示该资源的 addBook() 方法消耗了 "application/xml" 媒体类型。

Response.ok(book) 方法构建了一个类型为 MediaType.APPLICATION_XML_TYPE 的 ok 响应。

其他支持的媒体类型 @Produces@Consumes 包括 "text/xml""text/html""application/json" 等。

如果在 @Produces@Consumes 注解中没有指定媒体类型,则默认假设支持任何媒体类型。

这里是一段显示 @DELETE 注解的代码片段。

@DELETE
@Path("/{isbn}")
public Book deleteBook(@PathParam("isbn")String isbn) {
  return BooksCollection.deleteBook(isbn);
}

@PathParam 注解允许您将方法调用中的 URI 路径片段进行映射。在这个例子中,isbn URI 参数被传递以提供关于书籍的信息。

ISBN 唯一标识了书籍资源,以便可以删除它。

以下表格总结了包含在 Java EE 7 中并贯穿本书的 JAX-RS 2.0 重要注解。

注解 描述
@Path 用于注解一个 POJO,表示其资源路径。例如,@Path("books") 或注解一个注解类中的子资源方法。
@Produces 用于指定资源产生的输出类型,或在更窄的范围内指定资源中方法产生的输出类型。例如:@Produces(MediaType.APPLICATION_JSON)
@Consumes 用于指定资源消耗的类型,或在更窄的范围内指定资源中方法消耗的类型。例如:@Consumes (MediaType.APPLICATION_JSON)
@GET@POST@DELETE 等等 将 HTTP 方法映射到表示类的资源中的方法。例如,@GET 可以放置在 getBook 方法上。
@PathParam 用于指定查询参数名称和方法之间的映射。例如:getBook(@PathParam("isbn") String isbn)
@ApplicationPath 识别作为所有通过路径提供的资源 URI 的基础 URI 的应用程序路径。例如,为图书馆应用程序提供 @ApplicationPath("library")
@Context 可以用来注入上下文对象,如 UriInfo,它提供了关于请求 URI 的特定请求上下文信息。例如:getBook(@Context UriInfo uriInfo,

第五章, 《通过示例学习 RESTful Web 服务》,详细介绍了不同的 JAX-RS API,并将它们与其他 Java EE API 结合起来构建一个实际的应用程序。

JAX-RS 的客户端 API

JAX-RS 2.0 提供了一个丰富的客户端 API 来访问网络资源。以下是使用我们之前构建的 BooksResource 客户端 API 的代码示例:

Client client = ClientBuilder.newClient();
WebTarget target = client.target(URI);

可以使用 ClientBuilder.newClient() API 获取 javax.ws.rs.client.Client 对象的默认实例。BooksResource 可以通过 URI 进行识别。WebTarget 对象用于构建 URI。

String book = target.request().get(String.class);

target.request().get(String.class) 方法构建一个 HTTP GET 请求,并在响应中获取一个 String 类型的对象。下一节将展示其他动词的客户端 API 的更多示例。

JAX-RS 中的实体

HTTP 交互的主要部分由请求和响应实体组成。在某些情况下,实体也被称为有效载荷或消息体。

实体通过请求发送,通常使用 HTTP POSTPUT 方法,或者它们在响应中返回,这对于所有 HTTP 方法都适用。Content-Type HTTP 报头用于指示发送的实体类型。常见的 内容类型 包括 "text/plain""text/xml""text/html""application/json"

媒体类型用于 Accept 报头中,以指示客户端想要接收的资源表示类型。

以下片段展示了如何使用客户端 API 创建 POST 请求。此调用接受一个用户定义类 Book 的实体以及 MediaType.APPLICATION_XML_TYPE 参数。

这里是调用 POST 方法的客户端代码:

Response response = target.request()
post(Entity.entity(new Book("Getting Started with RESTful Web Services","111334444","Enterprise Applications"), MediaType.APPLICATION_XML_TYPE));

在前面的片段中,WebTarget#request() 方法返回一个 Response 对象。

这里是调用 delete 方法的客户端 API 代码:

response = target.path("111334444")
request( MediaType.APPLICATION_XML_TYPE)
.delete();

下一节将展示实现 JAX-RS API 的实体提供者如何映射到 Java 类型请求和响应实体。

JAX-RS 中的自定义实体提供者

JAX-RS 允许开发者向应用程序添加自定义实体提供者。自定义实体提供者可以用于处理请求和响应中的用户定义类。

添加自定义实体提供者提供了一种从消息体中反序列化用户定义类以及将任何媒体类型序列化到用户特定类的方法。

有两种类型的实体提供者:

  • MessageBodyReader

  • MessageBodyWriter

使用 @Provider 注解,可以查找特定于应用程序的提供者类。实体提供者提供表示和关联类型之间的映射。书中包含了一个示例,演示了实体提供者的使用。

MessageBodyReader

应用程序可以通过实现 isReadable() 方法和 readFrom() 方法来提供 MessageBodyReader 接口的实现,将实体映射到所需的 Java 类型。

下图展示了 MessageBodyReader 如何读取 InputStream 对象并将其转换为用户定义的 Java 对象。

MessageBodyReader

以下代码展示了如何提供 MessageBodyReader 的实现,并使用 JAX-RS 与 Java Architecture for XML BindingJAXB)结合。JAXB 提供了一种快速便捷的方式将 XML 架构和 Java 表示形式绑定,使得 Java 开发者能够轻松地将 XML 数据和处理函数集成到 Java 应用程序中。作为此过程的一部分,JAXB 提供了将 XML 实例文档反序列化(读取)到 Java 内容树的方法,然后将 Java 内容树序列化(写入)回 XML 实例文档。

这里有一个名为 Book 的 JAXB 根元素。Book 具有名称和 ISBN 等属性。

@XmlRootElement
public class Book {
  public String name;
  public String isbn;
  public String getName() {
    return name;
  }
  public String getIsbn() {
    return isbn;
  }
  public Book(String name, String isbn) {
    this.name=name;
    this.isbn=isbn;
  }
  //JAXB requires this
  public Book() {

  }
}

MessageBodyReader 实现类可以提供从 inputStream 对象读取并将其转换为 Book 对象的支持。以下表格显示了需要实现的方法:

消息体读取器方法 描述
isReadable() 检查 MessageBodyReader 类是否支持从流转换为 Java 类型。
readFrom() InputStream 读取类型。

这里是 SampleMessageBodyReader 类的代码,它是 MessageBodyReader 接口的实现:

@Provider
public class SampleMessageBodyReader implements 
MessageBodyReader<Book> {
}

@Provider 注解表示这是一个提供者,实现类也可以使用 @Produces@Consumes 注解来限制它们支持的媒体类型。

这里是 isReadable() 方法的实现:

public boolean isReadable(Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType) {
  return true;
}

isReadable() 方法返回 true 以指示这个 SampleMessageBodyReader 类可以处理 mediaType 参数。

这是 SampleMessageBodyReader 类的 readFrom() 方法的实现。可以在这里检查 mediaType 参数,并根据媒体类型采取不同的操作。

public Book readFrom(Class<Book> bookClass, Type type, Annotation[] annotations,
MediaType mediaType,
MultivaluedMap<String, String> stringStringMultivaluedMap,
InputStream inputStream) throws IOException, WebApplicationException {
  try {

 Book book = (Book)unmarshaller.unmarshal(inputStream) ;
    return book;
  } catch (JAXBException e) {
    e.printStackTrace();
  }
  return null;
  }
}

方法返回的 book 对象,然后使用 JAXB Unmarshaller 和提供的 inputStream 对象作为参数进行反序列化。

消息体写入器

MessageBodyWriter 接口代表了一个提供者合约,该合约支持将 Java 类型转换为流。

下图显示了 MessageBodyWriter 如何将用户定义的类 Book 序列化到 outputStream 对象。

MessageBodyWriter

下表显示了 MessageBodyWriter 必须实现的方法及其每个方法的简要描述。

消息体写入器方法 描述
isWritable() 检查 MessageBodyWriter 类 是否支持从指定的 Java 类型进行转换。
getSize() 如果已知大小或为 -1,则检查字节数长度。
writeTo() 从类型写入到流。

这里是需要实现 MessageBodyWriter 接口的方法:

public boolean isWriteable(Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType) {
    return true;
}

MessageBodyWriter 接口的 isWritable() 方法可以自定义以检查此 MessageBodyWriter 实现是否支持该类型。

 public long getSize(Book book, Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType) {
        return -1;
    }

writeTo() 方法之前调用 getSize() 方法以确定响应中的字节数长度。

public void writeTo(Book book, 
Class<?> aClass, 
Type type, Annotation[] annotations, 
MediaType mediaType,
MultivaluedMap<String, Object> map,
OutputStream outputStream) throws 
IOException, WebApplicationException {
  try {

    Marshaller marshaller = jaxbContext.createMarshaller();
    marshaller.marshal(book, outputStream);
  } catch (Exception e) {
    e.printStackTrace();
  }
}

writeTo() 方法将 Book 对象序列化到 OutputStream

提示

使用 MessageBodyReaderMessageBodyWriter 调试错误的技巧:

  • 查找 @Provider 注解。MessageBodyReader 实现类和 MessageBodyWriter 实现类需要 @Provider 注解。

  • 确认 MessageBodyReaderMessageBodyWriter 接口的实现类是否已添加到应用程序子类的 getClasses() 方法中。

  • 检查 MessageBodyReader.isReadable() 方法的实现是否返回 true

  • 检查 MessageBodyWriter.isWritable() 方法的实现是否返回 true

  • 确认 MessageBodyWriter.getSize() 方法是 -1,如果响应的大小未知,或者如果大小已知,则将其设置为正确的值。

客户端看起来是这样的:

Client client = ClientBuilder.newClient();
client.register(MessageBodyReaderWriter.class).register(BooksResource.class);
Response response = target
.request()
.post(Entity.entity(new Book("Getting Started with RESTful Web Services","13332233"), MediaType.APPLICATION_XML_TYPE));

Book  = response.readEntity(Book.class);

使用 client.register() 方法注册 MessageBodyReaderWriter.classBooksResource.class

应用程序类 Book 是通过 response.readEntity(Book.class) 从响应中提取的。

在 JAX-RS 中使用 Bean Validation API

验证是验证给定输入是否符合定义的约束的过程。Bean Validation 规范定义了用于验证 JavaBeans 的 API。本节展示了如何使用 Bean Validation API 验证 JAX-RS 2.0 资源。

验证可以用来确保 JAX-RS 资源中的字段遵循某些约束。例如,检查一个字段是否不是 null 或 ISBN 是否遵循某种模式。使用 Bean Validation,用户可以编写自定义验证器,并使用自定义验证器注解 JAX-RS 资源及其组件。

书中包含的示例将展示如何使用 JAX-RS 2.0 资源与 Bean Validation 结合使用。

下面是一个代码片段,展示了如何强制执行验证,同时定义一个约束并将其添加到用户定义的消息中:

@Path("books")
@ValidateOnExecution(ExecutableType.GETTER_METHODS)
public class BooksResource {

  @GET
  @Path("{isbn}")
  @Consumes(MediaType.APPLICATION_XML)
  @Produces(MediaType.APPLICATION_XML)
 @NotNull(message="Book does not exist for the
 ISBN requested")
  public Book getBook(
  @PathParam("isbn")String isbn)    {
    return BooksCollection.getBook(isbn);

  }
}

可以使用 @ValidateOnExecution 注解有选择地启用和禁用验证。在这个片段中,getBook() 方法被验证,因为 @ValidateOnExecution 注解启用了 ExecutableType.GETTER_METHODS 值的验证。

当执行示例代码时,如果书籍值不为 null,则返回书籍对象。如果书籍值为 null,则会出现验证错误,屏幕上显示的消息为 "Book does not exist for the ISBN requested"。这是之前显示的 @NotNull 注解提供的信息。

在应用程序中启用验证

默认情况下,从响应中获取验证错误是不启用的。书中包含的示例将演示如何从响应中获取验证错误。用户需要通过覆盖 getProperties() 方法,使用 Application 类将 BV_SEND_ERROR_IN_RESPONSE 属性设置为布尔值 true

下面是 Application 子类的 getProperties() 方法。

@override
public Map<String,Object> getProperties() {
  Map<String,Object> properties = new HashMap<String,Object>() ;
 properties.put(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
 return properties;
}

getProperties() 方法返回一个 Map<String,Object> 对象,其中 String 属性 ServerProperties.BV_SEND_ERROR_IN_RESPONSE 被设置为布尔值 true

小贴士

下载示例代码

您可以从您在 www.packtpub.com 的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

从响应中读取验证错误

在将应用程序类配置为将字符串属性 ServerProperties.BV_SEND_ERROR_IN_RESPONSE 设置为布尔值 true 之后,servlet 类中的以下代码将读取响应中的验证错误。

这是客户端代码的显示方式:

List<ValidationError> errors = response.readEntity(new GenericType<List<ValidationError>>() {});

response.readEntity() 方法接受一个 GenericType<ValidationError> 参数的列表。从 response.readEntity() 方法返回的 List<ValidationError> errors 中,我们可以提取验证错误并获取验证消息。运行示例时,将显示以下消息:

"在验证请求时出现 1 个错误"

"请求的 ISBN 对应的书籍不存在"

摘要

本章首先简要介绍了 REST 和 RESTful Web 服务开发的关键原则,然后介绍了将 POJO 转换为 JAX-RS 资源、RESTful 端点,并讨论了不同的 HTTP 动词及其用法。

在介绍之后,本章通过介绍用于向使用 JAX-RS API 开发的资源发送请求的客户端 API,更深入地探讨了 JAX-RS API。我们还介绍了如何使用 MessageBodyReaderMessageBodyWriters 定制实体提供者以生成不同的输出格式。我们学习了如何使用 Bean Validation 验证 JAX-RS 2.0 资源。

在下一章中,我们将介绍不同的轮询技术,将它们与服务器发送事件(SSE)和 WebSockets 进行比较和对比,然后更详细地探讨 Java EE 7 如何为 SSE 和 WebSockets 提供支持。

第二章:WebSocket 和服务器发送事件

随着网络架构的进步和新兴平台的涌现,这些平台可以提供实时或近实时信息,因此有必要有一种有效的方法将更新传达给客户端,这促使引入新的编程模型和新标准,使系统消费者端(也称为客户端,主要是网络浏览器)更容易利用这些实时信息。

在本章中,我们将介绍以下主题:

  • 可以用于解决向客户端传输近实时更新的编程模型和解决方案

  • 使用服务器发送事件(SSE)

  • 使用 WebSocket

本章包含不同的代码片段,但完整的示例,展示了这些片段在实际操作中的使用,作为本书源代码下载包的一部分。

编程模型

在本节中,我们将介绍针对基于服务器生成的更新来处理客户端视图近实时更新的不同编程模型。

轮询

如前所述,HTTP,作为互联网通信的基础,使用简单的请求/响应模型,其中请求要么超时,要么从服务器收到响应。响应可以是请求所期望的实际响应,也可以是错误消息,位于标准错误状态代码之下。客户端始终是通信的发起者;服务器不能在没有收到客户端请求发送响应的情况下发起通信通道。

因此,基本上,要更新客户端,就需要检查服务器上的新更新,如果可用,客户端可以响应更新,例如,更改文本以表示之前不可用的书籍现在可以借阅,或者显示新的图像,或者执行任何其他与从服务器收到的响应相对应的操作。

向服务器发送周期性请求以检查更新被称为轮询。这种方法无法扩展到数十万个客户端,因此它不能成为处理当今应用程序大量客户端的有效编程模型。在轮询模型中,响应不一定包括服务器生成的更新,而可能只是没有特定更新的200 OK响应。在这个模型中,数十个请求可能什么也收不到,只是收到200 OK,没有任何有意义的更新供客户端使用,这意味着这些请求白白浪费了资源。当然,如果客户端数量有限,并且存在严重的兼容性问题,例如非常旧的浏览器,那么这种模型是有用的。以下图表显示了轮询模型:

轮询

基于轮询的模型在客户端通过 JavaScript 得到增强;浏览器可以在不改变页面内容的情况下更新视图,从而在图书馆应用程序中列出可用的书籍,而无需用户刷新页面。以下代码片段展示了用 Java 编写的轮询对的服务器端:

public class PollingServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/plain");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write((new Date()).toString());
    }
}

之前的示例没有使用 JSON 作为数据格式,也没有使用 Java EE 7 和 Servlet 3.1 中引入的新异步功能,这些功能在第三章、详细理解 WebSocket 和服务器端事件和第四章、JSON 和异步处理中进行了讨论,而是展示了基本的工作原理。Servlet 为任何GET请求写入当前日期。servlet 在web.xml文件中映射到PollingServlet路径。

以下代码片段展示了我们如何使用 JavaScript 在后台执行请求,获取响应,并通过操作等效的 DOM 树元素来更新 HTML 页面中div元素的内容。

 <html>
  <head>
    <title>Ajax Continues Polling</title>        
      <script>
        function startUpdating() {
          req = new XMLHttpRequest();
          req.onreadystatechange = function() {updateDiv();};
          req.open("GET", "/PollingServlet", true);
          req.send(null);  
          results = req.responseText;
        }

        function updateDiv(){
          results = req.responseText;
          document.getElementById("dateDiv").innerHTML = results;
          setTimeout("startUpdating()", 5000);
        }
      </script>
  </head>
  <body onload="startUpdating()">
    <p>polling the time from server:</p>
    <div id="dateDiv"></div>
  </body>
</html>

前面的代码片段中的 HTML 页面是最简单的形式;它既不检查每个请求的响应是否为OK,也不检查代码是否在 IE 或非 IE 浏览器中执行,以简化处理。

现在,当页面加载时,会调用startUpdating函数,该函数向之前显示的 Servlet 发送请求,并调用updateDiv函数来更新 GUI,然后在 5 秒后再次调度它的调用。

轮询模型的局限性可以总结如下:

  • 是客户端执行轮询,没有服务推送的参与

  • 由于许多请求会导致没有对客户端有效的更新的响应,因此它消耗资源较多。

  • 请求之间的长时间间隔可能会导致客户端视图过时,而短时间间隔会超载服务器

长轮询

考虑到轮询模型的局限性,出现了一种新的编程模型,其中请求要么超时,要么将有用的更新返回给客户端。在这个模型中,请求被发送到服务器,并在非常长的时间后设置超时,这样通过减少固定时间段内的请求数量和响应数量,尽可能减少握手和请求发起的成本。只有当服务器有更新时,才会发送响应,客户端应该接收这些更新。当这种更新在服务器上可用时,服务器会发送带有更新的响应,客户端在消耗了接收到的更新后,会发起另一个请求。这种模型的优点是请求数量比轮询少,这减少了资源消耗并提高了可扩展性。以下图表展示了长轮询模型。

长轮询

长轮询客户端和 XMLHttpRequest

XMLHttpRequest 对象作为 JavaScript 浏览器 API 的一部分可用,以促进网页的 JavaScript 部分与 Web 服务器的交互。它可以用来向服务器发送请求并接收响应,而无需在页面加载后刷新页面;例如,在 available.html 页面加载后更新可用的书籍列表。

XMLHttpRequest 对象的功能分为事件、方法和属性。以下将简要讨论重要的属性、事件和重要方法。

当事件被触发或方法被调用时,属性的值会发生变化。检查属性值可以评估 XMLHttpRequest 对象的当前状态或处理响应。

  • readyState: 存储正在进行中的请求的当前状态。列表之后的表格显示了 readyState 属性的不同值。

  • responseText: 返回响应文本。

  • responseXML: 返回一个表示响应数据的 DOM 对象。假设响应文本是有效的 XML 文档。可以使用标准的 DOM 解析器方法遍历 XML 文档。

  • status: 显示请求的状态码;例如,200404 等。

  • statusText: 请求状态码的易读文本等效。例如 "OK""Not Found" 等。

readyState 值和文本等效 描述
0 (UNINITIALIZED) XMLHttpRequest 对象已创建,但尚未打开。
1 (LOADING) XMLHttpRequest 对象已创建,已调用 open 方法,但尚未发送请求。
2 (LOADED) 调用了发送方法,但尚未收到响应。
3 (INTERACTIVE) 调用了发送方法,已收到一些数据,但响应尚未结束。
4 (COMPLETED) 响应已结束,已接收到整个消息。消息内容可在 responseBodyresponseText 属性中找到。

每个事件都可以关联一个方法,当事件被触发时,该方法会被调用。随后的示例代码展示了如何使用这些事件。

  • onreadystatechange: 当由该 XMLHttpRequest 实例发起的请求状态改变时,会触发此事件。状态改变通过 readyState 属性进行通信。

  • ontimeout: 当使用该 XMLHttpRequest 实例发起的请求超时时,会触发此事件。

在调用每个方法后,相关属性的值将发生变化。

  • abort: 取消 XMLHttpRequest 实例的当前请求。readyState 值设置为 0

  • open: 通过设置方法、URL 和安全凭据来准备请求。

  • send: 通过 open 方法发送准备好的请求。

分块传输编码

使用长轮询模型的另一种可能方式是使用消息体流来发送数据块和更新事件,当这些块在服务器上可用并准备好由开发者消费时。在消息体流模型中,服务器不会关闭响应,而是保持其打开状态,并将更新事件发送给客户端,就像它们在服务器中产生时一样。消息体流涉及使用分块传输编码,这是一个 HTTP/1.1 功能。

分块传输编码可以用来发送多个数据块作为响应体的一部分,这些数据块以流的形式打开。这些块可以是 JavaScript 标签,它们在隐藏的 iframe 中加载并按到达顺序执行。到达的脚本的执行可以导致视图更新或触发任何其他所需操作。以下图显示了长轮询的实际操作。

分块传输编码

在前面的图中,客户端发送了一个包含 client_id 值的请求到服务器,当有更新可供发送给客户端时,服务器开始发送响应块。这些更新以 JavaScript 标签的形式发送,然后在客户端的浏览器中执行以更新 GUI。

长轮询模型的局限性可以概括如下:

  • 单向通信

  • 在分块传输编码模式下使用时没有标准的数据格式或消息格式

  • 当不使用 iframe 技术时,每个请求只有一个响应

  • 每个连接的初始化都有一个初始化成本

  • 客户端和服务器之间没有缓存,这影响了服务器性能而不是从缓存中读取内容

新兴标准

随着对那些需求及其解决方案的出现,标准出现了以确保不同层、应用程序和组件之间的兼容性;异步通信,尤其是客户端和服务器之间的事件传播,就是其中之一。

服务器发送事件

服务器发送事件 (SSE),有时简称为 EventSource,是一个 HTML5 浏览器 API,它使服务器和客户端之间的事件推送对 Web 应用程序开发者可用。SSE 组件提供了一个结构化的机制,具有类似于长轮询的功能,但没有长轮询的一些缺点。由于它是一个 HTML5 组件,浏览器应该支持 HTML5 SSE 才能利用此 API。

SSE 内核包括 EventSourceEvent

EventSource是提供客户端订阅事件源的 API,这可能是一个 Servlet 或类似的东西。订阅后,这仅仅是打开到 URL 的连接,事件以它们产生的顺序发送到客户端,在客户端,事件监听器可以响应这些事件,例如通过更新聊天窗口、更改图表或更新可借阅的书籍列表或列出对事件 URL 所针对的主题感兴趣的人。

SSE 的解剖结构

在我们深入 API 并了解 API 的工作原理之前,仔细研究 SSE 的特点以及 SSE 是如何工作的,这是很好的。以下图表显示了 SSE 的实际应用,这与前一部分中显示的块编码图表非常相似。可能会有人问:如果两者在请求、响应和消息内容方面工作方式相似,那么是什么使得 SSE 比长轮询更好?

SSE 的解剖结构

使用 SSE,事件是服务器发送到客户端的纯文本消息,这意味着它不需要是一个需要在客户端执行的 JavaScript 标签集合,以更新某些内容,而更可以是客户端的事件监听器可以消费的数据,事件监听器可以解释并响应接收到的事件。

第二个区别是消息格式;SSE 定义了从服务器发送到客户端的事件的消息格式。消息格式由一个由换行符分隔的纯文本字符流组成。以data:开头的行携带消息体或数据,而以 QoS 属性名称后跟冒号然后是 QoS 属性值directive: value开头的行携带一些服务质量QoS)指令。标准格式使得围绕 SSE 开发通用库成为可能,从而简化软件开发。以下代码片段显示了一个可以指示图中新点的示例消息。当客户端收到消息时,它可以在图上绘制新点以显示从图中构建的数据的变化。以下示例数据显示了一个多行消息,其中每行使用\n与下一行分隔,消息的结束用\n\n标记。

data: Position: 75,55\n\n
data: Label: Large increase\n\n
data: Color: red\n\n

可以使用 servlet 开发 SSE 解决方案的服务器组件,而客户端可以使用 JavaScript 或 Java API 进行开发。用于消费 SSE 事件的 Java API 是 Java EE 7 的一部分,通过 JAX-RS 2.0 提供。接下来的两个部分将详细介绍客户端 API 以及解决方案的服务器端组件,后者是一个 servlet。

如前所述,除了实际的消息或消息体之外,每个 SSE 消息还可以携带一些指令,这些指令指示浏览器或 SSE 兼容客户端在交互的一些 QoS(服务质量)属性。以下将讨论一些这些 QoS 指令。

注意

JAX-RS 2.0 的参考实现是在 Jersey 2.0 项目中完成的。Jersey 项目位于jersey.java.net/,拥有详尽的文档。

将 ID 与事件关联

每个 SSE 消息都可以有一个消息标识符,它可以用于各种目的;消息 ID 标准用法之一是跟踪客户端已接收的消息。当在 SSE 中使用消息 ID 时,客户端可以将最后一条消息 ID 作为连接参数之一,指示服务器从该消息开始继续。当然,服务器应该实现适当的程序来从客户端请求的位置恢复通信。

带有消息 ID 的示例消息格式如下代码片段所示:

id: 123 \n
data: single line data \n\n

连接丢失和重新连接重试

支持 SSE 的浏览器(本节开头列出),在浏览器和服务器之间的连接被切断的情况下,可以尝试重新连接到服务器。默认的重试间隔是3000毫秒,但可以通过在服务器发送给客户端的消息中包含retry指令来调整。例如,要将重试间隔增加到5000毫秒,服务器发送的 SSE 消息可以类似于以下代码片段:

retry: 5000\n
data: This is a single line data\n\n

将事件名称与事件关联

另一个 SSE 指令是事件名称。每个事件源可以生成多种类型的事件,客户端可以根据它订阅的事件类型来决定如何消费每种事件类型。以下代码片段显示了事件名称指令如何结合到消息中:

event: bookavailable\n
data: {"name" : "Developing RESTful Services with JAX-RS 2.0, WebSockets and JSON"}\n\n
event: newbookadded\n
data: {"name" :"Netbeans IDE7 Cookbook"}\n\n

服务器发送事件和 JavaScript

被认为是 JavaScript 开发者客户端 SSE(服务器发送事件)基础的 SSE API 是EventSource接口。EventSource接口包含相当数量的函数和属性,但最重要的如下列所示:

  • addEventListener 函数:用于添加事件监听器以处理基于事件类型传入的事件。

  • removeEventListener 事件函数:用于移除已注册的监听器。

  • onmessage 事件函数:在消息到达时被调用。使用onmessage方法时没有可用的自定义事件处理。监听器管理自定义事件处理。

  • onerror 事件函数:在连接出现问题时被调用。

  • onopen 事件函数:在连接打开时被调用。

  • close 函数:在连接关闭时被调用。

下面的片段显示了如何订阅来自一个源省略的不同事件类型。该片段假设传入的消息是 JSON 格式的消息。'bookavailable'监听器使用简单的 JSON 解析器解析传入的 JSON,然后使用它来更新 GUI,而'newbookadded'监听器使用 reviver 函数过滤并选择性地处理 JSON 对。

var source = new EventSource('books');
source.addEventListener('bookavailable', function(e) {
  var data = JSON.parse(e.data);
  // use data to update some GUI element...
}, false);

source.addEventListener('newbookadded', function(e) {
  var data = JSON.parse(e.data, function (key, value) {
    var type;
    if (value && typeof value === 'string') {
return "String value is: "+value;
    }
    return value;
});
}, false);

在我们转向 WebSocket 作为另一种新兴技术之前,让我们看看以下配对的客户端和服务器,它们被编写为 Java EE Servlet 和 JavaScript,以了解 SSE 是如何工作的:

Servlet 的processRequest函数看起来如下所示的片段:

  protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {        
                   response.setContentType("text/event-stream");
                   response.setCharacterEncoding("utf-8");

        PrintWriter out = response.getWriter();
        while(true){                
                Date serverDate = new Date();
                out.write( "event:server-time\n");
                out.write( "data:<b>Current Server Time is:" + serverDate.toString() +"</b>\n\n");
                out.flush();                
                try {
                        Thread.sleep(1000);
                } catch (InterruptedException e) {
                        e.printStackTrace();
                }   
        }                
    }

前面的 Servlet 每秒写入当前日期,如果浏览器访问 Servlet 的 URL,输出应该类似于以下图示。

服务器发送事件和 JavaScript

同一 Web 应用程序中的 JSP 页面看起来如下所示:

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
  <head>
    <title>JSP Page With SSE EventSource</title>
      <script type="text/JavaScript">
        function startSSEConnection(){
        var source = new EventSource('SimpleDateServlet');
        source.addEventListener("server-time", function(event){
         document.getElementById("server-time").innerHTML=event.data;
         },false);
       }

      </script>

      </script>
    </head>
    <body onload="startSSEConnection();">
      <div id="server-time">[Server-Time]</div>
    </body>
</html>

检查 JSP 页面的 URL 将显示类似于以下截图的输出。如您所见,Servlet 的输出消息以 JSP 页面中 JavaScript 代码中指定的格式显示:

服务器发送事件和 JavaScript

在第三章、“详细理解 WebSocket 和服务器发送事件”和第五章、“通过示例理解 RESTful Web 服务”中包含了更完整和高级的示例。前述示例的完整代码包含在本书的代码包中。

WebSocket

HTML5 的 WebSocket 组件通过在客户端和服务器之间引入一个全双工的事件通信通道,为现代 Web 规模应用所需的可扩展性和灵活性提供了一种全新的交互方法。一旦客户端发起,服务器就可以通过该通道发送关于客户端的二进制和文本数据,而客户端也可以在不重新初始化连接的情况下向服务器发送消息。在“服务器发送事件”部分讨论的事件源和事件订阅模型同样适用于 WebSocket。

WebSocket 握手

有一个可选的手动握手请求-响应机制,以便在需要时让应用程序切换到 WebSocket。在下面的示例场景中,客户端通过向服务器发送升级请求头来请求协议升级到 WebSocket。如果服务器支持升级,响应将包括后续显示的协议升级。

客户端请求升级到 WebSocket 看起来如下所示的代码片段:

GET /demo HTTP/1.1
Host: mybookstoresample.com
Connection: Upgrade
Upgrade: WebSocket
Origin: http://mybookstoresample.com

服务器响应握手可以看起来如下所示的片段:

        HTTP/1.1 101 WebSocket Protocol Handshake
        Upgrade: WebSocket
        Connection: Upgrade

在握手完成后,客户端和服务器之间的通信将通过双向套接字进行。WebSocket 的底层通信协议与 HTTP 底层协议不同,因此,中间服务器,如代理服务器或缓存服务器可能无法像处理 HTTP 消息那样拦截和处理 WebSocket 消息。

注意

在第三章,详细了解 WebSocket 和服务器端事件的 WebSocket 部分,你可以了解更多关于 WebSocket 客户端和服务器实现以及协议升级的细节。第五章,通过示例了解 RESTful Web 服务包括完整的示例应用程序,进一步深入使用 WebSocket。

浏览器和 JavaScript 对 WebSocket 的支持

新版的主要网络浏览器支持 WebSocket,在客户端使用 WebSocket 只需创建一个 WebSocket 对象,然后为不同的事件设置不同的监听器和事件处理器。以下列表显示了WebSocket类的关键函数和属性:

  • 构造函数:为了初始化WebSocket对象,只需将资源 URL 传递给WebSocket构造函数即可。

  • send 函数:在对象构造期间,可以使用send函数向服务器指定的 URL 发送消息。

  • onopen 事件函数:当连接创建时,该函数被调用。onopen处理open事件类型。

  • onclose 事件函数:当连接正在关闭时,该函数被调用。onclose处理close事件类型。

  • onmessage 事件函数:当收到新消息时,onmessage函数被调用以处理message事件。

  • onerror 事件函数:当通信通道发生错误时,该函数被调用以处理error事件。

  • close 函数:用于关闭通信套接字并结束客户端与服务器之间的交互。

下面展示了使用 JavaScript WebSocket API 的一个非常基础的示例:

//Constructionof the WebSocket object
var websocket = new WebSocket("books"); 
//Setting the message event Function
websocket.onmessage = function(evt) { onMessageFunc(evt) };
//onMessageFunc which when a message arrives is invoked.
function onMessageFunc (evt) { 
//Perform some GUI update depending the message content
}
//Sending a message to the server
websocket.send("books.selected.id=1020"); 
//Setting an event listener for the event type "open".
addEventListener('open', function(e){
        onOpenFunc(evt)});

//Close the connection.
websocket.close();

下面代码片段展示了示例服务器端组件,一个 WebSocket 端点:

@ServerEndpoint(
decoders = BookDecoder.class,
encoders = BookEncoder.class,
path = "/books/")
public class BooksWebSocketsEndpoint {
@OnOpen
public void onOpen(Session session) {
            }

@OnMessage
public void bookReturned(Library.Book book, Session session) {
            }

@OnClose
public void onClose(Session session){
sessionToId.remove(session);
}
}

WebSocket 端点实现细节的详细信息包含在第三章,详细了解 WebSocket 和服务器端事件以及第五章,通过示例了解 RESTful Web 服务中。

Java EE 和新兴标准

Java EE 一直是一个新兴标准和功能以及能力的采纳者,这些标准和能力是 Java EE 社区所需求的。从 Java EE 6 开始,Java EE 规范领导者将他们的注意力集中在新兴标准上,在 Java EE 7 中,规范中包括了 HTML5、SSE 和 WebSocket 的全面支持;因此,任何 Java EE 应用服务器都可以托管一个 WebSocket、SSE 和 HTML5 面向的应用程序,而不会在服务器端出现任何兼容性问题。

Java EE 和服务器端事件

对于 SSE,这是一个 HTML5 浏览器 API 组件,服务器端可以是生成 SSE 消息的 Servlet,或者它可以是注解了 @Path 的 SSE 资源,即 POJO。在客户端,可以使用 JavaScript 作为标准浏览器 API 来消费 SSE 事件,或者如果需要基于 Java 的客户端,可以使用 Jersey 2.0 中引入的 SSE 客户端 API 来开发。

下表显示了包含在 Jersey 2.0 中的 SSE API 的重要类和接口,它们是 SSE API 的入口点:

描述
Broadcaster 用于向多个 EventChannel 实例广播 SSE。
OutboundEvent 这是发送服务器端事件的输出事件类。一个 OutboundEvent 可以与 id、name、date 和 comment 关联。
EventChannel 这是输出事件消息通道。当从资源方法返回时,底层连接保持打开状态,应用程序能够发送事件。这个类的实例与一个精确的 HTTP 连接相对应。
EventSource 这是读取和处理服务器端 InboundEvents 的客户端。
InboundEvent 这代表一个传入的事件。

| ClientFactory | 这是客户端 API 的主要入口点,用于启动客户端实例。例如:

Client client = ClientFactory.newClient();
WebTarget webTarget= client.target(new URI(TARGET_URI)) ;

|

| Client | 客户端是构建和执行客户端请求以消费返回响应的流畅 API 的主要入口点。|

Client client = ClientFactory.newClient();
WebTarget webTarget= client.target(new URI(TARGET_URI)) ;

|

ResourceConfig 这封装了配置一个 Web 应用程序的配置。

下表显示了包含在 Java EE 7 中并用于本书中开发 SSE 应用程序的重要注解:

注解 描述
@Path 用于注解一个 POJO,表示其资源路径。例如 @Path("books") 或注解一个子资源,该子资源是注解类中的方法。例如 getBook,包括与该方法相关联的参数以及方法参数的验证表达式。例如:@Path("{id: ^\d{9}[\d&#124;X]$}") getBook(@PathParam("id") String isbn10)
@Produces 用于指定资源产生的输出类型,或者在更窄的范围内,指定资源中方法产生的输出类型。例如:@Produces(MediaType.APPLICATION_JSON)
@Consumes 用于指定资源消费的类型,或在更窄的范围内,指定资源中方法消费的类型。例如:@Consumes (MediaType.APPLICATION_JSON)
@GET@POST@DELETE 将 HTTP 方法映射到表示类的资源中的方法。例如,@GET 可以放置在 getBook 方法上
@PathParam 用于指定查询参数名称与方法之间的映射。例如:getBook(@PathParam("id") String isbn10)
@ApplicationPath 识别应用程序路径,该路径作为由 @Path 提供的所有资源 URI 的基本 URI。例如,对于图书馆应用程序,使用 @ApplicationPath("library")
@Context 这可以用来注入上下文对象,如 UriInfo,它提供了关于请求 URI 的特定请求上下文信息。例如:getBook(@Context UriInfo uriInfo)

第三章, 详细理解 WebSocket 和服务器发送事件,专门介绍注解;它解释了如何使用这些注解以及服务器发送事件和 第五章, 通过示例理解 RESTful Web 服务,包括在真实用例中如何使用服务器发送事件和 WebSocket 的完整示例。

Java EE 和 WebSocket

在 Java EE 7 中,有一个新的 JSR 支持在 Java EE 容器中实现 WebSocket。JSR-356 定义了 Java EE 应用服务器为开发基于 WebSocket 的应用程序提供的规范和 API。以下表格中包含了用于 WebSocket 开发的重要注解:

注解 描述
@ClientEndpoint 一个类级别的注解,用于表示一个 POJO 是一个 WebSocket 客户端,指示服务器将其部署为该类型的托管组件。
@OnClose 一个方法级别的注解,用于装饰一个 Java 方法,当 WebSocket 会话关闭时需要被调用。
@OnError 一个方法级别的注解,用于装饰一个 Java 方法,需要被调用以处理连接错误。
@OnMessage 一个方法级别的注解,用于标记一个 Java 方法作为 WebSocket 消息接收器。
@OnOpen 一个方法级别的注解,用于装饰一个 Java 方法,当新的 WebSocket 会话打开时应该被调用。
@PathParam 用于指定查询参数名称与方法之间的映射。例如:getBook(@PathParam("id") String isbn10)
@ServerEndpoint 一个类级别的注解,声明它所装饰的类是一个 WebSocket 端点,该端点将被部署并在 WebSocket 服务器的 URI 空间中可用。例如:@ServerEndpoint("/books "); public class Books {…}

以下表格显示了在讨论 WebSocket 时,整本书中使用的的重要类和接口:

描述
Encode(and subintefaces and subclasses) 定义如何将 WebSocket 消息映射到 Java 对象。
Decoder(and subintefaces and subclasses) 定义如何将 Java 对象映射到 WebSocket 消息。
Session 一个 WebSocket 会话代表两个 WebSocket 端点之间的对话。一旦 WebSocket 握手成功完成,WebSocket 实现就会为端点提供一个打开的 WebSocket 会话。

不同编程模型和标准的比较与用例

以下表格展示了本章中描述的三大技术和标准之间的比较和结论:

主题 SSE WebSockets 长轮询
错误处理 内建错误处理支持 内建错误处理支持 在分块传输的情况下几乎没有任何错误处理
性能 通常结果比长轮询好,但比 WebSocket 差 与其他两种解决方案相比,性能最佳结果 小型 CPU 资源,但每个客户端连接都有一个空闲的进程/线程,限制了可扩展性和内存使用
浏览器支持 1,2 Firefox, Chrome, Safari, Opera 对于 RFC 6455:IE 10, Firefox 11, Chrome 16, Safari 6, Opera 12.10 所有当前浏览器都支持此功能
浏览器性能 浏览器内建支持,资源占用少 浏览器内建支持,资源占用少 特别是在有很多 JavaScript 和可能的内存泄漏时,获取正确的性能比较复杂
通信通道 单向 HTTP 双向 WebSocket 单向 HTTP
实现复杂性 简单 需要支持 WebSocket 的服务器 最简单

更多详情请访问en.wikipedia.org/wiki/WebSocket#Browser_supporten.wikipedia.org/wiki/Server-sent_events#Web_browsers

注意

建议阅读www.ibm.com/developerworks/web/library/wa-memleak/上的JavaScript 内存泄漏模式文章,以避免 JavaScript 内存泄漏的陷阱。

以下列表显示了哪些类型的用例与一种编程模型和标准相匹配:

  • 长轮询:当兼容性是一个问题,并且浏览器没有更新(通常适用于坚持使用多年软件批准版本的企业的用户)

  • SSE: 当通信是单向的,服务器需要向浏览器发送事件以便浏览器更新一些 GUI 元素时。它提供了比长轮询更优越的错误处理和结构化消息格式。以下是一些示例用例:

    • 实时更新的图表

    • 显示最新头条的新闻播报器

    • 股票行情阅读器

  • WebSockets:当客户端和服务器之间需要全双工、双向通信时。以下是一些示例应用:

    • 聊天应用

    • 实时交互式多用户图表和绘图应用

    • 多用户基于浏览器的游戏

  • WebSockets 提供了 SSE 的所有优点和优势,但也存在以下缺点:

    • 传输协议不同,因此一些中间服务器,如代理服务器,可能无法拦截和解释消息。

    • 如果浏览器不支持 WebSockets,则无法使浏览器处理通信,而在 SSE 的情况下,浏览器可以使用 JavaScript 库来处理 SSE 通信,通过 polyfill 来弥补浏览器的不足。例如,Remy Polyfill

    • 缺乏对事件 ID 的支持。

注意

您可以在remysharp.com/2010/10/08/what-is-a-polyfill/找到一篇很好的文章,进一步了解 Polifill。

摘要

本章通过介绍涉及网络架构的基本概念,并随着基本请求/响应模型从轮询、长轮询、服务器发送事件(Server-sent Event)到 WebSockets 的演变,为异步 Web 世界的整个领域打开了大门。

在下一章中,将详细介绍 WebSockets 和服务器发送事件。第五章,《通过示例学习 RESTful Web 服务》,包含使用 WebSockets 和服务器发送事件开发的完整示例应用。

第三章。详细理解 WebSocket 和服务器发送事件

WebSocket 是 HTML5 提供的最有前途的特性之一。正如在第二章中所述,WebSocket 和服务器发送事件,传统的请求-响应模型由于 HTTP 头部而产生了开销。使用 WebSocket,一旦完成初始握手,客户端和服务器或对等方可以直接通信,无需使用头部。这减少了网络延迟,并减少了 HTTP 头部流量。

第二章,WebSocket 和服务器发送事件,还介绍了服务器发送事件,并提供了 SSE 和 WebSocket 的比较。

服务器发送事件定义了一个 API,其中服务器在事件发生时与客户端通信并推送事件。这是一种从服务器到客户端的单向通信,与传统轮询和长轮询技术相比,具有更多优势。

本章涵盖了 WebSocket 和服务器发送事件的先进概念,并涵盖了以下部分:

  • Java API 中 WebSocket 的编码器和解码器

  • Java WebSocket 客户端 API

  • 使用 Java API for WebSockets 发送不同类型的数据,如 Blob 和 Binary

  • WebSocket 的安全性和最佳实践

  • 基于 WebSocket 的应用程序的最佳实践

  • 使用 Jersey API 开发服务器发送事件客户端

  • 服务器发送事件的最佳实践

Java API 中 WebSocket 的编码器和解码器

如前一章所示,类级别的注解 @ServerEndpoint 表示在运行时 Java 类是一个 WebSocket 端点。value 属性用于指定端点的 URI 映射。此外,用户还可以添加编码器和解码器属性,将应用程序对象编码为 WebSocket 消息,并将 WebSocket 消息解码为应用程序对象。

以下表格总结了 @ServerEndpoint 注解及其属性:

注解 属性 描述
@ServerEndpoint 这个类级别注解表示 Java 类是一个 WebSocket 服务器端点。
value 该值是带有前缀 '/.' 的 URI
encoders 包含作为端点编码器的 Java 类列表。这些类必须实现 Encoder 接口。
decoders 包含作为端点解码器的 Java 类列表。这些类必须实现 Decoder 接口。
configurator configurator 属性允许开发者插入他们的 ServerEndpoint.Configurator 实现,该实现用于配置服务器端点。
subprotocols 子协议属性包含端点可以支持的子协议列表。

在本节中,我们将探讨为我们的 WebSocket 端点提供编码器和解码器实现。

Java API 中 WebSocket 的编码器和解码器

前面的图示显示了编码器如何将应用程序对象转换为 WebSocket 消息。解码器将 WebSocket 消息转换为应用程序对象。以下是一个简单的示例,其中客户端向一个被注解为@ServerEndpoint并装饰有编码器和解码器类的 WebSocket java 端点发送 WebSocket 消息。解码器将解码 WebSocket 消息并将相同的信息发送回客户端。编码器将消息转换为 WebSocket 消息。此示例也包含在书籍的代码包中。

下面是定义具有编码器和解码器值的ServerEndpoint的代码:

@ServerEndpoint(value="/book", encoders={MyEncoder.class}, decoders = {MyDecoder.class} )
public class BookCollection {
    @OnMessage
    public void onMessage(Book book,Session session) {

        try {
session.getBasicRemote().sendObject(book);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    @OnOpen
       public void onOpen(Session session) {
           System.out.println("Opening socket" +session.getBasicRemote() );
       }

       @OnClose
       public void onClose(Session session) {
           System.out.println("Closing socket" + session.getBasicRemote());
       }
} 

在前面的代码片段中,你可以看到类BookCollection被注解为@ServerEndpointvalue=/book属性为端点提供了 URI 映射。@ServerEndpoint还指定了在 WebSocket 传输期间要使用的编码器和解码器。一旦建立了 WebSocket 连接,就会创建一个会话,并调用注解为@OnOpen的方法。当 WebSocket 端点接收到消息时,将调用注解为@OnMessage的方法。在我们的示例中,该方法简单地使用Session.getBasicRemote()发送书籍对象,这将获取对RemoteEndpoint的引用并同步发送消息。

编码器可以用于将自定义用户定义的对象转换为文本消息、TextStreamBinaryStreamBinaryMessage格式。

文本消息的编码器类的实现如下:

public class MyEncoder implements Encoder.Text<Book> {
    @Override
 public String encode(Book book) throws EncodeException {
            return book.getJson().toString();
        }
}

如前所述的代码所示,编码器类实现了Encoder.Text<Book>。有一个被重写的编码方法,它将书籍转换为 JSON 字符串并发送。(关于 JSON API 的更多内容将在下一章详细讨论)

解码器可以用于在自定义用户定义的对象中解码 WebSocket 消息。它们可以以文本、TextStream、二进制或BinaryStream格式进行解码。

下面是解码器类的代码:

public class MyDecoder implements Decoder.Text<Book> {
    @Override
   public Book decode(String string) throws DecodeException {
           javax.json.JsonObject jsonObject = javax.json.Json.createReader(new StringReader(string)).readObject();
        return new Book(jsonObject);
    }
    @Override
    public boolean willDecode(String string) {
        try {
            javax.json.Json.createReader(new StringReader(string)).readObject();
            return true;
        } catch (Exception ex) { }
        return false;
        }

在前面的代码片段中,Decoder.Text需要重写两个方法。willDecode()方法检查它是否可以处理此对象并对其进行解码。decode()方法使用 JSON-P API javax.json.Json.createReader()将字符串解码为类型为Book的对象。

下面的代码片段显示了用户定义的类Book

public class Book {
    public Book() {}
    JsonObject jsonObject;
    public Book(JsonObject json) {
        this.jsonObject = json;
    }
    public JsonObject getJson() {
        return jsonObject;
    }
    public void setJson(JsonObject json) {
        this.jsonObject = json;
    }

    public Book(String message) {
        jsonObject = Json.createReader(new StringReader(message)).readObject();
    }
    public String toString () {
        StringWriter writer = new StringWriter();
        Json.createWriter(writer).write(jsonObject);
        return writer.toString();
    }
}

Book类是一个用户定义的类,它接收客户端发送的 JSON 对象。以下是如何从 JavaScript 将 JSON 细节发送到 WebSocket 端点的示例。

var json = JSON.stringify({
                    "name": "Java 7 JAX-WS Web Services",
                "author":"Deepak Vohra",
                "isbn": "123456789"
                });
function addBook() {
                websocket.send(json);
            }

客户端使用websocket.send()发送消息,这将导致BookCollection.javaonMessage()被调用。BookCollection.java将向客户端返回相同的书籍。在这个过程中,当接收到 WebSocket 消息时,解码器将对其进行解码。为了发送回相同的Book对象,首先编码器将Book对象编码为 WebSocket 消息并发送给客户端。

这是 Java WebSocket 客户端 API

第二章,WebSocket 和服务器发送事件,介绍了 Java WebSocket 客户端 API。任何 POJO 都可以通过使用@ClientEndpoint注解转换为 WebSocket 客户端。

此外,用户还可以向@ClientEndpoint注解添加编码器和解码器属性,以将应用程序对象编码为 WebSocket 消息,并将 WebSocket 消息解码为应用程序对象。

以下表格显示了@ClientEndpoint注解及其属性:

注解 属性 描述
@ClientEndpoint 此类级别注解表示 Java 类是一个 WebSocket 客户端,它将连接到 WebSocket 服务器端点。
value 值是带有前缀/的 URI。
encoders 包含一个 Java 类列表,这些类作为端点的编码器。这些类必须实现编码器接口。
decoders 包含一个 Java 类列表,这些类作为端点的解码器。这些类必须实现解码器接口。
configurator 配置器属性允许开发者插入他们自己的ClientEndpoint.Configurator实现,该实现用于配置客户端端点。
subprotocols 子协议属性包含端点可以支持的一组子协议列表。

发送不同类型的消息数据:blob/二进制

使用 JavaScript,我们传统上以字符串的形式发送 JSON 或 XML。然而,HTML5 允许应用程序处理二进制数据以改善性能。WebSocket 支持两种类型的二进制数据

  • 二进制大对象(blob

  • arraybuffer

WebSocket 在任何给定时间只能与一种格式一起工作。

使用 WebSocket 的binaryType属性,您可以在使用 blob 或arraybuffer之间切换:

websocket.binaryType = "blob";
// receive some blob data

websocket.binaryType = "arraybuffer";
// now receive ArrayBuffer data

以下代码片段展示了如何使用 WebSocket 显示服务器发送的图像。

这里有一个使用 WebSocket 发送二进制数据的代码片段:

websocket.binaryType = 'arraybuffer';

上述代码片段将websocketbinaryType属性设置为arraybuffer

websocket.onmessage = function(msg) {
        var arrayBuffer = msg.data;
        var bytes = new Uint8Array(arrayBuffer);

        var image = document.getElementById('image');
        image.src = 'data:image/png;base64,'+encode(bytes);
    }

当调用onmessage时,arrayBuffer被初始化为message.dataUint8Array类型表示一个 8 位无符号整数数组。image.src值使用数据 URI 方案直接嵌入。

安全性和 WebSocket

WebSocket 使用 Web 容器安全模型进行安全保护。WebSocket 开发者可以声明是否需要认证访问 WebSocket 服务器端点,谁可以访问它,或者是否需要加密连接。

映射到ws:// URI 的 WebSocket 端点在部署描述符中受到与具有相同hostname,port路径的http:// URI的保护,因为初始握手来自 HTTP 连接。因此,WebSocket 开发者可以为任何 WebSocket 端点分配认证方案、用户角色和传输保证。

我们将使用与第二章中相同的示例,WebSocket 和服务器发送事件,并将其制作成一个安全的 WebSocket 应用程序。

这是安全 WebSocket 端点的web.xml

<web-app version="3.0"  
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>BookCollection</web-resource-name>
            <url-pattern>/index.jsp</url-pattern>
            <http-method>PUT</http-method>
            <http-method>POST</http-method>
            <http-method>DELETE</http-method>
            <http-method>GET</http-method>
        </web-resource-collection>
            <user-data-constraint>
            <description>SSL</description>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>
</web-app>

如前一个片段所示,我们使用了<transport-guarantee>CONFIDENTIAL</transport-guarantee>

Java EE 规范以及应用程序服务器为客户端与应用服务器之间的通信提供了不同级别的传输保证。这三个级别是:

  • 数据机密性(CONFIDENTIAL):我们使用此级别来确保客户端和服务器之间的所有通信都通过 SSL 层进行,并且不会接受非安全通道上的连接。

  • 数据完整性(INTEGRAL):当不需要完整加密但希望以这种方式将数据从客户端传输到客户端时,我们可以使用此级别,如果有人更改了数据,我们可以检测到更改。

  • 任何类型的连接(NONE):我们可以使用此级别来强制容器接受 HTTP 和 HTTPS 上的连接。

在设置 SSL 并运行我们的示例以展示在 Glassfish 中部署的安全 WebSocket 应用程序时,应遵循以下步骤:

  1. 生成服务器证书:

    keytool -genkey -alias server-alias -keyalg RSA -keypass changeit --storepass changeit -keystore keystore.jks
    
  2. 将生成的服务器证书从keystore.jks导出到文件server.cer

    keytool -export -alias server-alias -storepass changeit -file server.cer -keystore keystore.jks
    
  3. 创建信任存储文件cacerts.jks并将服务器证书添加到信任存储:

    keytool -import -v -trustcacerts -alias server-alias -file server.cer  -keystore cacerts.jks -keypass changeit -storepass changeit
    
  4. 更改以下 JVM 选项,以便它们指向新的密钥库的位置和名称。在domain.xml下的java-config中添加此内容:

    <jvm-options>-Djavax.net.ssl.keyStore=${com.sun.aas.instanceRoot}/config/keystore.jks</jvm-options>
            <jvm-options>-Djavax.net.ssl.trustStore=${com.sun.aas.instanceRoot}/config/cacerts.jks</jvm-options>
    
  5. 重新启动 GlassFish。如果您访问https://localhost:8181/helloworld-ws/,您可以看到安全的 WebSocket 应用程序。

  6. 这是 Chrome 开发者工具下的头部信息看起来如何:安全和 WebSocket

  7. 打开 Chrome 浏览器,点击查看然后点击开发者工具

  8. 点击网络

  9. 在元素名称下选择book,然后点击Frames

如前一个截图所示,由于应用程序使用 SSL 进行加密,WebSocket URI 也包含wss://,这意味着 WebSocket 通过 SSL。

到目前为止,我们已经看到了 WebSocket 消息的编码器和解码器。我们还介绍了如何使用 WebSocket 发送二进制数据。此外,我们还演示了如何安全地基于 WebSocket 的应用程序。现在,我们将介绍 WebSocket 应用程序的最佳实践。

基于 WebSocket 的应用程序的最佳实践

本节将介绍基于 WebSocket 的应用程序的最佳实践。以下主题将涵盖:

  • 限制发送数据的速率

  • 控制消息的最大大小

  • 与代理服务器和 WebSocket 一起工作

限制发送数据的速率

在 WebSocket 连接打开后,可以使用发送函数发送消息。

WebSockets 有一个 bufferedAmount 属性,可以用来控制发送数据的速率。使用 bufferedAmount 属性,您可以检查已排队但尚未发送到服务器的字节数。

这里有一个用于测试 WebSocket 的 bufferedAmount 属性的代码片段。

// This snippet checks for amount of data buffered but not sent yet 
// in case it is less than a predefined THRESHOLD the webSocket 
// can send the data

if (webSocket.bufferedAmount < THRESHOLD)
     webSocket.send(someData);
};

这可以通过使用 setInterval 函数定期完成。正如您所看到的,开发者可以定期检查 bufferedAmount 属性,以查看要发送到服务器的队列中的字节数是否超过了某个阈值。在这种情况下,应该延迟发送消息。一旦缓冲区大小小于阈值,就应该发送更多消息。

检查 bufferedAmount 然后发送数据是一个好习惯。

控制消息的最大大小

在使用 @ServerEndpoint@ClientEndpoint 注解的 Java 类中,@OnMessage 注解上的 maxMessageSize 属性允许开发者指定 ClientEndpointServerEndpoint 可以处理的字节数的最大消息大小。

如果传入的消息超过最大大小,则连接将被关闭。这是一个好习惯,可以控制消息的最大大小,这样客户端在尝试处理一个它无法处理的消息时,不会耗尽其资源。

与代理服务器和 WebSockets 一起工作

第二章,WebSockets 和服务器发送事件,介绍了 WebSocket 升级握手的过程。并非所有代理服务器都支持 WebSockets;因此,代理服务器可能不允许未加密的 WebSocket 流量通过。客户端使用一个 CONNECT 调用,这是绝对不允许的。正确的方法是将请求通过 https 发送到标准端口 443。以下是一个浏览器客户端向 foo.com 端口 443 发送的 HTTP Connect 请求的示例。

CONNECT: foo.com:443
Host: foo.com

由于流量是加密的,因此有更大的机会通过代理服务器。然后 CONNECT 语句将工作,并且将有一个端到端的加密隧道用于 WebSockets。

以下图表显示了客户端如何发送 HTTPS 请求,这些请求可以绕过代理服务器和防火墙;WebSocket 安全方案将工作:

与代理服务器和 WebSockets 一起工作

使用基于 WebSocket 的应用程序与 SSL 一起使用是一个好习惯,这样代理服务器就不会阻碍 WebSocket 通信。

服务器发送事件

我们在第二章中介绍了服务器发送事件,WebSockets 和服务器发送事件,并比较了客户端/服务器轮询替代方案以及 WebSockets。在本章中,我们将介绍更高级的主题,例如使用 Jersey API 开发服务器发送事件客户端以及服务器发送事件的最佳实践。

使用 Jersey API 开发服务器发送事件客户端

第二章,WebSockets 和服务器发送事件,简要介绍了服务器发送事件和 JavaScript API。在本章中,我们将介绍由 Jersey 提供的服务器发送事件的 Java 客户端 API。除了 JAX-RS 2.0 规范的功能外,Jersey 还提供了对服务器发送事件的支持。

EventSource是用于读取InboundEvents的类:

以下代码片段展示了如何使用EventSource API:

   WebTarget webTarget = client.target(new URI(TARGET_URI));
EventSource eventSource = new EventSource(webTarget) {
@Override
public void onEvent(InboundEvent inboundEvent) {
    System.out.println("Data " + inboundEvent.getData(String.class);

}

EventSource对象是通过WebTarget创建的。我们在第一章,使用 JAX-RS 构建 RESTful Web 服务中介绍了WebTarget

当客户端接收到服务器发送事件时,EventSourceonEvent()方法将被调用。InboundEvent具有getData()方法,该方法接受String.class作为消息数据的类型。您可以在其中添加任何自定义定义的类。JAX-RS 的MessagebodyReader将用于读取消息的类型。因此,您可以在使用 JavaScript API 和使用 Jersey 客户端 API 之间的代码中看到相似之处。第五章,通过示例构建 RESTful Web 服务,将展示使用服务器发送事件 Jersey 客户端 API 的完整示例。

基于服务器发送事件的应用的最佳实践

以下章节将介绍基于服务器发送事件的应用的最佳实践。以下主题将被涵盖:

  • 检查事件源的来源是否符合预期

  • 与代理服务器和服务器发送事件一起工作

  • 处理服务器发送事件的容错性

检查事件源的来源是否符合预期

以下代码片段展示了如何检查事件源的来源,以确保它与应用程序的来源相匹配。

if (e.origin != 'http://foo.com') {
alert('Origin was not http://foo.com');
return;

来自与消费事件流内容来源不同的来源的事件流可能导致信息泄露。当从服务器获取事件时,检查事件的发起者是否符合预期是一种良好的做法。

与代理服务器和服务器发送事件一起工作

代理服务器可以在短时间超时后断开 HTTP 连接。为了避免这种断开连接的情况,定期发送注释可能是个好主意。

这就是使用服务器发送事件发送注释的方法。

: this is a comment

OutboundEvent event =  new OutboundEvent.Builder().comment("this is a comment").build();

Outboundevent.Builder API 将向客户端发送注释。

该注释不会触发任何操作,但会确保客户端和服务器之间的连接不会断开。

处理服务器发送事件的容错性

第二章,WebSockets 和服务器发送事件,介绍了如何将 ID 与事件关联。服务器可以通过以下代码片段使用事件 ID 发送事件:

   id: 123\n
   data : This is an event stream \n\n

客户端保持连接活跃,并在连接断开时尝试重新连接。设置一个 ID 让浏览器跟踪最后触发的事件,因此当客户端与服务器之间的连接断开时,客户端在重新连接到服务器时将最后事件 ID发送回服务器。这确保了客户端不会错过任何消息。然后服务器可以发送在最后事件 ID 之后发生的事件。

服务器可能需要一个消息队列来跟踪连接的不同客户端,检查重新连接,并根据最后事件 ID 发送消息。

摘要

在本章中,我们探讨了 WebSocket 和服务器发送事件的先进主题。我们通过代码片段展示了如何使用编码器和解码器,以及如何使用 WebSocket 接收不同类型的数据。我们还演示了一个示例,展示了 WebSocket 如何与 SSL 一起工作,以便在与代理服务器一起工作时,通信是加密的。

我们还讨论了实现服务器发送事件和 WebSocket 的最佳实践。我们学习了如何通过将 ID 与事件关联来确保服务器发送事件中的消息不会丢失。我们还介绍了用于服务器发送事件的 Jersey 客户端 API。

在下一章中,我们将涵盖更多高级主题,例如 Java EE 中的 JSON API 和异步编程的方面,以提高与各种 Java EE 规范(如 JAX-RS 2.0、EJB 和 Servlets)相关的可伸缩性。

第四章:JSON 和异步处理

本章涵盖了全新的 JSR,JSR 353:Java API for JSON Processing jcp.org/en/jsr/detail?id=353,以及与 Java EE 中不同服务和组件相关的 API 更新,这些更新为系统不同组件之间的异步交互提供了更好的支持。以下列表显示了本章涵盖的主题列表:

  • 使用 Java 生成、解析和操作 JSON 数据

  • 在 Servlet 3.1 中引入 NIO API

  • JAX-RS 2.0 的新特性

生成和解析 JSON 文档

JSON 格式被引入作为 XML 格式的替代品,当不需要 XML 的可扩展性和冗长性时,从而减轻复杂 XML 处理对资源的消耗,让较小的设备能够消费它们需要与之交互的不同服务产生的数据流或数据包。

在 Java EE 7 规范之前,Java 中没有标准 API 来处理 JSON 文档,而是有一些开源项目,如google-gsoncode.google.com/p/google-gsonJacksonjackson.codehaus.org,用于操作 JSON 文档。随着 Java EE 7 的发布以及 JSON-P 的加入,Java EE 中添加了一个标准 API,允许开发者以类似于 XML 处理 API 的方式标准地操作 JSON 文档。

JSON-P API 提供了两种解析 JSON 文档的方法,与解析 XML 文档可用的相同两种模型。下一节将解释的基于流的基于事件的解析和基于对象模型树的解析。

JSON API 概述

以下表格显示了 JSON-P 的重要 API 段以及每个类的简要描述。与 JSON API 相关的类位于javax.json包下。后续章节将介绍如何使用这些类。

类别 描述和使用
JsonParser 一个使用事件模型的拉式解析器,用于解析 JSON 对象。
JsonGenerator 一个 JSON 流写入器,以流式方式将 JSON 对象写入输出源,如OutputStreamWriter
JsonBuilder 以编程方式构建JsonObjectJsonArray
JsonReader 从输入源读取JsonObjectJsonArray
JsonWriter JsonObjectJsonArray写入输出源
JsonObjectJsonArray 用于存储JSONObject和数组结构
JsonStringJsonNumber 用于存储字符串和数值

JSONObject 是整个 JSON API 工具箱的入口点。对于以下每个对象,JSON API 都提供了一个工厂方法以及一个创建器来创建它们。例如,Json.createParserJson.createParserFactory 可以用来创建一个 JSON 解析器。工厂可以被配置为生成定制的解析器,或者当需要多个解析器时,以减少创建解析器的性能开销,而 createParser 重载可以用来创建具有默认配置的 JSON 解析器。

使用基于事件的 API 操作 JSON 文档

当需要单方向、向前解析或生成 JSON 文档时,最好使用基于事件的 API。基于事件的 API 的工作方式类似于 XML 文档的 StAX 解析,但方式更为简单(因为 JSON 格式更为简单)。

生成 JSON 文档

当有一系列事件到达,并且需要将它们转换为 JSON 格式供其他消费 JSON 格式的处理器使用时,使用基于事件的 API 来生成 JSON 文档是最合适的。

以下示例代码展示了如何使用 JsonGenerator 生成 JSON 输出:

public static void main(String[] args) {
        Map<String, Object>configs = new HashMap<String, Object>(1);
configs.put(JsonGenerator.PRETTY_PRINTING, true);
JsonGeneratorFactory factory = Json.createGeneratorFactory(configs);
JsonGeneratorgenerator = factory.createGenerator(System.out);

generator.writeStartObject()                    
            .write("title", "Getting Started with RESTful Web Services")  
                .write("type", "paperback")    
                .write("author", "Bhakti Mehta, Masoud Kalali")   
                .write("publisher", "Packt")            
                .write("publication year", "2013")     
                .write("edition",  "1")
        .writeEnd()                             
        .close();
    }

执行前面的代码会在标准输出中产生以下内容:

{
    "title":" Getting Started with RESTful Web Services",
        "type":" paperback",
        "author":" Bhakti Mehta, Masoud Kalali",
        "publisher":"Packt",
        "edition":"1"
}

在代码的开始部分,创建的属性对象可以用来向 JsonGenerator 对象添加预期的行为指令。可以指定的指令因实现而异,但在这里使用 JsonGenerator.PRETTY_PRINTING 确保生成的 JSON 文档格式化且易于阅读。

注意

JsonParserJsonGeneratorJsonReaderJsonWriter 可以在自动资源管理块中使用,例如:

try (JsonGenerator generator = factory.createGenerator(System.out);) 
       {
        }

解析 JSON 文档

假设前一个示例的结果已保存到名为 output.json 的文件中,以下代码片段可以用来使用流解析器解析 output.json

FileInputStreambooksInputfile = new FileInputStream("output.json");
JsonParser parser = Json.createParser(booksInputfile);
            Event event = null;
            while(parser.hasNext()) {
                event = parser.next();
                if(event == Event.KEY_NAME&&"details".equals(parser.getString())) {
                    event = parser.next();
                    break;
                }
            }
            while(event != Event.END_OBJECT) {
                switch(event) {
                    case KEY_NAME: {
                        System.out.print(parser.getString());
                        System.out.print(" = ");
                        break;
                    }
                    case VALUE_NUMBER: {
                        if(parser.isIntegralNumber()) {
                          System.out.println(parser.getInt());
                        } else {
                          System.out.println(parser.getBigDecimal());
                        }
                       break;
                    }
                    case VALUE_STRING: {
                         System.out.println(parser.getString());
                        break;
                    }
                    default: {
                    }
                }
                event = parser.next();
            }

解析 JSON 文档时应处理的的事件类型如下列出:

  • START_ARRAY:指示 JSON 文档中的数组开始

  • START_OBJECT:指示对象的开始

  • KEY_NAME:键的名称

  • VALUE_STRING:当键的值是字符串时

  • VALUE_NUMBER:当值是数字时

  • VALUE_NULL:如果值是 null

  • VALUE_FALSE:如果值是布尔值 false

  • VALUE_TRUE:如果值是布尔值 true

  • END_OBJECT:到达对象的末尾

  • END_ARRAY:到达数组的末尾

使用 JSON 对象模型操作 JSON 文档

解析 JSON 的文档对象模型提供了与 XML DOM 解析相同的灵活性和限制。灵活性的列表包括但不限于,向前和向后遍历和操作 DOM 树;缺点或权衡在于解析速度和内存需求。

生成 JSON 文档

以下示例代码展示了如何使用构建器 API 生成 JSON 文档,然后将其产生的对象写入标准输出:

Map<String, Object>configs = new HashMap<String, Object>();        
JsonBuilderFactory factory = Json.createBuilderFactory(configs);
JsonObject book= factory.createObjectBuilder()
.add("title", "Getting Started with RESTful Web Services")
.add("type", "paperback")
.add("author", "Bhakti Mehta, Masoud Kalali")
.add("publisher", "Packt")
.add("publication year", "2013")
.add("edition", "1")
.build();   
configs.put(JsonGenerator.PRETTY_PRINTING, true);
JsonWriter writer = Json.createWriterFactory(configs).createWriter(System.out);
writer.writeObject(book);

注意在创建 JsonBuilderFactory 时传递的配置属性。根据 JSON API 实现,可以向工厂传递不同的配置参数以生成定制的 JsonBuilder 对象。

生成的 JSON 输出看起来如下:

{
    "title":" Getting Started with RESTful Web Services",
        "type":" paperback",
        "author":" Bhakti Mehta, Masoud Kalali",
        " publisher ":" Packt",
        " edition":"1"
}

解析 JSON 文档

使用对象模型解析 JSONObject 是直接的,从创建读取器对象并将输入文件/文档读取到 JSONObject 开始。在访问 JSONObject 后,可以遍历其原始和数组属性。

Map<String, Object>configs = 
new HashMap<String, Object>(1);        
JsonReader  reader = 
Json.createReader(new FileInputStream("book.json"));        
JsonObject book=reader.readObject();       
        String title = book.getString("title");
int edition = book.getString("edition");

如示例代码所示,读取 JSONObject 的每个属性是通过类型化的 getter 完成的;例如 getStringgetIntgetNullgetBoolean 等等。

何时使用流式 API 与对象 API

当你正在操作大型 JSON 文档,且不希望将其存储在内存中时,基于流的事件驱动 API 是有用的。在你在 JSON 文档的不同节点之间导航的情况下,对象模型 API 是有用的。

介绍 Servlet 3.1

Java EE 7 规范带来了 Servlet API 的更新规范,该规范解决了社区请求和行业需求的一些变化,包括但不限于以下列表中的变化:

  • 将 NIO API 添加到 servlet 规范中

  • 添加新的 WebSocket 协议升级支持等

下两个部分将详细介绍这些变化及其使用方法。

NIO API 和 Servlet 3.1

Servlet 3 引入了异步处理传入请求的功能,其中请求可以在请求处理完成之前被放置在处理队列中,而不需要将线程绑定到请求。在 Servlet 3.1 中,又向前迈出一步,接收请求数据并写入响应可以以非阻塞、回调为导向的方式进行。

介绍 ReadListener 和 WriteListener

引入两个监听器,允许开发者在有可读数据传入时基本接收通知,而不是阻塞直到数据到达,并在可以写入输出而不被阻塞时接收通知。

ReadListener 接口,它提供在请求的 InputStream 中数据可用时的回调通知,如下所示,一个具有三个方法的简单接口,这些方法在代码片段之后进行描述。

public interface ReadListener extends EventListener { 
 public void onDataAvailable(ServletRequest request); 
 public void onAllDataRead(ServletRequest request); 
 public void onError(Throwable t); 
}
  • onDataAvailable:在读取当前请求的所有数据后被调用。

  • onAllDataRead:由容器在第一次可能读取数据时调用。

  • onError:在处理请求时发生错误时调用。

当在 Servlet 的 OutputStream 中可能写入数据时,WriteListener 会提供回调通知,这是一个简单的两个方法接口,如下所示,并在之后进行描述。

public interface WriteListener extends EventListener { 
public void onWritePossible(ServletResponse response); 
public void onError(Throwable t); 
}

当在 Servlet 的 OutputStream 中可能写入时,容器会调用 onWritePossible 方法。

当在 Servlet 的 OutputStream 中写入时遇到异常时,会调用 onError

如何使用这两个监听器的示例代码包含在 Servlet 3.1 介绍部分的末尾。

Servlet API 接口的更改

为了使用新引入的接口,Servlet API 进行了一些更改。这些更改如下:

  • ServletOutputStream 接口中:

    • isReady: 该方法可用于确定是否可以无阻塞地写入数据

    • setWriteListener: 指示 ServletOutputStream 在可能写入时调用提供的 WriteListener

  • ServletInputStream 接口中

    • isFinished: 当从流中读取所有数据时返回 true,否则返回 false

    • isReady: 如果可以无阻塞地读取数据,则返回 true,否则返回 false

    • setReadListener: 指示 ServletInputStream 在可能读取时调用提供的 ReadListener

现在是时候看看非阻塞的 Servlet 3.1 API 是如何工作的了。以下代码片段显示了一个使用非阻塞 API 的异步 Servlet:

@WebServlet(urlPatterns="/book-servlet", asyncSupported=true)
public class BookServlet extends HttpServlet {
    protected void doPost(HttpServletRequestreq, HttpServletResponse res)
            throws IOException, ServletException {
AsyncContext ac = req.startAsync();
ac.addListener(new AsyncListener() {
            public void onComplete(AsyncEvent event) throws IOException {
event.getSuppliedResponse().getOutputStream().print("Async Operation Completed");
            }
            public void onError(AsyncEvent event) {
System.out.println(event.getThrowable());
            }
            public void onStartAsync(AsyncEvent event) {
System.out.println("Async Operation Started");
            }
            public void onTimeout(AsyncEvent event) {
System.out.println("Async Operation Timedout");
            }
        });
ServletInputStream input = req.getInputStream();
ReadListenerreadListener =  new ReservationRequestReadListener(input, res, ac);
input.setReadListener(readListener);
    }
}

代码从声明 Servlet 并启用异步支持开始,通过在 @WebServlet 注解中指定 asyncSupported=true 来实现。

下一步是设置 AsyncListener 以处理 AsyncEvents。调用 AsyncContext 之一将 AsyncListener 设置为 AsyncContext#addListner 重载。当 Servlet 的异步调用成功完成或因超时或错误结束时会接收 AsyncEvents。可以注册多个监听器,并且监听器会按照它们注册的顺序接收事件。

代码的最后部分将 servlet 的 readListener 设置为下面的 ReadListener 实现。当设置 ReadListener 时,读取传入请求的操作委托给 ReservationRequestReadListener

class ReservationRequestReadListener implements ReadListener {
    private ServletInputStream input = null;
    private HttpServletResponse response = null;
    private AsyncContext context = null;    
    private Queue queue = new LinkedBlockingQueue();

ReservationRequestReadListener(ServletInputStream in, HttpServletResponse r, AsyncContext c) {
this.input = in;
this.response = r;
this.context = c;
    }

    public void onDataAvailable() throws IOException {
StringBuildersb = new StringBuilder();
int read;
byte b[] = new byte[1024];
while (input.isReady() && (read = input.read(b)) != -1) {
String data = new String(b, 0, read);
sb.append(data);
        }
queue.add(sb.toString());
    }

public void onAllDataRead() throws IOException {
performBusinessOperation();
ServletOutputStream output = response.getOutputStream();
WriteListenerwriteListener = new ResponseWriteListener(output, queue, context);
output.setWriteListener(writeListener);
    }

public void onError(Throwable t) {
context.complete();        
    }
}

当容器有数据可读并且读取数据完成后,会调用 ReservationRequestReadListener.onDataAvailableonAllDataRead 在可用的数据上执行业务操作,并将写入数据的 ResponseWriteListener 存储在队列中,然后发送回客户端。ResponseWriteListener 如下所示:

class ResponseWriteListener implements WriteListener {
    private ServletOutputStream output = null;
    private Queue queue = null;
    private AsyncContext context = null;

ResponseWriteListener(ServletOutputStreamsos, Queue q, AsyncContext c) {
this.output = sos;
this.queue = q;
this.context = c;
    }

    public void onWritePossible() throws IOException {
        while (queue.peek() != null &&output.isReady()) {
            String data = (String) queue.poll();
            output.print(data);
        }
        if (queue.peek() == null) {
            context.complete();
        }
    }

    public void onError(final Throwable t) {
           context.complete();
           t.printStackTrace();
    }
}

当写入操作正常或致命地完成时,需要使用 context.complete() 方法关闭上下文以执行此操作。

Servlet 3.1 的更多更改

除了非阻塞 IO 包含之外,Servlet 3.1 还引入了对协议升级的支持,以支持新的 WebSockets API。将 upgrade 方法添加到 HttpServletRequest 允许开发者在容器支持的情况下将通信协议升级到其他协议。

当发送升级请求时,应用程序决定执行升级操作,调用 HttpServletRequest#upgrade(ProtocolHandler),然后应用程序像往常一样准备并发送适当的响应给客户端。此时,Web 容器将回滚所有 servlet 过滤器,并标记连接由协议处理器处理。

JAX-RS 2.0 的新特性

JAX-RS 2.0 引入了几项新特性,这些特性与其他组件中提供的轻量级和异步处理特性保持一致。新特性包括以下内容:

  • 客户端 API

  • 常规配置

  • 异步处理

  • 过滤器/拦截器

  • 超媒体支持

  • 服务器端内容协商

从这个特性列表中,本节涵盖了异步处理,以及异步处理与过滤器/拦截器的相关性。

异步请求和响应处理

异步处理包含在 JAX-RS 2.0 的客户端和服务器端 API 中,以促进客户端和服务器组件之间的异步交互。以下列表显示了为支持此功能添加的新接口和类:

  • 服务器端:

    • AsyncResponse:一个可注入的 JAX-RS 异步响应,提供了异步服务器端响应处理的手段。

    • @Suspended@Suspended 指示容器 HTTP 请求处理应在副线程中发生。

    • CompletionCallback:一个请求处理回调,接收请求处理完成事件。

    • ConnectionCallback:异步请求处理生命周期回调,接收与连接相关的异步响应生命周期事件。

  • 客户端:

    • InvocationCallback:可以实现的回调,用于接收调用处理的异步处理事件。

    • Future:允许客户端轮询异步操作的完成情况,或者阻塞并等待它

注意

Java SE 5 中引入的 Future 接口提供了两种不同的机制来获取异步操作的结果:首先是通过调用 Future.get(…) 变体,这将在结果可用或发生超时之前阻塞;第二种方式是通过调用 isDone()isCancelled() 来检查完成情况,这两个布尔方法返回 Future 的当前状态。

以下示例代码展示了如何使用 JAX-RS 2 API 开发异步资源:

@Path("/books/borrow")
@Stateless
public class BookResource {   
  @Context private ExecutionContextctx;
  @GET @Produce("application/json")
  @Asynchronous
  public void borrow() {
         Executors.newSingleThreadExecutor().submit( new Runnable() {
         public void run() { 
         Thread.sleep(10000);     
         ctx.resume("Hello async world!"); 
         } });  
         ctx.suspend();		
         return;
  } 
} 

BookResource是一个无状态会话 bean,它有一个borrow()方法。此方法使用@Asynchronous注解,将以“发射后不管”的方式工作。当通过borrow()方法的资源路径请求资源时,会启动一个新线程来处理请求的响应准备。该线程被提交给执行器执行,处理客户端请求的线程通过ctx.suspend释放以处理其他传入请求。当用于准备响应的工作线程完成准备响应后,它调用ctx.resume,让容器知道响应已准备好发送回客户端。如果在ctx.suspend之前调用ctx.resume(工作线程在执行达到ctx.suspend之前已准备好结果),则挂起将被忽略,并将结果发送给客户端。

可以使用以下片段中显示的@Suspended注解实现相同的功能:

@Path("/books/borrow")
@Stateless
public class BookResource {   
  @GET @Produce("application/json")
  @Asynchronous
  public void borrow(@Suspended AsyncResponsear) {
  final String result = prepareResponse();
  ar.resume(result)  } 
}   

使用@Suspended更为简洁,因为它不涉及使用ExecutionContext方法来指示容器在工作线程(在这种情况下是prepareResponse()方法)完成时挂起和恢复通信线程。消费异步资源的客户端代码可以使用回调机制或代码级别的轮询。以下代码展示了如何通过Future接口进行轮询:

Future<Book> future = client.target("("books/borrow/borrow")
               .request()
               .async()
               .get(Book.class);
try {
   Book book = future.get(30, TimeUnit.SECONDS);
} catch (TimeoutException ex) {
  System.err.println("Timeout occurred");
}

代码从向图书资源发送请求开始,然后Future.get(…)阻塞,直到从服务器返回响应或 30 秒超时。

异步客户端的另一个 API 是使用InvocationCallback

过滤器和拦截器

过滤器和拦截器是 JAX-RS 2.0 中添加的两个新概念,允许开发者在请求和响应的入出之间进行拦截,以及在入出有效载荷的流级别上进行操作。

过滤器的工作方式与 Servlet 过滤器相同,并提供对入站和出站消息的访问,用于诸如身份验证/日志记录、审计等任务,而拦截器可以用于在有效载荷上执行诸如压缩/解压缩出站响应和入站请求等简单操作。

过滤器和拦截器是异步感知的,这意味着它们可以处理同步和异步通信。

EJB 3.1 和 3.2 中的异步处理

在 Java EE 6 之前,Java EE 中唯一的异步处理设施是JMSJava 消息服务)和MDBs消息驱动 Bean),其中会话 bean 方法可以发送 JMS 消息来描述请求,然后让 MDB 以异步方式处理请求。使用 JMS 和 MDBs,会话 bean 方法可以立即返回,客户端可以使用方法返回的引用检查请求的完成情况,该引用用于处理某些 MDBs 的长时间运行操作。

上述解决方案效果良好,因为它已经工作了十年,但它并不容易使用,这也是 Java EE 6 引入 @Asynchronous 注解来注解会话 Bean 中的方法或整个会话 Bean 类作为异步的原因。@Asynchronous 可以放在类上,以标记该类中的所有方法为异步,也可以放在方法上,以标记特定方法为异步。

有两种类型的异步 EJB 调用,如下所述:

  • 在第一个模型中,方法返回 void,并且没有容器提供的标准机制来检查方法调用的结果。这被称为触发并忘记机制。

  • 在第二个模型中,容器通过方法调用返回的 Future<?> 对象提供了一个机制来检查调用的结果。这个机制被称为调用后检查。请注意,Future 是 Java SE 并发包的一部分。有了从方法返回的 Future 对象,客户端可以使用不同的 Future 方法(如 isDone()get(…))来检查调用的结果。

在深入示例代码或使用 @Asynchronous 之前,值得提一下,在 Java EE 6 中,@Asynchronous 只在完整配置文件中可用,而在 Java EE 7 中,该注解也被添加到 Web 配置文件中。

开发异步会话 Bean

以下列表显示了如何使用调用后检查异步 EJB 方法:

@Stateless
@LocalBean
public class FTSSearch {

    @Asynchronous
    public Future<List<String>> search(String text, intdummyWait) {        
        List<String> books = null;
        try {
           books= performSearch(text,dummyWait);
        } catch (InterruptedException e) {
            //handling exception
        }
        return new AsyncResult<List<String>>(books);        
    }
    private List<String>performSearch(String content, intdummyWait) throws InterruptedException{
Thread.sleep(dummyWait);
return Arrays.asList(content);
    }
}

@Stateless@LocalBean 是自解释的;它们将这个类标记为具有本地接口的无状态会话 Bean。

search 方法被注解为 @Asynchronous,这告诉容器方法调用应该在单独的分离线程中发生;当结果可用时,返回的 Future 对象的 isDone() 返回 true。

search 本身调用一个可能运行时间较长的 performSearch 方法,以获取客户端请求的长时间运行搜索操作的结果。

开发异步会话 Bean 的客户端 Servlet

现在无状态会话 Bean 已经开发完成,是时候开发一个客户端来访问会话 Bean 的业务方法了。在这种情况下,客户端是一个 Servlet,以下代码中包含了部分样板代码:

@WebServlet(name = "FTSServlet", urlPatterns = {"/FTSServlet"})
public class FTSServlet extends HttpServlet {

    @EJB
FTSSearchftsSearch;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        Future<List<String>>wsResult = ftsSearch.search("WebSockets", 5000);
        Future<List<String>>sseResult = ftsSearch.search("SSE", 1000);

        while (!sseResult.isDone()) {
            try {
Thread.sleep(500);
                //perform other tasks... e.g. show progress status
            } catch (InterruptedException ex) {
Logger.getLogger(FTSServlet.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
        try {
            /* TODO output your page here. You may use following sample code. */
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
out.println("<title>Servlet d</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>SSE Search result: " + sseResult.get().get(0) + "</h1>");
            while (!wsResult.isDone()) {
                try {
Thread.sleep(500);
                } catch (InterruptedException ex) {
Logger.getLogger(FTSServlet.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
out.println("<h1>WS Search result: " + wsResult.get().get(0) + "</h1>");
out.println("</body>");
out.println("</html>");
        } catch (InterruptedException ex) {
Logger.getLogger(FTSServlet.class.getName()).log(Level.SEVERE, null, ex);
        } catch (ExecutionException ex) {
Logger.getLogger(FTSServlet.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
out.close();
        }
    }

}

从顶部开始,我们有 Servlet 声明注解、无状态 EJB 的注入以及 get 方法的实现。

get 方法的实现中,调用 EJB 的 search 方法时传递了两个不同的 dummyTime 来模拟等待。在 search 方法的调用之间,直到 Future 对象的 isDone 返回 true,客户端代码可以执行其他所需的操作。

现在已经描述了调用并稍后检查的模式,我们可以讨论另一种异步 EJB 调用模式,其中 EJB 业务方法返回void,并且没有容器提供的检查结果的方式。我们通常使用这些方法来触发一个长时间运行的任务,当前线程不需要等待它完成。

这种情况的例子是当图书馆新增一本电子书,全文搜索索引需要更新以包含新书时。在这种情况下,添加书籍的流程可以调用一个@Asynchronous EJB 方法在书籍注册期间以及上传到服务器仓库后对书籍进行索引。这样,注册过程就不需要等待全文搜索索引完成,而全文搜索索引则是在书籍添加到图书馆后立即开始。

摘要

本章,在展示一些真实世界示例之前的最后一章,讨论了 JSON 处理、异步 JAX-RS 资源,这些资源可以生成或消费 JSON 数据,同时讨论了 Servlet 3.1 中的新 NIO 支持。由于@Asynchronous EJB 现在包含在 Java EE 7 的 Web 配置文件中,我们讨论了该特性以及 Java EE7 中引入的其他新特性。下一章将展示如何将这些技术和 API 结合起来形成解决方案的真实世界示例。

第五章:通过示例学习 RESTful Web 服务

在前几章中介绍和讨论的 API 和技术适用于不同类型的项目和用例。本章将介绍这些 API 和技术如何融入解决方案和面向案例的软件系统。

在简要介绍应用程序预期要做什么之后,我们将将其分解,并专注于每个组件和技术。所以,来一杯额外的摩卡,加入我们的乐趣吧。

本章将涵盖以下两个示例:

  • 基于 Twitter 搜索 API 的事件通知应用程序,展示 Server-sent Events、Async Servlet、JSON-P API 和 JAX-RS

  • 展示 JAX-RS API、WebSockets、JSON-P API 和异步 JAX-RS 资源以形成一个端到端解决方案的图书馆应用程序

事件通知应用程序

基于 Twitter 的应用程序是第一个示例应用程序,它将演示一个基于 HTML5 的应用程序,该应用程序在 Server-sent Events、JAX-RS 2.0 API、异步 Servlet 和 Twitter 搜索 API 的基础上开发,以定期动态更新页面并显示更多搜索结果。

用于示例应用程序的构建系统是 Maven,示例可以部署在任何 Java EE 7 兼容的应用服务器中,特别是 GlassFish v4.0,它是 Java EE 7 规范的开源参考实现。

注意

Apache Maven 是一个构建管理工具。有关 Maven 的更多信息,请访问 maven.apache.org,有关 GlassFish 的更多信息,请访问 [glassfish.java.net/](http:// https://glassfish.java.net/)

项目的布局

项目的目录布局遵循标准的 Maven 结构,以下表格中简要说明了:

源代码 描述
src/main/java 此目录包含图书馆应用程序所需的所有源代码。
src/main/webapp 此目录包含 JavaScript 文件、html 文件和 WEB-INF/web.xml 文件。

事件通知 GUI

事件通知应用程序由一个屏幕组成,该屏幕用作根据 Twitter 流显示动态更新的载体。屏幕在以下屏幕截图中显示:

事件通知 GUI

该应用程序是一个基本示例,显示在携带更新的事件发生并接收时进行更新。这可能是一则新推文、Facebook 朋友的更新或任何其他类型的事件,这些事件可以被任何 Java EE 管理组件消费。关键是,一旦与服务器建立了通信通道,服务器就有责任在事件发生时发送更新。客户端不需要轮询更新。

在这个示例中,当 servlet 被加载时,有一个 EJB 定时器,每 10 秒运行一次,并激活一个使用 Twitter 搜索 API 获取新推文的 CDI 容器。Twitter 搜索 API 以 JSON 格式返回推文。然后,使用 JAX-RS 的服务器端发送事件支持将这些推文信息发送到客户端。在客户端,JSON 数据被解析以在屏幕上显示某些信息。

对事件通知应用程序的详细分析

在对应用程序应该做什么进行初步介绍之后,让我们进一步剖析它,并研究构建此应用程序的各个单独组件。

下面是介绍应用程序详细信息的顺序:

  • web.xml

  • Application 类的实现

  • 应用程序中使用的 JAX-RS 资源

  • 应用程序使用的异步 Servlet 客户端

  • 与 Twitter 搜索 API 交互的 EJB

web.xml

要设置应用程序,请按照以下方式配置 servlet 部署描述符 web.xml

<display-name>jersey-sse-twitter-sample</display-name>

<servlet>
  <servlet-name>Jersey application</servlet-name>
  <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
  <init-param>
    <param-name>javax.ws.rs.Application</param-name>
    <param-value>org.glassfish.jersey.sample.sse.MyApplication</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
  <async-supported>true</async-supported>
</servlet>
<servlet-mapping>
  <servlet-name>Jersey application</servlet-name>
  <url-pattern>/*</url-pattern>
</servlet-mapping>

MyApplicationjavax.ws.rs.Application 的子类。它用于注册 JAX-RS 资源,以便 JAX-RS API 能够识别。

async-supported 元素设置为 true 以指示该 servlet 支持异步处理。

Application 类的实现

下面是 Application 子类的实现:

public class MyApplication extends Application {

  Set<Class<?>> classes = new HashSet<Class<?>>() {
    { add(ServerSentEventsResource.class);
      add(SseFeature.class);
    }
  };

  @Override
  public Set<Class<?>> getClasses() {
    return classes;
  }
}

getClasses() 方法被重写以返回:

  • ServerSentEventsResource.class

  • SseFeature.class

ServerSentEventsResource 类是一个简单的 JAX-RS,它将来自 Twitter 搜索 API 的 JSON 数据作为服务器端发送事件发送。我们将在下一节中更详细地查看 ServerSentEventsResource

SseFeature.class 是由 Jersey 提供的实现,用于支持 ServerSentEvents 功能。它将确保数据是 "text/event-stream" 媒体类型。

小贴士

要启用服务器端发送事件功能,将 SseFeatures.class 添加到 javax.ws.rs.Application 实现中 getClasses() 方法返回的类列表中。

应用程序使用的 JAX-RS 资源

下面是 ServerSentEventsResource.java 的源代码。这是一个简单的 POJO,使用 @Path 注解来标识资源的 URI。

@Path("twittersse")
public class ServerSentEventsResource {

  static EventOutput eventOutput = new EventOutput();

  @GET
  @Produces(SseFeature.SERVER_SENT_EVENTS)
 public EventOutput getMessage() {
    return eventOutput;
  }

  @POST
  @Consumes(MediaType.TEXT_PLAIN)
 public void sendMessage(String message) throws IOException {
    eventOutput.write(new OutboundEvent.Builder().name("custom-message").data(String.class, message).build());
  }
}

EventOutput 类是一个提供出站服务器端发送事件的通道。当我们从 getMessage() 方法返回 EventOutput 对象时,Jersey 实现保持连接打开,以便可以发送服务器端发送事件。这个类的单个实例与一个精确的 HTTP 连接相对应。

sendMessage() 方法使用 eventOutput.write() 方法写入消息。要写入服务器端发送事件,我们使用 OutboundEvent.Builder() 方法。将名称 "custom-message" 传递给这个 OutboundEvent.Builder() 方法,然后我们将消息对象传递给 build() 方法。消息对象包含我们示例中与推文相关的信息。

此外,可以使用OutboundEvent.Builder().id(id)将一个 ID 与之前未涵盖的 Server-sent Event 关联。

应用程序使用的异步 Servlet 客户端

在正常的请求响应场景中,每个请求都会保持一个线程运行,直到响应可用。当后端处理请求花费很长时间时,这会变成一个瓶颈,处理请求的线程等待后端完成准备所需的响应,因此无法处理任何新的传入请求。

解决这个问题的一种方法是将请求保存在一个集中队列中,并在线程可用时发送请求。调用startAsync()方法将请求/响应对存储在队列中,doGet()方法返回,调用线程可以被回收。

下面的部分讨论了使用 Servlet 进行异步请求处理的概念。

这里是应用程序的 Servlet 客户端代码:

@WebServlet(name = "TestClient", urlPatterns = {"/TestClient"}, asyncSupported = true)
public class TestClient extends HttpServlet {

  private final static String TARGET_URI = "http://localhost:8080/jersey-sse-twitter-sample/twittersse";

这是一个设置了urlPatterns={"/TestClient"}async-supported属性为 true 的 Servlet。async-supported属性指示容器这个 Servlet 将异步处理传入的请求,因此容器应该对处理线程的请求分配进行必要的修改。

下面的代码片段显示了处理GETPOST请求的service()方法的实现:

/**
* Processes requests for both HTTP
* <code>GET</code> and
* <code>POST</code> methods.
*
* @param request  servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException      if an I/O error occurs
*/
@Override
protected void service(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
  response.setContentType("text/html;charset=UTF-8");

  try {

 final AsyncContext asyncContext = request.startAsync();
 asyncContext.setTimeout(600000);
 asyncContext.addListener(new AsyncListener() {

      @Override
      public void onComplete(AsyncEvent event) throws IOException {
      }

      @Override
      public void onTimeout(AsyncEvent event) throws IOException {
        System.out.println("Timeout" + event.toString());
      }

      @Override
      public void onError(AsyncEvent event) throws IOException {
        System.out.println("Error" + event.toString());
      }

      @Override
      public void onStartAsync(AsyncEvent event) throws IOException {
      }
    });

 Thread t = new Thread(new AsyncRequestProcessor(asyncContext));
    t.start();

    } catch (Exception e) {
    e.printStackTrace();
  }

}

在前面的代码片段中,通过调用request.startAsync()方法获得AsyncContext对象的实例。

asyncContext.setTimeout(60000)方法表示 Servlet 异步操作的毫秒级超时。

使用asyncContext.addListener()方法向异步上下文添加一个AsyncListener接口的实现。

在请求上调用startAsync()方法后,当操作完成、出现错误或操作超时时,将发送一个AsyncEvent对象到AsyncListener接口的实现。如前所述,我们有一个实现AsyncListener接口的实现,可以执行以下方法:onComplete()onError()onTimeOut()onStartAsync()

下面的代码中显示的AsyncRequestProcessor类是线程的Runnable实例,执行实际工作。AsyncRequestProcessor类将EventSource对象注册为监听由之前提到的 JAX-RS ServerSentEventsResource.java发送的 Server-sent Events。当事件发生时,onEvent()回调被触发,并使用 JSONP 解析事件。

class AsyncRequestProcessor implements Runnable {

  private final AsyncContext context;

  public AsyncRequestProcessor(AsyncContext context) {
    this.context = context;
  }

  @Override
  public void run() {
    Client client = ClientBuilder.newClient();
    context.getResponse().setContentType("text/html");
    final javax.ws.rs.client.WebTarget webTarget;
    try {
      final PrintWriter out = context.getResponse().getWriter();
      webTarget = client.target(new URI(TARGET_URI));
      out.println("<html>");
      out.println("<head>");
      out.println("<title>Glassfish SSE TestClient</title>");
      out.println("</head>");
      out.println("<body>");
      out.println("<h1>");
      out.println("Glassfish tweets");
      out.println("</h1>");
      // EventSource eventSource = new EventSource(webTarget, executorService) {
        EventSource eventSource = new EventSource(webTarget) {
          @Override
          public void onEvent(InboundEvent inboundEvent) {
            try {
              //get the JSON data and parse it
              JSONObject jsonObject = JSONObject.fromObject(inboundEvent.getData(String.class,
              MediaType.APPLICATION_JSON_TYPE));
              //get the JSON data and parse it
              JsonReader jsonReader = Json.createReader (new ByteArrayInputStream(inboundEvent.getData(String.class,
              MediaType.APPLICATION_JSON_TYPE).getBytes()));
              JsonArray jsonArray = jsonReader.readArray();
              for (int i = 0; i <jsonArray.size(); i++) {
                JsonObject o = ((JsonObject)jsonArray.getJsonObject(i)) ;
                out.println( o.get("text"));
                out.println("<br>");
                out.println("Created at " + o.get("created_at"));
                out.println("<br>");

              }
              out.println("</p>");
              out.flush();
            } catch (IOException e) {
              e.printStackTrace();
            }
          }
        };
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }
}

如前所述的代码所示,我们使用JSR 353 Java API for JSON ProcessinginboundEvent#getData()方法创建一个JSonReader对象。JSONArray对象由jsonReader.readArray()方法返回。从数组中读取JsnObject对象,并显示推文信息。

与 Twitter 搜索 API 交互的 EJB

这是调用 Twitter 搜索 API 的 EJB 代码。此 EJB 有一个计时器,将定期调用 Twitter 搜索 API 以获取 GlassFish 的推文,并将结果以 JSON 格式返回。

@Stateless
@Named
public class TwitterBean {
}

@Stateless 注解表示这是一个无状态会话 Bean。

Twitter v1.1 API 使用 OAuth 提供对其 API 的授权访问。Twitter 允许应用程序代表应用程序本身(而不是代表特定用户)发出认证请求。有关OAuth的更多信息,请查看dev.twitter.com/docs/api/1.1/overview

要运行此演示,您需要一个 Twitter 账户,并根据以下链接中指定的信息创建一个应用程序:dev.twitter.com/docs/auth/oauth。请参阅包含示例的 Readme.txt,了解如何运行示例的说明。

以下代码使用了来自 twitter4j.org/en/index.htmltwitter4j API 来集成 Java 和 Twitter API。

这是连接到SEARCH_URL并获取推文的代码

/**
* Since twitter uses the v1.1 API we use twitter4j to get
* the search results using OAuth
* @return a JsonArray containing tweets
* @throws TwitterException
* @throws IOException
*/
public JsonArray getFeedData() throws TwitterException, IOException {

  Properties prop = new Properties();

  //load a properties file
  prop.load(this.getClass().getResourceAsStream("twitter4j.properties"));

  //get the property value and print it out
  String consumerKey = prop.getProperty("oauth.consumerKey");
  String consumerSecret= prop.getProperty("oauth.consumerSecret");
  String accessToken = prop.getProperty("oauth.accessToken");
  String accessTokenSecret = prop.getProperty("oauth.accessTokenSecret");
  ConfigurationBuilder cb = new ConfigurationBuilder();
  cb.setDebugEnabled(true)
  .setOAuthConsumerKey(consumerKey)
  .setOAuthConsumerSecret(consumerSecret)
  .setOAuthAccessToken(accessToken)
  .setOAuthAccessTokenSecret(accessTokenSecret);

  TwitterFactory tf = new TwitterFactory(cb.build());
  Twitter twitter = tf.getInstance();
  Query query = new Query("glassfish");
  QueryResult result = twitter.search(query);
  JsonArrayBuilder jsonArrayBuilder  = Json.createArrayBuilder();
  for (Status status : result.getTweets()) {
    jsonArrayBuilder
    .add(Json.createObjectBuilder().
    add("text", status.getText())
    .add("created_at", status.getCreatedAt().toString()));
  }
  return jsonArrayBuilder.build() ;
}

上述代码读取twitter4j.properties,并使用consumerKeyconsumerSecretaccessTokenaccessTokenSecret键创建一个ConfigurationBuilder对象。使用TwitterFactory API 创建 Twitter 对象的实例。创建一个Query对象,用于向 Twitter 发送带有关键字"glassfish"的搜索请求。twitter.search返回与指定查询匹配的推文。此方法调用search.twitter.com/search.json

一旦获得QueryResult对象,使用JsonArrayBuilder对象构建包含结果的 JSON 对象。有关 twitter4j API 的更多信息,请查看twitter4j.org/oldjavadocs/3.0.0/index.html

EJB Bean 有一个额外的方法,将调用 EJB 计时器。以下是 EJB 计时器代码,它将使用POST方法将这些从 Twitter 搜索 API 获取的推文发送到 REST 端点ServerSentEventsResource

private final static String TARGET_URI = "http://localhost:8080/jersey-sse-twitter-sample";

@Schedule(hour = "*", minute = "*", second = "*/10")
public void sendTweets() {

  Client client = ClientBuilder.newClient();
  try {
    WebTarget webTarget= client.target(new URI(TARGET_URI)) ;
    JsonArray statuses = null;

 statuses = getFeedData();
    webTarget.path("twittersse").request().post(Entity.json(statuses));
  }(catch Exception e) {
    e.printStackTrace();
  }
}

使用@Schedule注解来安排每 10 秒抓取一次推文。EJB 规范提供了更多关于@Schedule用法的详细信息。JsonArray对象statuses从前面章节中提到的getFeedData()方法获取内容。

使用TARGET_URI创建WebTarget,该 URL 为http://localhost:8080/jersey-sse-twitter-sample,应用程序部署于此。

webTarget.path("twittersse")方法指向前面提到的ServerSentEventsResource类,该类是 REST 资源。

使用request().post(Entity.text(message))方法将来自 Twitter 搜索 API 的推文作为文本实体发送。

这是事件序列:

  1. 用户从以下 URL 部署应用并调用 Servlet 客户端http://localhost:8080/jersey-sse-twitter-sample

  2. EJB 计时器每 10 秒被调度一次。

  3. EJB 计时器将每 10 秒调用一次 Twitter 搜索 API 以获取"glassfish"的推文,格式为 JSON。

  4. EJB 计时器使用POST请求将步骤中获得的数据发送到 JAX-RS ServerSentEventsResource类。

  5. JAX-RS 资源ServerSentEventsResource打开EventOutput通道,这是 Server-sent Events 的输出通道。

  6. 步骤 1 中的 Servlet 客户端打开了一个EventSource对象,该对象正在监听 Server-sent Events。

  7. Servlet 客户端使用 JSON-P API 解析 Twitter 动态。

  8. 最后,推文在浏览器中显示。

图书馆应用

图书馆应用是一个简单、自包含、基于现实生活的应用,演示了 HTML5 技术,如 WebSockets,并展示了如何使用 JAX-RS 动词,如何使用 JSON-P API 写入数据,以及如何利用处理资源的异步特性。为了保持一致性,应用包含使用简单 GUI 描述先前技术的组件,并且没有花哨的对话框或非常复杂的企业逻辑。

应用程序的部署方式

用于示例应用的构建系统是 Maven,示例可以部署在任何 Java EE 7 兼容的应用服务器中,特别是 GlassFish v4.0,它是 Java EE 规范的开放源代码参考实现。

项目的布局

项目目录布局遵循标准的 Maven 结构,以下表格简要说明:

来源代码 描述
src/main/java 此目录包含图书馆应用所需的所有源代码。
src/main/webapp 此目录包含 JavaScript 文件、HTML 文件和WEB-INF/web.xml文件。

图书馆应用 GUI

图书馆应用由一个屏幕组成,该屏幕作为展示不同数据表示和收集输入表单的载体。屏幕如下截图所示:

图书馆应用 GUI

使用屏幕,用户可以进行以下操作:

  1. 浏览书籍集合。

  2. 搜索一本书。

  3. 检出书籍。

  4. 归还一本书。

  5. 预订一本书。

以下表格显示了用户执行的操作、幕后发生的事情的详细信息以及处理请求涉及的 API 和技术:

动作 使用的 API 和技术
浏览书籍集合 此任务使用 JAX-RS GET动词获取图书馆中的书籍集合。它使用 JSON-P API 将数据写入 JSON 格式。我们使用 JAX-RS MessageBodyWriter类的实现,该类知道如何将自定义类序列化为 JSON 输出。
借阅书籍 当从图书馆借阅书籍时,它将从图书馆拥有的书籍集合中减少。这个任务展示了 JAX-RS 动词 DELETE 的使用,并将书籍从集合中删除。
归还书籍 当书籍归还到图书馆时,它将被添加到图书馆拥有的书籍集合中。这个任务展示了 JAX-RS 动词 POST 的使用,并将书籍添加到集合中。
预订书籍 当一本书被预订时,图书馆应用程序应该通知当前拥有这本书的其他用户归还它。一旦书籍归还,应向请求书籍的用户发送通知。这是一个异步操作。这个任务展示了 JAX-RS 资源异步处理的使用。

应用程序交互监控

有一个面板会显示发送到端点的查询,此外,我们还将显示端点返回的输出。

对图书馆应用程序的详细分析

在对应用程序应该做什么的初步介绍之后,让我们进一步剖析它,并研究构建此应用程序的每个单独组件。

以下是将介绍应用程序详细信息的顺序:

  • web.xml

  • 我们应用程序中 Application 子类的实现

  • 我们应用程序中使用的 JAX-RS 实体提供者

  • HTML 页面

  • 以下功能的 JavaScript 片段和 JAX-RS 资源方法:

    • 浏览书籍集合

    • 搜索书籍

    • 借阅书籍

    • 归还书籍

    • 预订书籍

web.xml

要设置应用程序,按照以下方式配置 servlet 部署描述符 web.xml

<servlet>
  <servlet-name>org.sample.library.BookApplication</servlet-name>
  <init-param>
    <param-name>javax.json.stream.JsonGenerator.prettyPrinting</param-name>
    <param-value>true</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>

<welcome-file-list>
  <welcome-file>
    index.html
  </welcome-file>
</welcome-file-list>
<servlet-mapping>
  <servlet-name>org.sample.library.BookApplication</servlet-name>
  <url-pattern>/app/*</url-pattern>
</servlet-mapping>

在前面的片段中,我们定义了一个用于接收 JAX-RS 应用程序 BookApplication 子类的 servlet。URL 模式是 /app/*

Application 子类

这里是 BookApplication 类的片段,该片段在 web.xml 描述中提到。

public class BookApplication extends Application {

  @Override
  public Set<Class<?>> getClasses() {
    Set<Class<?>> classes = new HashSet<Class<?>>();
    classes.add(BooksResource.class);
    classes.add(BookCollectionWriter.class);
    classes.add(BookWriter.class);
    return classes;
  }
}

BookApplication 类扩展了 JAX-RS 的 Application 类。在 getClasses() 方法实现中,以下内容被注册:

  • BookResource.class

  • BookCollectionWriter.class

  • BookWriter.class

BookResource 类将在接下来的几节中详细介绍,包括 JavaScript 的每个功能;BookResource 类的相应方法将进行解释。

BookCollectionWriter 类是 MessageBodyWriter 接口的一个实现,它接收一个 List<Book> 对象并将其序列化为 JSON 格式。为了产生 application/json 编码的输出,BookCollectionWriter 类使用了 JSON-P API。

BookWriter类提供了序列化用户定义的Book类的功能,如下节所示。Book类有诸如书籍名称、作者和 ISBN 等字段。使用这个BookWriter类,可以将这个Book类转换为资源中指定的格式,例如"text/plain""application/json"

JAX-RS 实体提供者:BookCollectionWriter

与前面章节中提到的BookWriter类类似,在示例中还有一个名为BookCollectionWriter的类;这个类用于序列化书籍列表。以下是BookCollectionWriter类中writeTo()方法的实现:

@Override
public void writeTo(List<Book> books, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
  StringWriter writer = new StringWriter();
 if (mediaType.equals(MediaType.APPLICATION_JSON_TYPE)) {
    JsonGenerator generator = Json.createGenerator(writer);
    Map<String, Object> configs;
    configs = new HashMap<String, Object>(1);
    configs.put(JsonGenerator.PRETTY_PRINTING, true);

    generator.writeStartArray();
    for (Book book: books) {
      generator.writeStartObject()
      .write("Name", book.getName())
      .write(" ISBN", book.getIsbn())
      .write("Author",book.getAuthor()) .writeEnd();

    }
    generator.writeEnd();
    generator.close();
    entityStream.write(writer.toString().getBytes());
 } else if (mediaType.equals(MediaType.TEXT_PLAIN_TYPE)) {
    StringBuilder stringBuilder = new StringBuilder("Book ");
    for (Book book: books) {
      stringBuilder.append(book.toString()).append("\n");
    }
    entityStream.write(stringBuilder.toString().getBytes());

  }
}

上述代码执行媒体类型过滤;如果mediaType参数等于MediaType.APPLICATION_JSON_TYPE,则使用 JSON-P API 创建一个JsonGenerator对象。使用JsonGenerator类的writeStartArray()writeStartObject()方法,写入 JSON 对象数组。

如果mediaType参数等于MediaType.TEXT_PLAIN_TYPE,则返回书籍的字符串表示形式。

HTML 页面

如您所回忆的,当应用程序在浏览器中启动时,您将看到index.html屏幕。让我们看一下index.html文件的源代码:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Library App</title>
    <script src="img/main.js">
    </script>
  </head>
  <body>
    <h1 id="helloMessage">
    </h1>

    Please enter the following details:
    <p>
      Book Name:
      <input type="text" value="Game of thrones" id="bookName"/>
    </p>
    <br>
    <button onclick="search_onclick()">Search</button>
    <button onclick="checkout_onclick()">Checkout</button>
    <button onclick="return_onclick()">Return</button>
    <button onclick="hold_onclick()">Hold</button>
    <button onclick="browse_onclick()">Browse Collection</button>

    <h2>Book Information</h2>
    <h3>JAX-RS query sent by the Application:</h3>

 <div id="query" style="border: 1px solid black; color: black; height: 6em; width: 80%"></div>
    <h3>Output from the JAX-RS query</h3>
 <div id="output" style="border: 1px solid black; color: black; height: 18em; width: 80%"></div>
  </body>
</html>

这是一种标准的 HTML,它使用一个名为main.js的外部 JavaScript 文件来导入以下功能:

  • 浏览书籍集合

  • 搜索书籍

  • 检索书籍

  • 归还书籍

  • 预订书籍

突出的div元素queryoutput显示了 JAX-RS 查询和页面上的输出。每个按钮都有一个与它关联的onclick()事件,该事件调用 JavaScript 中的一个函数。每个函数将在下一节中详细介绍。

浏览书籍集合

当用户点击 HTML 页面上的浏览集合按钮时,会检查输入,然后在 JavaScript 中调用sendBrowseRequest()函数。

使用 JavaScript

这里是sendBrowseRequest()的代码片段:

function sendBrowseRequest( ) {
  var req = createRequest(); // defined above
  // Create the callback:
  req.onreadystatechange = function() {
    if (req.readyState == 4) {
      document.getElementById("query").innerHTML="GET app/library/books" ;
      document.getElementById("output").innerHTML=req.responseText;
    }
  }
  req.open("GET","app/library/books" ,true);
  req.send(null);
}

createRequest()函数用于创建XMLHttpRequest对象,如第二章中所述,WebSockets 和服务器端事件。发送到 JAX-RS 资源的请求是一个带有 URI /app/library/booksGET请求(我们将在下一节中介绍 JAX-RS 资源)。当XMLHttpRequest对象的readyState值为4时,表示响应已完成,我们可以获取数据。在我们的示例中,我们使用代码片段document.getElementById("output").innerHTML=req.responseText;显示responseText

GET 请求的 JAX-RS 资源方法

这里是GET请求的代码片段:

@GET
@Path("books")
@Produces({MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON})
public List<Book> browseCollection() {
  return bookService.getBooks();
} 

这是一个非常简单的方法,它将使用我们之前提到的BookCollectionWriter类来输出List<Book>对象,以 JSON 格式或纯文本格式。

搜索书籍

当用户点击 HTML 页面上的搜索按钮时,在 JavaScript 中调用sendSearchWSRequest()函数。

使用 JavaScript

sendSearchWSRequest()函数展示了我们应用程序中的 WebSocket 功能。在 JavaScript 中初始化 WebSocket URI 如下:

var wsUri = "ws://localhost:8080/libraryApp/app/websockets";
function sendSearchWSRequest(book) {
  websocket.send(book);
  console.log("Searching for: " + book);
}

sendSearchWSRequest()函数使用 WebSocket JavaScript API 将字符串book名称发送到下节中所示的BookWebSocket类。

WebSockets 端点

这里是 WebSockets 的ServerEndpoint注解类BookWebSocket的代码片段:

@ServerEndpoint(value="/app/websockets")
public class BookWebSocket {
  @OnMessage
  public String searchBook(String name) {
    return "Found book " + name;
  }
}

BookWebSocket是一个带有@ServerEndpoint注解的 POJO,初始化为/app/websockets的 URI。searchBook()方法上的@OnMessage注解会在 WebSocket 服务器端点接收到消息时调用此方法。为了示例的简单性,WebSocket 端点仅返回一个包含书籍名称的字符串。

检索一本书

当用户点击 HTML 页面上的借阅按钮时,会检查输入,然后在 JavaScript 中调用sendCheckoutRequest()函数。

使用 JavaScript

这里是sendCheckoutRequest()函数的代码片段:

function sendCheckoutRequest( book) {
  var req = createRequest(); // defined above
  ;
  // Create the callback:
  req.onreadystatechange = function() {

    if (req.readyState == 4) {
      document.getElementById("query").innerHTML="DELETE app/library/book/" + encodeURI(book.trim());
      document.getElementById("output").innerHTML=req.responseText;

    }
  }
  req.open("DELETE","app/library/book/" + book,true);
  req.send(null);
}

发送到 JAX-RS 资源的请求是一个放置在/app/library/book/ URI 上的DELETE请求。我们将在下一节中介绍 JAX-RS 资源。

用于DELETE请求的 JAX-RS 资源方法

这里是DELETE请求的代码片段:

@DELETE
@Path("book/{name}")
@Produces({MediaType.TEXT_PLAIN })
@Consumes({MediaType.TEXT_PLAIN })
public Book checkoutBook(@PathParam("name") String nameOfBook) {
  return bookService.deleteBook(nameOfBook);

}

这是一个非常简单的函数,如果集合中存在书籍,则会删除书籍,并使用之前介绍的BookWriter类发送书籍详情。

归还一本书

当用户点击 HTML 页面上的归还按钮时,会检查输入,然后在 JavaScript 中调用sendReturnRequest()函数。

使用 JavaScript

这里是sendReturnRequest()函数的代码片段:

function sendReturnRequest( book) {
  var req = createRequest(); // defined above
  ;
  // Create the callback:
  req.onreadystatechange = function() {

    if (req.readyState == 4) {
      document.getElementById("query").innerHTML="POST app/library/book/" + encodeURI(book.trim());
      document.getElementById("output").innerHTML=req.responseText;

    }
  }
  req.open("POST","app/library/book/" + book,true);
  req.send(null);
}

发送到 JAX-RS 资源的请求是一个以app/library/book/为目标 URI 的POST请求。

用于POST请求的 JAX-RS 资源方法

这里是POST请求的代码片段:

@POST
@Path("book/{name}")
@Produces({MediaType.TEXT_PLAIN })
@Consumes({MediaType.TEXT_PLAIN })
public String returnBook(@PathParam("name") String nameOfBook)      {

  return "Successfully returned Book " + nameOfBook;
}

预约一本书

当用户点击 HTML 页面上的预约按钮时,会检查输入,然后在 JavaScript 中调用sendHoldRequest()函数。

使用 JavaScript

这里是sendHoldRequest()函数的代码片段:

function sendHoldRequest( book) {
  var req = createRequest(); // defined above
  ;
  // Create the callback:
  req.onreadystatechange = function() {

    if (req.readyState == 4) {
      document.getElementById("query").innerHTML="POST app/library/hold/" + encodeURI(book.trim());
      document.getElementById("output").innerHTML=req.responseText;

    }
  }
  req.open("POST","app/library/hold/" + book,true);
   req.send(null);

}

向位于app/library/hold/ URI 的 JAX-RS 资源发送一个POST请求。该资源将在下一节中介绍。

用于异步POST请求的 JAX-RS 资源方法

这里是放置书籍预约的 JAX-RS 资源方法。这是一个异步资源,在第四章中介绍,JSON 和异步处理

/**
* Asynchronously reply to placing a book on hold after sleeping for sometime
*
*/
@POST
@Produces({MediaType.TEXT_PLAIN})
@Path("hold/{name}")
public void asyncEcho(@PathParam("name") final String name,  @Suspended final AsyncResponse ar) {
  TASK_EXECUTOR.submit(new Runnable() {

    public void run() {
      try {
        Thread.sleep(SLEEP_TIME_IN_MILLIS);
      } catch (InterruptedException ex) {
        ar.cancel();
      }
      ar.resume("Placed a hold for " + name);
    }
  });
}

AsyncResponse 类型的参数 ar 与 Servlet 3.0 规范中的 AsyncContext 类类似,它简化了异步请求的执行。在这个例子中,请求被暂停了特定的时间,响应通过 AsyncResponse.resume() 方法推送到客户端。

单例 EJB BookService

下面是存储关于书籍详细信息的单例 EJB 的代码:

@Singleton
public class BookService {

  private static final HashMap<String,Book> books = new HashMap<String,Book>();

  public static void addBook(Book book) {
    books.put(book.getName(), book);
  }

  public static int getSize() {
    return  books.size();
  }

  public static Book deleteBook(String isbn) {
    return books.remove(isbn);
  }

  public static List<Book> getBooks() {
    return new ArrayList<Book>(books.values());
  }

  public BookService() {
    // initial content
    addBook( new Book("Java EE development using GlassFish Aplication Server","782345689","David Heffinger"));
    addBook( new Book("Java 7 JAX-WS Web Services","123456789","Deepak Vohra"));
    addBook( new Book("Netbeans IDE7 CookBook","2234555567","Rhawi Dantas"));
    addBook( new Book("Getting Started with RESTful WebServices","11233333","Bhakti Mehta, Masoud Kalali"));

  }
}

因此,我们已经详细了解了使用不同 JAX-RS 2.0、WebSockets 和 JSON-P API 的库应用程序。

摘要

本章介绍了两个实际的 RESTful Web 服务示例。一开始,使用事件通知示例,我们演示了如何使用服务器端发送事件(Server-sent Events)与异步处理 servlet,以及服务器如何在事件发生时将数据推送到客户端。

在图书馆应用程序中,我们介绍了 JAX-RS API 以及自定义消息体读取器和写入器。我们还演示了 JSON-P API 的使用。该库应用程序展示了如何从 JavaScript 客户端使用 WebSockets 并向 WebSockets 端点发送消息。

posted @ 2025-09-11 09:45  绝不原创的飞龙  阅读(15)  评论(0)    收藏  举报