kagamihogeの日記

kagamihogeの日記です。

The Java EE 7 TutorialのAccessing REST Resources with the JAX-RS Client APIの章をテキトーに訳した

The Java EE 7 Tutorial30 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

JAX-RS Client APIは、必ずしもJAX-RSサービスで無くても良い、任意のRESTリソースにアクセスするための高レベルAPIを提供します。Client APIjavax.ws.rs.clientパッケージに定義されています。

30.1.1 Creating a Basic Client Request Using the Client API

Client APIを使用してRESTリソースにアクセスするには以下の手順を踏みます。

  1. javax.ws.rs.client.Clientインタフェースのインスタンスを取得。
  2. ターゲット*1Clientインスタンスを設定。
  3. ターゲットに基づいてリクエストを生成。
  4. リクエストを実行。

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

rsvpcustomerのサンプルは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, cookieRFC仕様バージョンを持ちます。以下の例では、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);

この例では、呼び出しにはMyLoggingFilterMyAuthenticationFilterの両方とも登録されます。

リクエストとレスポンスのフィルタークラスはjavax.ws.rs.client.ClientRequestFilterjavax.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インタフェースには、completedfailedの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
            }
    });

関連リンク

*1:原文はtarget.後で出てくるけどおおむねリソースのURLと考えて良い