導讀java
JAX-RS 2.0 又稱 JSR 339 不只定義了一套用於構建 RESTful 網絡服務的 API,同時也經過加強客戶端 API 功能簡化了REST 客戶端的構建過程。git
JAX-RS: Java API for RESTful Web Services是一個Java編程語言的應用程序接口,支持按照 表象化狀態轉變 (REST)架構風格建立Web服務Web服務[1]. JAX-RS使用了Java SE 5引入的Java 標註來簡化Web服務客戶端和服務端的開發和部署 [wikipedia]。shell
在下面的教程中,咱們將爲一個預先設置好的 REST 服務構建一個客戶端,並在這個過程當中探索新的構建選項。例如,如何處理同步或者異步的請求,如何給一個請求註冊一個回調,如何指定調用對象來構建一個請求使得請求能夠被延遲執行。再或者好比,如何使用客戶端請求和相應的過濾方法來過濾客戶端與服務器以前的通訊。編程
咱們開始吧
對於想要重建下述客戶端例子的讀者,我已經使用 Maven 建立好了一個完整的 RESTful 網絡服務程序。程序中有內嵌的應用程序服務器,以及一個可獨立運行的應用服務器 (war-file 能夠經過下文中的下載地址獲取)。json
請根據下面的一系列命令來下載並啓動 REST 服務器 (下載全部依賴可能會耗費些時間……):服務器
1
|
clone https:
//bitbucket
.org
/hascode/jaxrs2-client-tutorial
.git &&
cd
jaxrs2-client-tutorial &&
make
rest-server
|
如今,讓咱們先來看看這個 REST 服務的一些實現細節和咱們的客戶端示例中要用到的對象。若是你對這些沒什麼興趣,大能夠略過服務端的細節直接去看客戶端示例。restful
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 數據。session
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
若是你對舊版本的協議感興趣,這篇文章正是你須要的。