您的位置:首页 > 编程语言

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

2017-08-16 10:31 567 查看
导读

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

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

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

我们开始吧

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

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

1
clonehttps://bitbucket.org/hascode/jaxrs2-client-tutorial.git&&cdjaxrs2-client-tutorial&&makerest-server
现在,让我们先来看看这个REST服务的一些实现细节和我们的客户端示例中要用到的对象。如果你对这些没什么兴趣,大可以略过服务端的细节直接去看客户端示例。

REST服务

下面的代码就是个客户端提供服务的REST服务。这里的BookRepository就是一个由@Singleton和@Startup修饰的简单sessionbean,这个bean用来模拟存储或获取BookEntity。服务对外提供了保存一本书、删除一本书、根据标识查找书籍和获取所有可用书籍的接口。当一本书被保存在服务端时,服务器会为该书生成一个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
packagecom.hascode.tutorial.jaxrs.server;

importjava.util.List;

importjavax.ejb.EJB;
importjavax.ejb.Stateless;
importjavax.ws.rs.Consumes;
importjavax.ws.rs.DELETE;
importjavax.ws.rs.GET;
importjavax.ws.rs.POST;
importjavax.ws.rs.Path;
importjavax.ws.rs.PathParam;
importjavax.ws.rs.Produces;
importjavax.ws.rs.core.GenericEntity;
importjavax.ws.rs.core.MediaType;
importjavax.ws.rs.core.Response;

importcom.hascode.tutorial.jaxrs.entity.Book;

@Stateless
@Path("/book")
publicclassBookStoreService{
@EJB
privateBookRepositorybookRepository;

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
publicResponsesaveBook(finalBookbook){
BookbookPersisted=bookRepository.saveBook(book);
returnResponse.ok(bookPersisted).build();
}

@DELETE
@Path("/{id}")
publicResponsedeleteBook(final@PathParam("id")Stringid){
bookRepository.deleteBook(id);
returnResponse.ok().build();
}

@GET
@Produces(MediaType.APPLICATION_JSON)
publicResponsegetAll(){
List<Book>books=bookRepository.getAll();
GenericEntity<List<Book>>bookWrapper=newGenericEntity<List<Book>>(books){};
returnResponse.ok(bookWrapper).build();
}

@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
publicResponsegetById(final@PathParam("id")Stringid){
Bookbook=bookRepository.getById(id);
returnResponse.ok(book).build();
}
}
备注:我修改了应用服务器,以便使用Jackson提供的服务发现机制处理JSON数据。

BookEntity

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
packagecom.hascode.tutorial.jaxrs.entity;

importjava.io.Serializable;
importjava.math.BigDecimal;
importjava.util.Calendar;

publicclassBookimplementsSerializable{
privatestaticfinallongserialVersionUID=1L;

privateStringid;
privateStringtitle;
privateBigDecimalprice;
privateCalendarpublished;

//getter+setter..
}

创建并绑定一个客户端

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

通过ClientBuilder获取一个客户端的引用:Clientclient=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
privatestaticfinalStringREST_SERVICE_URL="http://localhost:8080/tutorial/rs/book";

privatestaticfinalStringTITLE="Onebigbook";
privatestaticfinalBigDecimalPRICE=newBigDecimal("20.0");
privatestaticfinalGregorianCalendarPUBLISHED=newGregorianCalendar(
2013,12,24);

Clientclient=ClientBuilder.newClient().register(JacksonFeature.class);

