JAX-RS 2.0 REST客户端编程实例

2014/01/28 | 分类: 基础技术教程 | 0 条评论 | 标签: JAX-RSRESTFUL

分享到:3
本文由 ImportNew - 靳禹 翻译自 hascode。欢迎加入翻译小组。转载请见文末要求。

导读

JAX-RS 2.0 又称 JSR 339 不仅定义了一套用于构建 RESTful 网络服务的 API,同时也通过增强客户端 API 功能简化了REST 客户端的构建过程。

JAX-RS: Java API for RESTful Web Services是一个Java编程语言应用程序接口,支持按照 表象化状态转变 (REST)架构风格创建Web服务Web服务[1]. JAX-RS使用了Java SE 5引入的Java 标注来简化Web服务客户端和服务端的开发和部署 [wikipedia]。

在下面的教程中,我们将为一个预先设置好的 REST 服务构建一个客户端,并在这个过程中探索新的构建选项。例如,如何处理同步或者异步的请求,如何给一个请求注册一个回调,如何指定调用对象来构建一个请求使得请求可以被延迟执行。再或者比如,如何使用客户端请求和相应的过滤方法来过滤客户端与服务器之前的通信。

我们开始吧

对于想要重建下述客户端例子的读者,我已经使用 Maven 创建好了一个完整的 RESTful 网络服务程序。程序中有内嵌的应用程序服务器,以及一个可独立运行的应用服务器 (war-file 可以通过下文中的下载地址获取)。

请根据下面的一系列命令来下载并启动 REST 服务器 (下载所有依赖可能会耗费些时间……):

1
clone https://bitbucket.org/hascode/jaxrs2-client-tutorial.git && cd jaxrs2-client-tutorial && make rest-server

现在,让我们先来看看这个 REST 服务的一些实现细节和我们的客户端示例中要用到的对象。如果你对这些没什么兴趣,大可以略过服务端的细节直接去看客户端示例。

REST 服务

下面的代码就是个客户端提供服务的 REST 服务。这里的 BookRepository 就是一个由 @Singleton 和 @Startup 修饰的简单 session bean,这个 bean 用来模拟存储或获取 Book Entity。服务对外提供了保存一本书、删除一本书、根据标识查找书籍和获取所有可用书籍的接口。当一本书被保存在服务端时,服务器会为该书生成一个 id,并会返回一个 entity 或一组 entity 的 JSON 数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.hascode.tutorial.jaxrs.server;
 
import java.util.List;
 
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
 
import com.hascode.tutorial.jaxrs.entity.Book;
 
@Stateless
@Path("/book")
public class BookStoreService {
    @EJB
    private BookRepository bookRepository;
 
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response saveBook(final Book book) {
        Book bookPersisted = bookRepository.saveBook(book);
        return Response.ok(bookPersisted).build();
    }
 
    @DELETE
    @Path("/{id}")
    public Response deleteBook(final @PathParam("id") String id) {
        bookRepository.deleteBook(id);
        return Response.ok().build();
    }
 
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response getAll() {
        List<Book> books = bookRepository.getAll();
        GenericEntity<List<Book>> bookWrapper = new GenericEntity<List<Book>>(books) {};
        return Response.ok(bookWrapper).build();
    }
 
    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getById(final @PathParam("id") String id) {
        Book book = bookRepository.getById(id);
        return Response.ok(book).build();
    }
}

备注:我修改了应用服务器,以便使用 Jackson 提供的服务发现机制处理 JSON 数据。

Book Entity

下面代码中的 bean 就是贯穿本教程的 Book Entity,它包含id、书名、价格和出版日期属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.hascode.tutorial.jaxrs.entity;
 
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Calendar;
 
public class Book implements Serializable {
    private static final long serialVersionUID = 1L;
 
    private String id;
    private String title;
    private BigDecimal price;
    private Calendar published;
 
    // getter+setter..
}

创建并绑定一个客户端

