The Java EE 7 Tutorialの30 Accessing REST Resources with the JAX-RS Client APIのセクションを読んで訳した。
30 Accessing REST Resources with the JAX-RS Client API
この章ではJAX-RS Client APIを解説し、Java言語を使用してRESTリソースにアクセスするサンプルも示します。
JAX-RSは他のJavaアプリケーションからRESTリソースにアクセスするクライアントAPIを提供します。
この章では以下のトピックを扱います。
- 30.1 Overview of the Client API
- 30.2 Using the Client API in the JAX-RS Example Applications
- 30.3 Advanced Features of the Client API
30.1 Overview of the Client API
JAX-RS Client APIは、必ずしもJAX-RSサービスで無くても良い、任意のRESTリソースにアクセスするための高レベルAPIを提供します。Client APIはjavax.ws.rs.client
パッケージに定義されています。
30.1.1 Creating a Basic Client Request Using the Client API
Client APIを使用してRESTリソースにアクセスするには以下の手順を踏みます。
Client APIは流れるようなインタフェースで設計されており、メソッド呼び出しのチェーンでRESTリソースへのリクエスト設定とサブミットを数行のコードで行えます。
Client client = ClientBuilder.newClient(); String name = client.target("http://example.com/webapi/hello") .request(MediaType.TEXT_PLAIN) .get(String.class);
この例では、まずクライアントのインスタンスをjavax.ws.rs.client.ClientBuilder.newClient
メソッドを呼ぶことで生成します。それから、一行のコードのメソッドチェーンでリクエストの設定を行います。Client.target
メソッドでURIベースのターゲットを設定します。javax.ws.rs.client.WebTarget.request
メソッドは返されるエンティティ用のメディアタイプを設定します。javax.ws.rs.client.Invocation.Builder.get
メソッドはHTTP GET
リクエストを使用して、返されるエンティティの型にString
を設定し、サービスを実行します。
30.1.1.1 Obtaining the Client Instance
Client
インタフェースはRESTクライアントがRESTful webサービスを使用するのに必要となるインフラとアクションを定義します。Client
のインスタンスはClientBuilder.newClient
メソッドを呼び出すことで取得します。
Client client = ClientBuilder.newClient();
ターゲットのリソースを使用し終えたら、Client
インスタンスのclose
メソッドを呼び出します。
Client client = ClientBuilder.newClient();
...
client.close();
Client
インスタンスはヘビーウェイトな(heavyweight)オブジェクトです。パフォーマンス上の理由により、実行環境においてこれらのインスタンスの生成と破棄は重い処理となるので、Client
インスタンス数には制限をかけてください。
30.1.1.2 Setting the Client Target
クライアントのターゲットは特定URLのRESTリソースで、これはjavax.ws.rs.client.WebTarget
インタフェースのインスタンスで表現します。WebTarget
インスタンスは、Client.target
メソッドにターゲットとなるRESTリソースのURLを渡して、取得します。
Client client = ClientBuilder.newClient();
WebTarget myResource = client.target("http://example.com/webapi");
複雑なRESTリソースの場合、複数のWebTarget
インスタンスを生成すると便利です。以下の例では、ベースとなるターゲットは、RESTリソースのサービスを表現する複数のターゲットで構成しています。
Client client = ClientBuilder.newClient(); WebTarget base = client.target("http://example.com/webapi"); // WebTarget at http://example.com/webapi/read WebTarget read = base.path("read"); // WebTarget at http://example.com/webapi/write WebTarget write = base.path("write");
WebTarget.path
メソッドはターゲットURIに渡されたパスを追加した新規のWebTarget
インスタンスを生成します。
30.1.1.3 Setting Path Parameters in Targets
クライアントリクエストにおけるパスパラメータはURLテンプレートパラメータとして指定可能で、これはJAX-RSサービスのリソース定義時に使われるテンプレートパラメータと同じようなものです。テンプレートパラメータは中括弧({}
)で囲んだテンプレート変数で指定します。{username}
を解決するのにresolveTemplate
メソッドを呼び出し、パスに別の変数を追加するのにqueryParam
メソッドを呼び出します。
WebTarget myResource = client.target("http://example.com/webapi/read") .path("{userName}") .resolveTemplate("userName", "janedoe") .queryParam("chapter", "1");// http://example.com/webapi/read/janedoe?chapter=1Response response = myResource.request(...).get();
30.1.1.4 Invoking the Request
ターゲットに対して任意の設定オプションを設定ないし適用した後、リクエストを生成するにはWebTarget.request
メソッドを呼び出します。たいていの場合、MIMEタイプ文字列あるいはjavax.ws.rs.core.MediaType
定数の内の一つを適切なメディアレスポンスタイプとしてWebTarget.request
に渡します。WebTarget.request
メソッドはjavax.ws.rs.client.Invocation.Builder
のインスタンスを返し、このヘルパーオブジェクトはクライアントリクエストを行うメソッドを提供します。
Client client = ClientBuilder.newClient();
WebTarget myResource = client.target("http://example.com/webapi/read");
Invocation.Builder builder = myResource.request(MediaType.TEXT_PLAIN);
MediaType
定数とMIMEタイプを定義する文字列は同等です。
Invocation.Builder builder = myResource.request("text/plain");
メディアタイプを設定した後、ターゲットのRESTリソースが期待するHTTPリクエストタイプに相当するInvocation.Builder
インスタンスのメソッドの一つを呼び出します。これらのメソッドは以下の通りです。
get()
post()
delete()
put()
head()
options()
たとえば、ターゲットRESTリソースがHTTP GETリクエストを受け入れるのであれば、Invocation.Builder.get
メソッドを呼び出します。戻される型はターゲットRESTリソースが返すエンティティと一致させます。
Client client = ClientBuilder.newClient(); WebTarget myResource = client.target("http://example.com/webapi/read"); String response = myResource.request(MediaType.TEXT_PLAIN) .get(String.class);
ターゲットRESTリソースがHTTP POSTリクエストを期待するのであれば、Invocation.Builder.post
メソッドを呼び出します。
Client client = ClientBuilder.newClient(); StoreOrder order = new StoreOrder(...); WebTarget myResource = client.target("http://example.com/webapi/write"); TrackingNumber trackingNumber = myResource.request(MediaType.APPLICATION_XML) .post(Entity.xml(order), TrackingNumber.class);
上記の例では、戻される型のカスタムクラスはInvocation.Builder.post(Entity<?> entity, Class<T> responseType)
メソッドの引数として設定される型が対象となります。
戻される型がコレクションの場合、レスポンス型の引数としてjavax.ws.rs.core.GenericType<T>
を使用し、T
がコレクションの型になります。
List<StoreOrder> orders = client.target("http://example.com/webapi/read") .path("allOrders") .request(MediaType.APPLICATION_XML) .get(new GenericType<List<StoreOrder>>() {});
上記の例は、Client APIのメソッドチェーンがどのようにリクエストの設定と実行を行うかの例にもなっています。
30.2 Using the Client API in the JAX-RS Example Applications
rsvp
とcustomer
のサンプルはJAX-RSサービスを呼ぶのにClient APIを使用します。このセクションではサンプルアプリケーションがどのようにClient APIを使用しているかを解説します。
30.2.1 The Client API in the rsvp Example Application
rsvp
アプリケーションは、Chapter 29, "The rsvp Example Applicationに解説がある通り、JAX-RSリソースを使用してユーザがイベント招待に返事をするものです。webアプリケーションはサービスリソースと通信するのにCDIバッキングビーンでClient APIを使用し、Facelets webインタフェースが結果を表示します。
StatusManager
CDIバッキングビーンはシステム内の現在の全イベントを取得します。バッキングビーンで使われるクライアントのインスタンスはコンストラクタで取得しています。
public StatusManager() { this.client = ClientBuilder.newClient(); }
StatusManager.getEvents
メソッドはhttp://localhost:8080/rsvp/webapi/status/all
でリソースを呼ぶことでシステム内の現在の全イベントのコレクションを返し、そのコレクションには各イベントごとのエントリでXMLドキュメントが含まれます。Client APIは自動的にXMLをアンマーシャルしてList<Event>
インスタンスを生成します。
public List<Event> getEvents() { List<Event> returnedEvents = null; try { returnedEvents = client.target(baseUri) .path("all") .request(MediaType.APPLICATION_XML) .get(new GenericType<List<Event>>() { }); if (returnedEvents == null) { logger.log(Level.SEVERE, "Returned events null."); } else { logger.log(Level.INFO, "Events have been returned."); } } catch (WebApplicationException ex) { throw new WebApplicationException(Response.Status.NOT_FOUND); } ... return returnedEvents; }
StatusManager.changeStatus
メソッドは出席者の返答を更新するのに使用します。サービスに対し出席者の返答を含めたHTTP POST
リクエストを生成します。リクエストのボディはXMLドキュメントです。
public String changeStatus(ResponseEnum userResponse, Person person, Event event) { String navigation; try { logger.log(Level.INFO, "changing status to {0} for {1} {2} for event ID {3}.", new Object[]{userResponse, person.getFirstName(), person.getLastName(), event.getId().toString()}); client.target(baseUri) .path(event.getId().toString()) .path(person.getId().toString()) .request(MediaType.APPLICATION_XML) .post(Entity.xml(userResponse.getLabel())); navigation = "changedStatus"; } catch (ResponseProcessingException ex) { logger.log(Level.WARNING, "couldn''t change status for {0} {1}", new Object[]{person.getFirstName(), person.getLastName()}); logger.log(Level.WARNING, ex.getMessage()); navigation = "error"; } return navigation; }
30.2.2 The Client API in the customer Example Application
customer
サンプルアプリケーションは、Chapter 31, "The customer Example Application"で解説されているように、データベースに顧客データを格納し、XMLリソースとしてデータを提供します。サービスリソースが提供するメソッドは、顧客を生成するものと全顧客の取得するものになります。Facelets webアプリケーションがサービスリソースのクライアントとして振る舞い、顧客を作成するためのフォームと表形式で顧客のリストを表示します。
CustomerBean
ステートレスセッションビーンはサービスリソースと通信するのにJAX-RS Client APIを使用します。CustomerBean.createCustomer
メソッドはFaceletsのフォームが生成するCustomer
エンティティインスタンスを引数に取り、サービスURIにPOSTを行います。
public String createCustomer(Customer customer) { if (customer == null) { logger.log(Level.WARNING, "customer is null."); return "customerError"; } String navigation; Response response = client.target("http://localhost:8080/customer/webapi/Customer") .request(MediaType.APPLICATION_XML) .post(Entity.entity(customer, MediaType.APPLICATION_XML), Response.class); if (response.getStatus() == Status.CREATED.getStatusCode()) { navigation = "customerCreated"; } else { logger.log(Level.WARNING, "couldn''t create customer with id {0}. Status returned was {1}", new Object[]{customer.getId(), response.getStatus()}); FacesContext context = FacesContext.getCurrentInstance(); context.addMessage(null, new FacesMessage("Could not create customer.")); navigation = "customerError"; } return navigation; }
XMLのリクエストエンティティはInvocation.Builder.post
メソッドを呼ぶことで生成し、このメソッドの引数にはCustomer
インスタンスから新規のEntity
インスタンスを生成したものを渡し、メディアタイプにはMediaType.APPLICATION_XML
を指定しています。
CustomerBean.retrieveCustomer
メソッドは顧客IDをサービスURIに追加することでサービスからCustomer
エンティテイのインスタンスを取得します。
public String retrieveCustomer(String id) { String navigation; Customer customer = client.target("http://localhost:8080/customer/webapi/Customer") .path(id) .request(MediaType.APPLICATION_XML) .get(Customer.class); if (customer == null) { navigation = "customerError"; } else { navigation = "customerRetrieved"; } return navigation; }
CustomerBean.retrieveAllCustomers
メソッドはList<Customer>
インスタンスとして顧客のコレクションを取得します。このリストはFacelets webアプリケーションで表形式で表示されます。
public List<Customer> retrieveAllCustomers() { List<Customer> customers = client.target("http://localhost:8080/customer/webapi/Customer") .path("all") .request(MediaType.APPLICATION_XML) .get(new GenericType<List<Customer>>() { }); return customers; }
レスポンスの型がコレクションなので、Invocation.Builder.get
メソッドにはGenericType<List<Customer>>
の新規インスタンスを渡しています。
30.3 Advanced Features of the Client API
このセクションではJAX-RS Client APIの高度な機能について解説します。
30.3.1 Configuring the Client Request
クライアントリクエストを実行する前にオプション設定をいくつか追加可能です。
30.3.1.1 Setting Message Headers in the Client Request
Invocation.Builder.header
メソッドでリクエストにHTTPヘッダーを設定可能です。
Client client = ClientBuilder.newClient(); WebTarget myResource = client.target("http://example.com/webapi/read"); String response = myResource.request(MediaType.TEXT_PLAIN) .header("myHeader", "The header value") .get(String.class);
リクエストに複数のヘッダーを設定する場合、Invocation.Builder.headers
にHTTPヘッダーのname-valueペアのjavax.ws.rs.core.MultivaluedMap
インスタンスを渡して実行します。headers
メソッドの呼び出しはMultivaluedMap
インスタンスで提供されるヘッダーですべての既存のヘッダーを上書きします。
Client client = ClientBuilder.newClient(); WebTarget myResource = client.target("http://example.com/webapi/read"); MultivaluedMap<String, Object> myHeaders = new MultivaluedMap<>("myHeader", "The header value"); myHeaders.add(...); String response = myResource.request(MediaType.TEXT_PLAIN) .headers(myHeaders) .get(String.class);
MultivaluedMap
インタフェースは、あるキーに複数の値を指定することも可能です。
MultivaluedMap<String, Object> myHeaders = new MultivaluedMap<String, Object>(); List<String> values = new ArrayList<>(); values.add(...) myHeaders.add("myHeader", values
30.3.1.2 Setting Cookies in the Client Request
Invocation.Builder.cookie
メソッドでリクエストにHTTP cookieを追加できます。このメソッドの引数にはname-valueを取ります。
Client client = ClientBuilder.newClient(); WebTarget myResource = client.target("http://example.com/webapi/read"); String response = myResource.request(MediaType.TEXT_PLAIN) .cookie("myCookie", "The cookie value") .get(String.class);
javax.ws.rs.core.Cookie
クラスはHTTP cookieの属性をカプセル化するもので、name, value, path, domain, cookieのRFC仕様バージョンを持ちます。以下の例では、Cookie
オブジェクトは、name-value, path, domainを持ちます。
Client client = ClientBuilder.newClient(); WebTarget myResource = client.target("http://example.com/webapi/read"); Cookie myCookie = new Cookie("myCookie", "The cookie value", "/webapi/read", "example.com"); String response = myResource.request(MediaType.TEXT_PLAIN) .cookie(myCookie) .get(String.class);
30.3.1.3 Adding Filters to the Client
ターゲットリソースからのレスポンスやクライアントリクエストにカスタムのフィルターを登録出来ます。フィルタークラスを登録するには、Client
インスタンス作成時にClient.register
メソッドを使用します。
Client client = ClientBuilder.newClient().register(MyLoggingFilter.class);
この例では、Client
インスタンスを使用するすべての実行にはMyLoggingFilter
が登録されます。
WebTarget.register
でターゲットのクラスにフィルターを登録することも出来ます。
Client client = ClientBuilder.newClient().register(MyLoggingFilter.class); WebTarget target = client.target("http://example.com/webapi/secure") .register(MyAuthenticationFilter.class);
この例では、呼び出しにはMyLoggingFilter
とMyAuthenticationFilter
の両方とも登録されます。
リクエストとレスポンスのフィルタークラスはjavax.ws.rs.client.ClientRequestFilter
とjavax.ws.rs.client.ClientResponseFilter
のインタフェースを実装します。これらのインタフェースにはfilter
メソッド一つだけが定義されています。すべてのフィルターはjavax.ws.rs.ext.Provider
アノテーションを付与する必要があります。
以下のクラスはクライアントリクエストとレスポンス両方でログ出力を行うフィルターです。
@Provider public class MyLoggingFilter implements ClientRequestFilter, ClientResponseFilter { static final Logger logger = Logger.getLogger(...); // implement the ClientRequestFilter.filter method @Override public void filter(ClientRequestContext requestContext) throws IOException { logger.log(...); ... } // implement the ClientResponseFilter.filter method @Override public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { logger.log(...); ... } }
フィルターがアクティブで呼び出しを停止する必要がある場合、コンテキストオブジェクトのabortWith
メソッドを使用します。このメソッドの引数にはフィルター内で作成したjavax.ws.rs.core.Response
インスタンスを渡します。
@Override public void filter(ClientRequestContext requestContext) throws IOException { ... Response response = new Response(); response.status(500); requestContext.abortWith(response); }
30.3.2 Asynchronous Invocations in the Client API
ネットワークアプリケーションでは、特に長時間実行や複雑なネットワーク呼び出しにおいて、ネットワークトラブルはアプリケーションの体感的なパフォーマンスに影響を与えます。非同期処理はブロッキングを防いでアプリケーションリソースをより良く使うのに役立ちます。
JAX-RS Client APIでサービス呼び出しが非同期に実行されるよう指示するには、クライアントリクエストの作成時にInvocation.Builder.async
メソッドを使用します。非同期呼び出しはすぐに呼び出し元に制御が戻り、戻り値の型はjava.util.concurrent.Future<T>
です。Future<T>
オブジェクトのメソッドには、非同期呼び出しが完了したか調べるもの・最終的な結果を取得するもの・呼び出しをキャンセルするもの・呼び出しがキャンセルされたか調べるもの、があります。
以下の例ではリソースに対して非同期リクエストを実行する方法を示しています。
Client client = ClientBuilder.newClient(); WebTarget myResource = client.target("http://example.com/webapi/read"); Future<String> response = myResource.request(MediaType.TEXT_PLAIN) .async() .get(String.class);
30.3.2.1 Using Custom Callbacks in Asynchronous Invocations
InvocationCallback
インタフェースには、completed
とfailed
の2つのメソッドがあり、それぞれ非同期呼び出しが完了または失敗したときに呼ばれます。requestメソッドでInvocationCallback
のインスタンスを登録可能です。
以下の例は非同期呼び出し時のコールバックオブジェクトの登録方法を示しています。
Client client = ClientBuilder.newClient(); WebTarget myResource = client.target("http://example.com/webapi/read"); Future<Customer> fCustomer = myResource.request(MediaType.TEXT_PLAIN) .async() .get(new InvocationCallback<Customer>() { @Override public void completed(Customer customer) { // Do something with the customer object } @Override public void failed(Throwable throwable) { // handle the error } });
関連リンク
- The Java EE 7 Tutorialのテキトー翻訳まとめ - Qiita - Java EE 7 Tutorialのうち、自分がテキトー翻訳したものの一覧
*1:原文はtarget.後で出てくるけどおおむねリソースのURLと考えて良い