publicBookmockBook(){
Bookbook=newBook();
book.setTitle(TITLE);
book.setPrice(PRICE);
book.setPublished(PUBLISHED);
returnbook;
}
唯一值得注意的是,我在客户端运行时中加入了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
publicvoidcrudExample(){
//1.Saveanewbook
Bookbook=mockBook();
BookbookPersisted=client
.target(REST_SERVICE_URL)
.request()
.post(Entity.entity(book,MediaType.APPLICATION_JSON),
Book.class);

StringbookId=bookPersisted.getId();
assertThat(bookId,notNullValue());

//2.Fetchbookbyid
Bookbook2=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.Fetchallbooks
GenericType<List<Book>>bookType=newGenericType<List<Book>>(){
};//generictypetowrapagenericlistofbooks
List<Book>books=client.target(REST_SERVICE_URL).request()
.get(bookType);
assertThat(books.size(),equalTo(1));

//4.Deleteabook
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的FutureAPI提供的多种途径来异步地处理请求。

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

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
publicvoidasyncExample()throwsException{
Bookbook=mockBook();

Future<Book>fb=client
.target(REST_SERVICE_URL)
.request()
.async()
.post(Entity.entity(book,MediaType.APPLICATION_JSON),
Book.class);

BookbookPersisted=fb.get();

StringbookId=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(newGenericType<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
publicvoidinvocationCallbackExample()throwsException{
Bookbook=mockBook();
client.target(REST_SERVICE_URL)
.request()
.async()
.post(Entity.entity(book,MediaType.APPLICATION_JSON),
newInvocationCallback<Book>(){
@Override
publicvoidcompleted(finalBookbookPersisted){
System.out.println("booksaved:"
+bookPersisted);
assertThat(bookPersisted.getId(),
notNullValue());
}

@Override
publicvoidfailed(finalThrowablethrowable){
throwable.printStackTrace();
}
}).get();

client.target(REST_SERVICE_URL).request().async()
.get(newInvocationCallback<List<Book>>(){
@Override
publicvoidcompleted(finalList<Book>books){
System.out.println(books.size()+"booksreceived");
assertThat(books.size(),greaterThanOrEqualTo(1));
}

@Override
publicvoidfailed(finalThrowablethrowable){
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
publicvoidrequestPreparationExample()throwsException{
Bookbook=mockBook();
InvocationsaveBook=client.target(REST_SERVICE_URL).request()
.buildPost(Entity.entity(book,MediaType.APPLICATION_JSON));
InvocationlistBooks=client.target(REST_SERVICE_URL).request()
.buildGet();

Responseresponse=saveBook.invoke();
Bookb1=response.readEntity(Book.class);

//alternative:Bookb1=saveBook.invoke(Book.class);
assertThat(b1.getId(),notNullValue());

//asyncinvocation
Future<List<Book>>b=listBooks.submit(newGenericType<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
packagecom.hascode.tutorial.client;

importjava.io.IOException;
importjava.math.BigDecimal;

importjavax.ws.rs.client.ClientRequestContext;
importjavax.ws.rs.client.ClientRequestFilter;

importcom.hascode.tutorial.jaxrs.entity.Book;

publicclassTaxAdjustmentFilterimplementsClientRequestFilter{
publicstaticfinalBigDecimalTAX_RATE=newBigDecimal("2.5");

@Override
publicvoidfilter(finalClientRequestContextrc)throwsIOException{
Stringmethod=rc.getMethod();
if("POST".equals(method)&&rc.hasEntity()){
Bookbook=(Book)rc.getEntity();
BigDecimalpriceWithTaxes=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
publicvoidclientRequestFilterExample(){
Bookbook=mockBook();

Clientclient=ClientBuilder.newClient()
.register(JacksonFeature.class)
.register(TaxAdjustmentFilter.class);
BookbookPersisted=client
.target(REST_SERVICE_URL)
.request()
.post(Entity.entity(book,MediaType.APPLICATION_JSON),
Book.class);

StringbookId=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
packagecom.hascode.tutorial.client;

importjava.io.IOException;
importjava.util.List;
importjava.util.Map.Entry;

importjavax.ws.rs.client.ClientRequestContext;
importjavax.ws.rs.client.ClientResponseContext;
importjavax.ws.rs.client.ClientResponseFilter;

publicclassClientResponseLoggingFilterimplementsClientResponseFilter{

@Override
publicvoidfilter(finalClientRequestContextreqCtx,
finalClientResponseContextresCtx)throwsIOException{
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(Stringvalue: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
publicvoidclientResponseFilterExample(){
Bookbook=mockBook();

Clientclient=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:SatDec2818:50:16CET2013
last-modified:null
location:null
headers:
Date:Sat,28Dec201317:50:16GMT,
Transfer-Encoding:chunked,
Content-Type:application/json,
Server:GlassFishServerOpenSourceEdition3.1,
X-Powered-By:Servlet/3.0JSP/2.2(GlassFishServerOpenSourceEdition3.1Java/OracleCorporation/1.7),
media-type:application
译注:GlassFish是SUN所研发的开放源代码应用服务器,GlassFish以Java编写以增加跨平台性[wikipedia]。

教程源码

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

下载war-FileREST服务器

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

JAX-RS1.0andJAX-B

如果你对旧版本的协议感兴趣,这篇文章正是你需要的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: