JAX-RS 2.0 ことはじめ

この投稿は JavaEE Advent Calendar 2012 の12月7日担当分として新たに書き下ろしたものです。前日分は @masafumi_ohta さんの「JBoss on PI」です。

また、本稿の執筆途中に派生した関連投稿「GlassFish Internals」「JAX-RS 1.1 総復習」もよろしくお願いします。


JAX-RS 2.0 (JSR 339)のPublic Previewが先日承認されました。Java EE 7のテクノロジーとしてはEL 3.0 (JSR 341)に次いで2番目となります。JAX-RS 2.0のリファレンス実装は引き続きJerseyが採用されています(Jersey 2.0以降がJAX-RS 2.0対応です)。今回はJersey 2.0 Milestone 10を具体例としてJAX-RS 2.0をご紹介します。

Jersey 2.0は本稿執筆時点でGlassFish 4.0の開発版(Build 66)にバンドルされていますので、これをダウンロードして利用するのが一番簡単な方法です。IDEではまだGlassFish 4.0の選択肢はないかもしれませんが(少なくともEclipse 4.2.1とNetBeans 7.2.1にはありません)、GlassFish 4系はGlassFish 3系のアーキテクチャをそのまま引き継いでいるため、GlassFish 3.1で代用することができます。

Jerseyのバージョンを判別する簡易的な方法は、Jerseyのクライアント・サーバー共通モジュールのJARファイル名での判別です。Jersey 1.x系ではjersey-core.jar、Jersey 2.x系ではjersey-common.jarです。特に現時点ではGlassFish 4.0に更新ツールがバンドルされていないため、この判別方法が第一選択肢となります。

その他の方法として、Jerseyを含んだWARをTomcat 7.0またはTomEE(TomEE+は不可)にデプロイして実行する方法があります。Java EE 6 Full ProfileのサーバもしくはGlassFish 3.x(Profile問わず)は既に何らかのJAX-RS 1.1実装があるためデプロイ先としては適切ではありません。

上記の問題はJAX-RS 1.1実装を取り除くことで対応できるサーバーもありますが、GlassFish 3.xに関しては管理ツール群がJAX-RS 1.1(Jersey 1.x系)に依存しているため不可能です。IPSの仕組みが分かっていれば強引にやれないこともないですが、管理系が壊れますのでドSな方にしかお勧めしません。


JAX-RS 2.0はJAX-RS 1.1を拡張したものであり、基礎の部分にはほぼ変更はありません。そのため、JAX-RSの予備知識のない場合は情報の多いJAX-RS 1.1を学んで、その後にJAX-RS 2.0との差分を見ていくのが確実な方法です。

本稿では以下、JAX-RS 1.1の予備知識があるものとして話を進めます。なお、JAX-RS 1.1については関連投稿「JAX-RS 1.1 総復習」(2012年12月末までに公開予定)を参照して下さい。

1. クライアントAPI

JAX-RS 1.1によってRESTful Webサービスの実装は非常に容易になりました。その一方でJavaのHTTPクライアントAPI(java.net.HttpURLConnection)は極めて貧弱であり、Apache HttpClient (現在はApache HTTP Components)や、JAX-RS実装が独自に用意しているクライアントAPIといった非標準ライブラリを利用するケースが大半でした。

JAX-RS 2.0ではクライアントAPIについても定義されるようになりました。全体的にJerseyのクライアントAPIの影響を強く受けたものとなっています。以下にJerseyとJAX-RSのクライアントAPIの対応表を示します。

Jersey 1.x と JAX-RS 2.0 のクライアントAPI対応表
Jersey 1.xJAX-RS 2.0
Client Client
Client.create() ClientFactory
Client.resource() Client.target
WebResource WebTarget
Invocation
WebResource.header() N/A
WebResource.cookie() N/A
WebResource.get() N/A
WebResource.post() N/A
WebResource.put() N/A
WebResource.delete() N/A
WebResource.Builder Invocation.Builder
WebResource.getRequestBuilder() WebTarget.request()
WebResource.header() Invocation.Builder.header()
WebResource.cookie() Invocation.Builder.cookie()
WebResource.Builder.get() Invocation.Builder.get()
WebResource.Builder.post() Invocation.Builder.post()
WebResource.Builder.put() Invocation.Builder.put()
WebResource.Builder.delete() Invocation.Builder.delete()
[Entity] Object Entity

全体的な傾向として、JerseyのクライアントAPIは具象クラス中心に実装されているのに対して、JAX-RS 2.0のクライアントAPIは大半をインタフェースの定義に留めており、実装は各ベンダーに委ねていることが伺えます。最近のJava EE APIはインタフェースだけ定義する傾向にあるようです。

なお、Java SE 8にも新しいHTTPクライアントAPIが含まれる見込みで、しかもJAX-RS 2.0のクライアントAPIとは仕様が異なるようです。JAX-RS 2.0のクライアントAPIはチェック例外をスローしない(すべてランタイム例外を使う)仕様で、一方Java SE 8向けのHTTPクライアントAPIはチェック例外をスローする仕様になっています。

JAX-RS 2.0のクライアントAPIはおそらくJava EE環境での使用が前提で、「例外=即システムダウン」というシナリオに沿っているためだと考えられます。Java EE 5以降、この考え方に沿ってチェック例外を投げない(ランタイム例外のみスローする)APIが主流となっています。

一方でJava SE 8のHTTPクライアントAPIはJava SE Embeddedなどの組み込み用途での使用も想定され、異常時には例外をキャッチして何らかのリカバリー処理を行う必要があるため、意図的にチェック例外をスローする仕様にしているものと推測されます。

Java EE 7 & Java SE 8時代には2つの高機能HTTPクライアントAPIが提供されることになりますが、その違いを良く理解した上で適切に使い分けたいものです。

2. フィルターとインターセプター

JAX-RSでもサーブレット同様にフィルターが使用できるようになります。クライアントのリクエスト送信時、サーバーのリクエスト受信時、サーバーのレスポンス送信時、クライアントのレスポンス受信時、計4箇所にフィルターを適用できます。フィルターは上記4種類に対応するインタフェースを実装し、@Provider アノテーションを付加したクラスであり、クラスパス上にあれば自動的にロードされます。デフォルトではすべてのリソースクラスにフィルターが適用されますが、フィルターの適用範囲をクラスまたはメソッドレベルで絞り込むこともできます。JSR 339の仕様書では簡単なロギングの例が挙げられています。なお、フィルターについては(少なくともサーバー側に関しては)概ね仕様通りに動くレベルに達しています。

JAX-RSではフィルターに加えてインターセプターという、サーバーが送受信するボディを加工する機能が追加されます。インターセプター(Interceptor)は当初ハンドラー(Handler)と呼ばれていたもので、リクエストボディー(フィルターを通っている場合あり)をMessage Body Readerが受け取る前と、Message Body Writerからレスポンスボディーが作成された後に、その内容を加工することができます。インターセプターはフィルターに似ていますが、ボディの加工が主目的であることと、サーバー側にのみ適用できることが異なります。JSR 339の仕様書ではメッセージボディーのgzip圧縮・展開処理の例が掲載されています。以下、JSR 339の仕様書から引用します。

@Provider
class GzipInterceptor implements ReaderInterceptor, WriterInterceptor {

  @Override
  Object aroundReadFrom(ReaderInterceptorContext ctx) ... {
    if (isGzipped(ctx)) {
      InputStream old = ctx.getInputStream();
      ctx.setInputStream(new GZIPInputStream(old));
      try {
        return ctx.proceed();
      } finally {
        ctx.setInputStream(old);
      }
    } else {
      return ctx.proceed();
    }
  }
  
  @Override
  void aroundWriteTo(WriterInterceptorContext ctx) ... {
    OutputStream old = ctx.getOutputStream();
    GZIPOutputStream gzipOutputStream = new GZIPOutputStream(old);
    ctx.setOutputStream(gzipOutputStream);
    updateHeaders(ctx);
    try {
      ctx.proceed();
    } finally {
      gzipOutputStream.finish();
      ctx.setOutputStream(old);
    }
  }
  ...
}

