JAX-RSでJSONのやりとり

このエントリはJava EE Advent Clanedar2015の16日目の記事です。
昨日はaf-not-foundさんの「Spring Bootでお手軽ゼロダウンタイムデプロイ」でした。
明日は@opengl-8080さんの担当になります。

【前提条件】

[環境]
  • JDK 1.8.66
  • Paraya 4.1.1.154

【概要】

個人的にはJava EE内ではJAX-RS推しなので今年もJAX-RSネタです。

JAX-RSはRESTFulサービスを実現するためのAPI仕様です。

【サンプルコードについて】

今回のサンプルコードはGitHubに置いてあります。
https://github.com/gloryof/JaxrsJson/tree/y2015/

今回のメインではない箇所についてはブログでは説明しませんので、
細かい部分についてはGitHubのソースを見てください。

[サンプルの概要]

個人の簡単なプロフィールを登録するAPI
保存されたプロフィールの一覧を取得するAPIを作ります。
Hello world」っぽいやつだとつまらないかなと思い、ちょっと複雑にしてみました。

[登録APIの仕様]

登録APIのパラメータの構成は下記です。
結果はHTTPステータスのみを受け取ります。
ソースコードを書くと長くなってしまうので概要を書いています。

  • Profile:Class(プロフィール)
    • Name:Class(名前)
      • Last:String(苗字)
      • First:String(名前)
    • Type:Enum(タイプ[YOUTH or VETERAN])
    • Age:int(年齢)

Profile、Nameはそれぞれ独自のクラスです。
TypeはEnumです。

[一覧取得APIの仕様]

一覧取得APIはパラメータなしで、下記の構成の結果を返します。

  • ProfileResult:Class(プロフィール結果)
    • Summary:Class(サマリ)
      • TotalCount:int(合計件数)
      • YouthCount:int(タイプでYOUTHを選んだ数)
      • VeteranCount:int(タイプでVETERANを選んだ数)
    • Profiles:List(プロフィールのリスト)

ProfileResult、Summaryはそれぞれ独自のクラスです。

【設定】

[pom.xml]

まずはpom.xmlです。
今回はJAX-RSだけで構成するというコンセプトでやっているので、
CDIと連携させたりということはしていません。

<dependencies>
    <dependency>
            <groupId>javax.ws.rs</groupId>
            <artifactId>javax.ws.rs-api</artifactId>
            <version>2.0.1</version>
    </dependency>
</dependencies>

dependenciesの設定を抜き出してきました。
JAX-RSJava仕様に対する依存のみです。

JAX-RSはあくまで仕様です。
今回のサンプルは実装がJerseyでもRESTEasyでも動くはずです。
私の環境ではPayaraを使用しているのでJerseyが使用されています。

[JAX-RSの基本設定]

続いてJAX-RSを動かすための設定を行います。

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("api")
public class ApplicationSetting extends Application {

}

Applicationクラスを継承したクラスを作成し、ApplicationPathアノテーションをつけるだけです。
これで「/${コンテキストパス}/api」以下がJAX-RSのリソースクラスとして認識されます。

[余談]
JeresyMVCとかを使う場合はこのクラスに設定用のコードを書くことになります。

JAX-RSのリソースクラス】

さて、ようやくJAX-RSのコードについて解説していきます。

JAX-RSではサービスのエンドポイントをリソースクラスと呼んでいます。
HTTPにおけるリソースとほぼ同じ意味かなと思います。

リソースクラスはGitHub上ではこちらになります。
ソース上、データを作成する箇所がありますが、JAX-RSの本質的なところではないので説明は省略します。

[クラス定義]

まずはクラスの定義部分から。

@Path("profile")
public class ProfileHolder {
// 省略
}

Pathアノテーションに文字列を渡すことによりリソースクラスが登録されます。
ApplicationPathアノテーションで指定したパス + Pathアノテーションでしてしたパスがリソースのパスになります。

JAX-RSの基本設定」で解説したところでは

@ApplicationPath("api")

のように指定していました。

そのためProfileHolderのパスは「/${コンテキストパス}/api/profile」になります。
この段階ではメソッドがないためアクセスをしても何も起きません。

[登録API]

続いて登録APIのメソッドを見ていきましょう。

@POST
@Path("add")
@Consumes("application/json")
public Response save(Profile profile) {

    System.out.println("==================== Profile Save  ====================");
    System.out.println("Name : " + profile.getName().getLast() + " " + profile.getName().getFirst());
    System.out.println("Type : " + profile.getType().name());
    System.out.println("Age : " + profile.getAge());

    profiles.add(profile);

    return Response.ok().build();
}

POSTアノテーションをつけるとPOSTメソッドだけを受け取ります。
(細かい話をするとOPTION/HEADについては許可するはずです)

Pathアノテーションが再び出てきました。
メソッドに対するPathアノテーションはなくても動きます。
今回は「/${コンテキストパス}/api/profile/add」というパスにするためにつけています。

ConsumesアノテーションではどのContent-Typeを指定します。
今回はJSONでのやりとりを行うので「application/json」にしています。

saveメソッドではProfileクラスのパラメータを受け取って、標準出力に出力して、リストに追加する。
という普通のJavaの処理のみを行っています。
HTTPリクエストとBeanのプロパティのマッピングですが、リソースクラスでは意識しません。

今回はHTTPステータスの200を返すので、Response.ok().build()でResponseオブジェクトを返却しています。
Response.okメソッドには任意のオブジェクトを詰めることができますが、今回は何もせずに返しています。

[一覧取得API]

続いて一覧取得のAPIです。

@GET
@Path("get")
@Produces("application/json")
public ProfileResult list() {

    ProfileResult result = new ProfileResult();
    result.setSummary(Summary.build(profiles));
    result.setProfiles(profiles);

    return result;
}

GETアノテーションでGETメソッドのみ許可、Pathアノテーションでパスを指定。
というところまでは登録APIとほぼ同じです。

Producesアノテーションはレスポンスで返却するContent-typeを指定します。

一覧取得メソッドではResponseオブジェクトではなくBeanで返却しています。
Beanが返却された場合はHTTPステータスは200でレスポンスが返ります。

こちらもアノテーションをつける以外は特別なことはしていません。

【リソースクラス以外】

[Bean]

JAX-RSJSONを取り扱う場合はBeanにアノテーションをつける必要があります。
ネストされるBeanも含めてクラスにXmlRootElementアノテーションをつけます。
サンプルでは下記のクラスになります。

@XmlRootElement
public class ProfileResult { ... }

@XmlRootElement
public class Profile { ... }

@XmlRootElement
public class Summary { ... }
[Enum]

valueOf/fromStringメソッドがあるとJAX-RSは文字列から対象のオブジェクトに変換してくれます。
今回のEnumはもともとvalueOfがあるので自動で変換されています。

【動作確認】

動作確認はPayaraにデプロイして確認します。
デプロイしたのちに「http://localhost:8080/JaxrsJson/」にアクセスします。

こんな感じの画面が表示されます。

[保存]ボタンをクリックすると登録フォームに入力された内容を登録APIにリクエストします。

[一覧ロード]ボタンをクリックすると一覧取得APIがリクエストされます。
帰って来たJSONを元にJavaScriptでテーブルをレンダリングしています。

[登録API]

登録フォームのすべての項目を入力して保存ボタンをクリックしてみましょう。

では、クライアント側からどのようなリクエストが送られているかを見てみましょう。
JavaScriptでコンソール出力をしているので、ブラウザから確認することができます。
IEだと[object object]が出力されるとおもいます・・・)

{
    "name": {
        "last": "ほげほげ",
        "first": "ふがふが"
    },
    "type": "VETERAN",
    "age": "54"
}

出力された内容を整形したものが上記になります。
Beanと同じ構成で、Enumに対応する部分はEnum.name()の値が入っています。

つづいてサーバサイドです。

情報:   ==================== Profile Save  ====================
情報:   Name : ほげほげ ふがふが
情報:   Type : VETERAN
情報:   Age : 54

サーバのコンソールに上記のような内容が出力されます。
Beanにマッピングされているのがわかります。

[一覧取得API]

[一覧ロード]ボタンをクリックします。
こちらもJavaScriptでコンソール出力をしているので、ブラウザから確認することができます。

{
    "profiles": [{
        "age": 25,
        "name": {
            "first": "太郎",
            "last": "山田"
        },
        "type": "YOUTH"
    },
    // 長いので1件だけ
    ],
    "summary": {
        "totalCount": 4,
        "veteranCounts": 3,
        "youthCounts": 1
    }
}

サーバサイドのBean通りの構成で返却されていることがわかります。

【まとめ】

サンプルの動きを見ていただいた通り、JAX-RSではPOJOアノテーションのみでJSONを受け取るAPIが作れます。
JAX-RSのコードが出現している箇所のほとんどはアノテーションです。
Responseオブジェクトを使用しているので完全には異存なしとは言えないですが。

MVC1.0についてはJAX-RSに乗っかるという話なので、
画面が必要になるWebアプリについても今後は対応することが予想されます。

サーバサイドレンダリング、クライアントサイドレンダリング、ネイティブアプリ・・・
Webに関するところはますますAPI化が進んでいくのかなぁと個人的に思います。

現状、Twitterなどの大きなサービスだけがAPIを提供となっていますが、
エンタープライズ部分や小さなサービスについてもAPI化されているのではないかなと思います。

JAX-RSでベースを作って必要であればMVC1.0で画面を作っていくことになるのではないでしょうか?