2.3. JAX-RS 要求処理

2.3.1. 非同期 HTTP リクエストの処理

非同期リクエスト処理により、非ブロッキング入力および出力を使用して単一の HTTP リクエストを処理し、必要に応じて別々のスレッドで処理できます。

クライアントとサーバーの両方からプッシュおよびプルする AJAX クライアントについて考えてみましょう。このシナリオでは、サーバーのソケットでクライアントが長時間ブロックし、新しいメッセージを待ちます。サーバーが着信および発信の入出力でブロックしている同期 HTTP 処理の場合は、クライアント接続ごとに別々のスレッドが消費されます。リクエスト処理のこのモデルでは、大量のメモリーおよび有意なスレッドリソースを消費します。

非同期処理は、受け入れる接続とリクエスト処理操作を切り分けます。クライアント接続を受け入れるスレッドと、長時間の時間がかかる操作を処理させるスレッドと、異なるスレッドが割り当てられます。このモデルでは、コンテナーは以下のように動作します。

  1. アクセプターであるクライアント接続を受け入れるためにスレッドをディスパッチします。
  2. その後、リクエストを処理スレッド (worker) に渡します。
  3. 最後に、アクセプタースレッドを解放します。

結果が、worker (ワーカー) スレッドによってクライアントに戻されます。そのため、クライアントの接続は開いたままであるため、サーバーのスループットとスケーラビリティーが向上します。

2.3.1.1. 非同期 NIO 要求処理

RESTEasy のデフォルトの非同期エンジン実装クラスは ApacheHttpAsyncClient4Engine です。これは Apache httpcomponents の HttpAsyncClient でビルドされ、内部で非ブロッキング IO モデルを使用してリクエストをディスパッチします。

ResteasyClientBuilder クラスで useAsyncHttpEngine メソッドを呼び出すと、非同期エンジンをアクティブなエンジンとして設定できます。

Client asyncClient = new ResteasyClientBuilder().useAsyncHttpEngine()
                     .build();
Future<Response> future = asyncClient
                          .target("http://locahost:8080/test").request()
                          .async().get();
Response res = future.get();
Assert.assertEquals(HttpResponseCodes.SC_OK, res.getStatus());
String entity = res.readEntity(String.class);

2.3.1.2. サーバー非同期の応答処理

サーバー側では、非同期処理は、元のリクエストスレッドを一時停止し、別のスレッドでリクエスト処理を開始します。これにより、元のサーバー側のスレッドが解放され、他の受信リクエストが許可されます。

2.3.1.2.1. AsyncResponse API

JAX-RS 2.0 仕様は、@Suspended アノテーションと AsyncResponse インターフェイスという 2 つのクラスを使用して非同期 HTTP サポートを追加しました。

AsyncResponse をパラメーターとして JAX-RS メソッドに挿入すると、現在実行されているスレッドから HTTP リクエストと応答を切り離すよう RESTEasy が要求します。これにより、現在のスレッドが応答を自動的に処理しないようにします。

AsyncResponse はコールバックオブジェクトです。resume() メソッドのいずれかを呼び出す動作により、応答がクライアントに返送され、HTTP リクエストも終了します。以下は非同期処理の例になります。

import javax.ws.rs.container.Suspended;
import javax.ws.rs.container.AsyncResponse;

@Path("/")
public class SimpleResource {
   @GET
   @Path("basic")
   @Produces("text/plain")
   public void getBasic(@Suspended final AsyncResponse response) throws Exception {
      Thread t = new Thread() {
         @Override
         public void run() {
            try {
               Response jaxrs = Response.ok("basic").type(MediaType.TEXT_PLAIN).build();
               response.resume(jaxrs);
            }
            catch (Exception e) {
               e.printStackTrace();
            }
         }
      };
      t.start();
   }
}

2.3.1.3. Async Missionr Client API

同様に、クライアント側の非同期処理は、サーバーからの応答待ちがないため、リクエストスレッドのブロックを阻止します。たとえば、リクエストを発行したスレッドは、ユーザーインターフェイスコンポーネントも更新することがあります。応答を待つスレッドがブロックされると、ユーザーが認識したアプリケーションのパフォーマンスに影響が出ます。