フィルターとインターセプターの前後関係を図示すると以下のようになります。

JAX-RS Filter and Interceptor.png

3. CDI連携

JAX-RS 1.1はCDIに対応しておらず、JAX-RSのリソースクラスをEJB化する(JAX-RS - EJB連携)ことで間接的にCDIを利用していました。JAX-RS 2.0では標準でJSF 2.x同様にCDIに対応するようになります。この機能は、JerseyではMilestone 11(2013年1月末)で実装される予定です。

4. パラメーターの検証

JAX-RS 2.0ではJSF 2.xやJPA 2.xと同様にBean Validationに対応して、アノテーションによるパラメーターの検証ができるようになります。この機能は、JerseyではMilestone 11(2013年1月末)で実装される予定です。

5. 非同期処理

JAX-RS 2.0では標準で非同期処理をサポートします。JAX-RS 1.1の実装ではRESTEasyが独自に非同期処理を実装していましたが、Jerseyは非同期処理を持っていませんでした。JAX-RS 2.0の非同期処理はRESTEasyのものに似ており、リソースクラスのメソッド内でスレッドを実行することにより実現します。以下、JSR 339の仕様書からサンプルコードを引用します。

@Path("/async/longRunning")
public class MyResource {

  @GET
  public void longRunningOp(@Suspended final AsyncResponse ar) {
    executor.submit(
      new Runnable() {
        public void run() {
          executeLongRunningOp();
          ar.resume("Hello async world!");
        }
      });
  }
  ...
}

JAX-RS 2.0の非同期処理サポートはサーバーだけでなくクライアントAPIにも用意されています。こちらもJSR 339の仕様書からサンプルコードを引用します。

Client client = ClientFactory.newClient();
Target target = client.target("http://example.org/customers/{id}");
target.pathParam("id", 123).request().async().get(
  new InvocationCallback() {
    @Override
    public void completed(Customer customer) {
      // Do something
    }
    @Override
    public void failed(Throwable throwable) {
      // Process error
    }
  });
  Future ff = target.pathParam("id", 123).request().async().get(
    new InvocationCallback() {
      @Override
      public void completed(Customer customer) {
        // Do something
      }
      @Override
      public void failed(Throwable throwable) {
        // Process error
      }
    });
  
  // After waiting for a while ...
  if (!ff.isDone()) {
  ff.cancel(true);
}

6. その他

JAX-RS 2.0ではハイパーリンクを扱うためのAPI(Linkクラス)が追加されています。サーバーでのレスポンスボディへのハイパーリンク埋め込みや、クライアントにおけるレスポンスボディからのハイパーリンク取得が少しだけ便利になるかもしれません。

また、JAX-RS 2.0ではコンテント・ネゴシエーションに本格対応します。JAX-RS 1.1でも複数のMIMEタイプをサポートすることはできましたが、JAX-RS 2.0ではHTTP 1.1で定義されているコンテント・ネゴシエーションをサポートするようになります。

7. まとめ

JAX-RS 2.0の新機能について、Public Review後の仕様書をもとに駆け足で紹介しました。JAX-RS 2.0は規格こそ承認されましたが、リファレンス実装であるJersey 2.0系の開発が追いついていないのが現状です。さらにJersey 2.0系がバンドルされているサーバーが開発中のGlassFish 4.0だけで、未実装の機能に加えGlassFish 4.0の完成度がまだ十分でないこともあり、本格的な検証が難しい状況にあります(検証に耐えうる品質に到達するのはJava EE 7の仕様がFixする2013年2月以降になると思われます)。

現状ではJAX-RS 2.0が自由に試せるまでしばらく時間があります。その間はJAX-RS 1.1への理解を深め、来たるべき新バージョンの基礎知識を蓄えておきましょう。


12月8日の担当は @tt4cs さんです。また、12月17日には筆者の助手である @yumix_h がまるで筆者への当てつけのように JAX-RS ネタを投稿する予定とのことです。どうぞお楽しみに!