我们可以创建一个 REST 客户端,将其绑定到一个特定的目标 URL 上。并且为它指定专属的、参数化的路径。具体步骤如下:

  • 通过 ClientBuilder 获取一个客户端的引用:Client client = ClientBuilder.newClient();
  • 使用 target() 方法将客户端绑定到 REST 服务上提供的某个 URL:client.target(“http://localhost:8080/myrestservice”);
  • 通过 path() 和 resolveTemplate() 方法来处理动态的 URL 路径参数:client.target(..).path(“{id}”).resolveTemplate(“id”, someId);
  • 使用 request() 函数来初始化一个请求并用后续的 post 或者 get 等方法来指定请求的类型,例如:client.target(..).request().get();
  • 每一步都提供了多样的可选择的参数和配置选项,稍后的教程中我将用到其中的一些配置像异步请求、回调处理、还有过滤器注册和特性类等。

现在,让我们先看一些具备说明性的例子。

客户端例子

由于我把所有客户端示例都融进了 jUnit 和 Hamcrest 驱动的测试用例,因此下面的代码实际上在每一个测试用例中都有使用。不过为了让文章尽量简练,重复代码将在后面的代码示例中省略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final String REST_SERVICE_URL = "http://localhost:8080/tutorial/rs/book";
 
private static final String TITLE = "One big book";
private static final BigDecimal PRICE = new BigDecimal("20.0");
private static final GregorianCalendar PUBLISHED = new GregorianCalendar(
        2013, 12, 24);
 
Client client = ClientBuilder.newClient().register(JacksonFeature.class);
 
public Book mockBook() {
    Book book = new Book();
    book.setTitle(TITLE);
    book.setPrice(PRICE);
    book.setPublished(PUBLISHED);
    return book;
}

唯一值得注意的是,我在客户端运行时中加入了 Jackson 框架,因此可以通过 javax.ws.rs.client.ClientBuilder 来获取客户端实例。

Maven 整合

所有代码示例运行都需要用到下面依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
    <groupId>org.glassfish.jersey.core</groupId>
    <artifactId>jersey-client</artifactId>
    <version>2.5</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>2.5</version>
</dependency>

基础操作

下面的示例中我们首先将一个书本实体的信息序列化成 JSON 格式,通过 POST 请求发送到服务端来保存这本书。

之后,我们使用客户端提供的 path() 和 resolveTemplate() 方法通过匹配服务端返回值的协议来获取该本书的标识。

第三步, 我们获取所有可用图书的列表,并在最后删除掉刚才保存的那本书。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Test
public void crudExample() {
    // 1. Save a new book
    Book book = mockBook();
    Book bookPersisted = client
            .target(REST_SERVICE_URL)
            .request()
            .post(Entity.entity(book, MediaType.APPLICATION_JSON),
                    Book.class);
 
    String bookId = bookPersisted.getId();
    assertThat(bookId, notNullValue());
 
    // 2. Fetch book by id
    Book book2 = client.target(REST_SERVICE_URL).path("/{bookId}")
            .resolveTemplate("bookId", bookId).request().get(Book.class);
    assertThat(book2, notNullValue());
    assertThat(book2.getTitle(), equalTo(TITLE));
    assertThat(book2.getPrice(), equalTo(PRICE));
    assertThat(book2.getPublished().getTime(), equalTo(PUBLISHED.getTime()));
 
    // 3. Fetch all books
    GenericType<List<Book>> bookType = new GenericType<List<Book>>() {
    }; // generic type to wrap a generic list of books
    List<Book> books = client.target(REST_SERVICE_URL).request()
            .get(bookType);
    assertThat(books.size(), equalTo(1));
 
    // 4. Delete a book
    client.target(REST_SERVICE_URL).path("/{bookId}")
            .resolveTemplate("bookId", bookId).request().delete();
    List<Book> books2 = client.target(REST_SERVICE_URL).request()
            .get(bookType);
    assertThat(books2.isEmpty(), equalTo(true));
}

异步处理

只要给请求构造器加一个简单的 async() 方法,我们就可以使用 Java 的 Future API 提供的多种途径来异步地处理请求。

下面的例子中,我们在第一个请求中添加一本书,然后再删除它。最后获取所有可用图书的列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
public void asyncExample() throws Exception {
    Book book = mockBook();
 
    Future<Book> fb = client
            .target(REST_SERVICE_URL)
            .request()
            .async()
            .post(Entity.entity(book, MediaType.APPLICATION_JSON),
                    Book.class);
 
    Book bookPersisted = fb.get();
 
    String bookId = bookPersisted.getId();
    assertThat(bookId, notNullValue());
 
    client.target(REST_SERVICE_URL).path("/{bookId}")
            .resolveTemplate("bookId", bookId).request().async().delete()
            .get();
 
    Future<List<Book>> bookRequest = client.target(REST_SERVICE_URL)
            .request().async().get(new GenericType<List<Book>>() {
            });
    List<Book> books2 = bookRequest.get();
    assertThat(books2.isEmpty(), equalTo(true));
}

发起回调

在客户端与服务器通信过程中,我们还有另一种方式可以对服务器的相应进行修改,那就是在请求中加入一个 InvocationCallback 回调处理。

可以看到,下面代码段中有着很多缩进那部分就是我们的回调函数了,这些回调可以打印保存成功的图书的完整信息,或者在出现错误的情况下则打印错误和堆栈信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Test
public void invocationCallbackExample() throws Exception {
    Book book = mockBook();
    client.target(REST_SERVICE_URL)
            .request()
            .async()
            .post(Entity.entity(book, MediaType.APPLICATION_JSON),
                    new InvocationCallback<Book>() {
                        @Override
                        public void completed(final Book bookPersisted) {
                            System.out.println("book saved: "
                                    + bookPersisted);
                            assertThat(bookPersisted.getId(),
                                    notNullValue());
                        }
 
                        @Override
                        public void failed(final Throwable throwable) {
                            throwable.printStackTrace();
                        }
                    }).get();
 
    client.target(REST_SERVICE_URL).request().async()
            .get(new InvocationCallback<List<Book>>() {
                @Override
                public void completed(final List<Book> books) {
                    System.out.println(books.size() + " books received");
                    assertThat(books.size(), greaterThanOrEqualTo(1));
                }
 
                @Override
                public void failed(final Throwable throwable) {
                    throwable.printStackTrace();
                }
            }).get();
}

延迟调用 / 请求构建

通过 javax.ws.rs.client.Invocation 类,我们可以先构建一个请求而不用即时发送。这个请求可以是同步的, 也可以是异步的。

在下面的示例中,我们构建了两个调用但并不马上使用—— 一个请求用来保存图书,另一个请求则是获取所有可用的图书。然后,我们在后面调用时才使用这两个构建好的请求。

我们应当使用 invoke() 方法来同步地调用一个请求。当需要使用异步请求时,则需要用 submit() 方法——两种调用都会返回一个 javax.ws.rs.core.Response 对象。如果调用者在调用参数中给定了返回实体的类,则上述方法会返回该类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void requestPreparationExample() throws Exception {
    Book book = mockBook();
    Invocation saveBook = client.target(REST_SERVICE_URL).request()
            .buildPost(Entity.entity(book, MediaType.APPLICATION_JSON));
    Invocation listBooks = client.target(REST_SERVICE_URL).request()
            .buildGet();
 
    Response response = saveBook.invoke();
    Book b1 = response.readEntity(Book.class);
 
    // alternative: Book b1 = saveBook.invoke(Book.class);
    assertThat(b1.getId(), notNullValue());
 
    // async invocation
    Future<List<Book>> b = listBooks.submit(new GenericType<List<Book>>() {
    });
    List<Book> books = b.get();
    assertThat(books.size(), greaterThanOrEqualTo(2));
}

客户端请求过滤器

JAX-RS 允许我们使用请求过滤器来截获客户端发送到服务器的请求。

为了达成这个目标,只需要实现 javax.ws.rs.client.ClientRequestFilter 这个接口。当创建客户端时,使用客户端的 register() 方法将 ClientRequestFilter 的具体实现注册到客户端中。

javax.ws.rs.client.ClientRequestContext 对象将赋予访问信息请求足够的权限。

下面就是一个客户端请求过滤的例子。这个例子中,所有客户端发出的 POST 请求中如果包含书籍实体,则书籍价格都会被这个过滤器修改(虽然这不是一个好的实际示例)。对价格的修改则依据相应的税率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.hascode.tutorial.client;
 
import java.io.IOException;
import java.math.BigDecimal;
 
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
 
import com.hascode.tutorial.jaxrs.entity.Book;
 
public class TaxAdjustmentFilter implements ClientRequestFilter {
    public static final BigDecimal TAX_RATE = new BigDecimal("2.5");
 
    @Override
    public void filter(final ClientRequestContext rc) throws IOException {
        String method = rc.getMethod();
        if ("POST".equals(method) && rc.hasEntity()) {
            Book book = (Book) rc.getEntity();
            BigDecimal priceWithTaxes = book.getPrice().multiply(TAX_RATE);
            book.setPrice(priceWithTaxes);
            rc.setEntity(book);
        }
    }
 
}

在我们的测试用例中,只要把这个过滤器注册到客户端上,随后就会看到:保存书籍时候,书本的价格就会根据税率进行的调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void clientRequestFilterExample() {
    Book book = mockBook();
 
    Client client = ClientBuilder.newClient()
            .register(JacksonFeature.class)
            .register(TaxAdjustmentFilter.class);
    Book bookPersisted = client
            .target(REST_SERVICE_URL)
            .request()
            .post(Entity.entity(book, MediaType.APPLICATION_JSON),
                    Book.class);
 
    String bookId = bookPersisted.getId();
    assertThat(bookId, notNullValue());
    assertThat(bookPersisted.getPrice(),
            equalTo(PRICE.multiply(TaxAdjustmentFilter.TAX_RATE)));
 
}

客户端响应过滤器

为了获得对服务器相应的控制,有一个十分类似的办法:客户端相应过滤器。

同样地,只要实现 javax.ws.rs.client.ClientResponseFilter 这个接口,就能够修改或者截获服务器返回的响应。

下面这个响应过滤器能够将一些 HTTP 响应头打印到标准输出(STDOUT):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.hascode.tutorial.client;
 
import java.io.IOException;
import java.util.List;
import java.util.Map.Entry;
 
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientResponseContext;
import javax.ws.rs.client.ClientResponseFilter;
 
public class ClientResponseLoggingFilter implements ClientResponseFilter {
 
    @Override
    public void filter(final ClientRequestContext reqCtx,
            final ClientResponseContext resCtx) throws IOException {
        System.out.println("status: " + resCtx.getStatus());
        System.out.println("date: " + resCtx.getDate());
        System.out.println("last-modified: " + resCtx.getLastModified());
        System.out.println("location: " + resCtx.getLocation());
        System.out.println("headers:");
        for (Entry<String, List<String>> header : resCtx.getHeaders()
                .entrySet()) {
            System.out.print("\t" + header.getKey() + " :");
            for (String value : header.getValue()) {
                System.out.print(value + ", ");
            }
            System.out.print("\n");
        }
        System.out.println("media-type: " + resCtx.getMediaType().getType());
    }
 
}

要使用这个过滤器,只需要把它注册到我们的客户端程序中:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void clientResponseFilterExample() {
    Book book = mockBook();
 
    Client client = ClientBuilder.newClient()
            .register(JacksonFeature.class)
            .register(ClientResponseLoggingFilter.class);
    client.target(REST_SERVICE_URL)
            .request()
            .post(Entity.entity(book, MediaType.APPLICATION_JSON),
                    Book.class);
}

使用内嵌的 GlassFish 服务,POST 请求将有如下结果:

1
2
3
4
5
6
7
8
9
10
11
status: 200
date: Sat Dec 28 18:50:16 CET 2013
last-modified: null
location: null
headers:
 Date :Sat, 28 Dec 2013 17:50:16 GMT,
 Transfer-Encoding :chunked,
 Content-Type :application/json,
 Server :GlassFish Server Open Source Edition 3.1,
 X-Powered-By :Servlet/3.0 JSP/2.2 (GlassFish Server Open Source Edition 3.1 Java/Oracle Corporation/1.7),
media-type: application

译注:GlassFish是SUN所研发的开放源代码应用服务器,GlassFish以Java编写以增加跨平台性[wikipedia]。

教程源码

欢迎下载本教程中的源码,你可以用 Git 来 Fork 或者直接 Clone:Bitbucket代码仓库

下载 war-File REST 服务器

你可以从这里下载 war-file 然后运行自己的 RESTful 服务:https://bitbucket.org/hascode/jaxrs2-client-tutorial/downloads

JAX-RS 1.0 and JAX-B

如果你对旧版本的协议感兴趣,这篇文章正是你需要的。

 

JAX-RS 2.0 REST客户端编程实例的更多相关文章

  1. NIO Socket编程实例

    1.阻塞模式实例 NIOUtil类,用来通过SOcket获取BufferedReader和PrintWriter. package IO; import java.io.BufferedReader; ...

  2. linux下socket编程实例

    linux下socket编程实例一.基本socket函数Linux系统是通过提供套接字(socket)来进行网络编程的.网络的socket数据传输是一种特殊的I/O,socket也是一种文件描述符.s ...

  3. Java NIO Socket编程实例

    各I/O模型优缺点 BIO通信模型 BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接 线程池I/O编程 假如所有可用 ...

  4. 4 kafka集群部署及kafka生产者java客户端编程 + kafka消费者java客户端编程

    本博文的主要内容有   kafka的单机模式部署 kafka的分布式模式部署 生产者java客户端编程 消费者java客户端编程 运行kafka ,需要依赖 zookeeper,你可以使用已有的 zo ...

  5. AMQ学习笔记 - 02. JMS客户端编程模型

    概述 客户端编程模型,是讲如何使用JMS API实现Java应用程序和JMS Provider的通信. 消息传送模式 消息传送模式又称为消息传送域,JMS API定义了两种模式:PTP和Pub/Sub ...

  6. php网络编程实例

    php网络编程实例 一.总结 一句话总结: socket_create():创建socket socket_bind():绑定IP和端口 socket_listen():监听客户端信息 <?ph ...

  7. Redis系列(三)-Redis发布订阅及客户端编程

    阅读目录 发布订阅模型 Redis中的发布订阅 客户端编程示例 0.3版本Hredis 发布订阅模型 在应用级其作用是为了减少依赖关系,通常也叫观察者模式.主要是把耦合点单独抽离出来作为第三方,隔离易 ...

  8. PHP多进程编程实例

    这篇文章主要介绍了PHP多进程编程实例,本文讲解的是在Linux下实现PHP多进程编程,需要的朋友可以参考下 羡慕火影忍者里鸣人的影分身么?没错,PHP程序是可以开动影分身的!想完成任务,又觉得一个进 ...

  9. 【转】Android Https服务器端和客户端简单实例

    转载地址:http://blog.csdn.net/gf771115/article/details/7827233 AndroidHttps服务器端和客户端简单实例 工具介绍 Eclipse3.7 ...

随机推荐

  1. ActiveMQ之Queue

    Queue实现的是点到点模型,在以下的例子中,启动2个消费者共同监听一个Queue,然后循环给这个Queue发送多个消息. 代码如下: public class QueueTest { /** * @ ...

  2. 驱动makefile

    1 ifeq ($(KERNELRELEASE),)  2 CURRENT_PATH=$(shell pwd)  3 #KERNEL_DIR:=/lib/modules/$(shell uname - ...

  3. 【quartz】 数据库方式管理任务

    public static void Run(bool inClearJobs, bool inScheduleJobs) { var properties = new NameValueCollec ...

  4. docker 感性体验

    Docker 1.0正式发布!1.0 版本包含很多新特性,这也是 Docker 的首个产品级的版本.从今天开始,你将会一直听到一个新的概念 —— Docker as a platform ,其组件包括 ...

  5. java & xml parser

    参考: JDK8 API: http://docs.oracle.com/javase/8/docs/api/ DOM: http://www.w3.org/TR/2004/REC-DOM-Level ...

  6. Careercup - Facebook面试题 - 5188884744896512

    2014-05-02 07:18 题目链接 原题: boolean isBST(const Node* node) { // return true iff the tree with root 'n ...

  7. android开发,assets下面的资源文件不会变化/改动

    我再调试asserts下面的资源文件,发现我改动assets下面的文件内容,在真机上测试的时候还是最原先的内容,没有变,后来,卸载,重装就ok了. 原因: assets下面的资源文件,若覆盖重装,则里 ...

  8. ajax正确返回数据,却进入了error分支

    .net 开发: $.ajax({ type: "POST", //post没有数据量限制 url: "ashx/PostHandle.ashx", data: ...

  9. android聊天,存储聊天记录sqlite

    项目中有聊天模块,需要用到打开activity的时候初始化聊天记录的情况.大致情况如下: 辅助类:ChatSQLiteHelper   在第一次时会调用oncreate方法(判断的标准是schedul ...

  10. C++文件操作之 seekg/seekp/tellg/tellp

    问题描述: C++文件操作之 tellg/tellp/seekg/seekp 的使用 问题解决: (1)seekg/tellg/seekp/tellp 使用 tellp用于ostream调用,用来&q ...