2.3.1.3.1. future の使用

以下のコードスニペットでは、get() メソッドはリクエストではなく async() メソッド上で呼び出されます。これにより、呼び出しメカニズムが同期から非同期に変更されます。同期的に応答するのではなく、async() メソッドは future オブジェクトを返します。get() メソッドを呼び出すと、応答の準備ができるまで呼び出しがブロックされます。future.get() メソッドは、応答の準備ができると必ず返されます。

import java.util.concurrent.Future;
import javax.ws.rs.client.Client;
...

@Test
public void AsyncGetTest() throws Exception {
    Client client = ClientBuilder.newClient();
    Future<String> future = client.target(generateURL("/test")).request().async().get(String.class);
    String entity = future.get();
    Assert.assertEquals("get", entity);
}
2.3.1.3.2. InvocationCallback の使用

AsyncInvoker インターフェイスを使用すると、非同期呼び出しの処理の準備ができたときに呼び出されるオブジェクトを登録できます。InvocationCallback インターフェイスは completed() および failed() のメソッドを提供します。completed() メソッドは、処理が正常に完了し、応答を受信するたびに呼び出されます。逆に、要求の処理に成功していないと、failed() メソッドが呼び出されます。

import javax.ws.rs.client.InvocationCallback;
...

@Test
public void AsyncCallbackGetTest() throws Exception {
    Client client = ClientBuilder.newClient();
	final CountDownLatch latch = new CountDownLatch(1);
        Future<Response> future = client.target(generateURL("/test")).request().async().get(new InvocationCallback<Response>() {
         	@Override
                public void completed(Response response) {
                    String entity = response.readEntity(String.class);
                    Assert.assertEquals("get", entity);
                    latch.countDown();
                }

                @Override
                public void failed(Throwable error) {
                }
            });
            Response res = future.get();
            Assert.assertEquals(HttpResponseCodes.SC_OK, res.getStatus());
            Assert.assertTrue("Asynchronous invocation didn't use custom implemented Invocation callback", latch.await(5, imeUnit.SECONDS));
}

2.3.2. カスタム RESTEasy アノテーション

バイトコードにパラメーター名が追加されたため、@PathParam@QueryParam@FormParam@CookieParam@HeaderParam、および @MatrixParam アノテーションでパラメーター名を指定する必要がなくなりました。これには、任意の value パラメーターを持つ異なるパッケージで、同じ名前を持つ新しいアノテーションに切り替える必要があります。これは、以下の手順で実現できます。

  1. org.jboss.resteasy.annotations.jaxrs パッケージをインポートし、JAX-RS 仕様のアノテーションを置き換えます。
  2. ビルドシステムがバイトコードにメソッドパラメーター名を記録するように設定します。

    Maven ユーザーは、maven.compiler.parameterstrue に設定すると、バイトコードにメソッドパラメーター名を記録できます。

    <properties>
        <maven.compiler.parameters>true</maven.compiler.parameters>
    </properties>
  3. 名前がアノテーション変数の名前に一致する場合は、アノテーション値を削除します。

    注記

    アノテーション付きのパラメーター、アノテーション付きのフィールド、または JavaBean プロパティーのアノテーション名は省略できます。

以下の使用例を見てみましょう。

import org.jboss.resteasy.annotations.jaxrs.*;

@Path("/library")
public class Library {

   @GET
   @Path("/book/{isbn}")
   public String getBook(@PathParam String isbn) {
      // search my database and get a string representation and return it
   }
}

アノテーションが付けられた変数に path パラメーターと同じ名前がない場合は、以下のように名前を指定できます。

import org.jboss.resteasy.annotations.jaxrs.*;

@Path("/library")
public class Library {

   @GET
   @Path("/book/{isbn}")
   public String getBook(@PathParam("isbn") String id) {
      // search my database and get a string representation and return it
   }
}