最近のWebアプリケーションはRESTインターフェイスによるAPIを備えることが多くなってきました。
APIはWebアプリケーションの用途を大きく広げる重要な要素であり、また、これをシンプルなインターフェイスであるREST形式で提供することは、より APIを利用しやすくするための重要な手法となっています。
本エントリではJavaEEでRESTインターフェイスを提供するための仕様であるJAX-RSをご紹介します。
JAX-RSとは
前述のように、RESTによるAPI提供の重要性は増すばかりです。JavaEEにおいても、JavaEE6からRESTインターフェイスを開発するための仕様であるJAX-RSが提供されるようになりました。
JAX-RSの特徴として、アノテーションを使ったシンプルな開発スタイルが挙げられます。まずは基本的な使い方を見てみましょう。
最小のサンプル
それではJAX-RSの最小のサンプルを作ってみましょう。JavaEEコンテナ環境下であれば、たった2つのファイルを作るだけで済みます。
JAX-RSの設定クラス
まずはJAX-RSの設定を持つクラスを作りましょう。
package sandbox.web.api; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; @ApplicationPath("/api") public class RestApplication extends Application { // nodef }
ポイントは2つ。
@ApplicationPath("/api")
というアノテーションにより/api
以下がJAX-RSで扱うURLになりますjavax.ws.rs.core.Application
クラスを継承します。
クラス名はどんな名前でも構いません。また実装は何も必要ありません。
リソースクラス
JAX-RSによる開発の主たる作業は、リソースクラスを作ることです。
リソースというのは情報の断片のことで、URLで識別されるものです。リソースクラスは、リソースに対する操作をリクエストに従って処理するクラスとなります。
ここでは次のような仕様のリソースを、リソースクラスで実装してみましょう。
Hello, World
というテキストをリソースとする/api/hello/text
というパスに対するGETメソッドでテキストが得られる- テキストのContent-Typeは
text/plain
クラス名は何でもいいのですが、ここでは分かりやすくHelloResource
としましょう。
package sandbox.web.api; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("/hello") public class HelloResource { @Path("/text") @GET @Produces(MediaType.TEXT_PLAIN) public String asText() { return "Hello, World"; } }
仕様がそのままアノテーションで表現されていますね。
ではブラウザで次のURLにアクセスしてみましょう。
http://localhost:8081/api/hello/text次のような画面が表示されるはずです。
リソースクラスのポイント
JAX-RSの根幹、リソースクラスを詳しく見てみましょう。とはいえ非常に直感的なコードになっていますので、読み解きやすいと思います。
@PathアノテーションとURLの関係
クラスとメソッドに@Path
アノテーションを付与することで、このリソースクラスにアクセスするためのパス(≒URLの一部)を表現しています。
クラス宣言の@Path
アノテーションは、このリソースが/hello
というパスで表されることを示しています。
@Path("/hello") public class HelloResource
メソッド宣言の@Path
アノテーションは、このリソースに実際にアクセスするためのパスを示しています。
@Path("/text") @GET @Produces(MediaType.TEXT_PLAIN) public String asText()
実際にアクセスするためのパスは、JAX-RSの設定クラスに付与した@ApplicationPath
アノテーションにも影響されることに注意してください。
/api/hello/text
というパスの階層毎に対応するアノテーションをまとめてみましたので、イメージしてみてください。
メソッドに付与された@GET/@Producesアノテーション
リソースクラスのメソッドには、@Path
の他にも2つのアノテーションが付いています。いずれも重要なアノテーションです。
@GET
アノテーションは、メソッドが動作するHTTPメソッドがGETであることを示しています。HTTPメソッドに該当するアノテーションとして、他にも@PUT/@POST/@DELETEなどがあります。
@Produces
アノテーションは、このメソッドが返す値をどのようなデータ形式で返すかを示していて、HTTP応答ヘッダ内のContent-Type
を指定していることに相当します。
このようにJAX-RSによるREST開発は、HTTPインターフェイスをアノテーションを駆使して表現するというスタイルになります。
それではJAX-RSが提供する様々な機能を見ていきましょう。多数の機能があるのですが、重要なものに絞ってご紹介します。
POSTメソッドに対応する
POSTメソッドでアクセスされた時に動くメソッドには@POST
アノテーションを付与します。
@Path("text") @POST public void postText(@FormParam("text") final String pText) { System.out.println(pText); }
パラメータを@FormParam
アノテーションで受け取っています。これは後ほど、もっと詳しく解説します。
同じ考え方で、PUTメソッドやDELETEメソッドにも対応できます。単に@PUT
や@DELETE
アノテーションを付与すればいいのです。直感的ですね。
@QueryParamアノテーションでクエリパラメータを受け取る
クエリパラメータとは、URLの?以降の部分を言います。例えば以下のURLのクエリパラメータはformat=json
です。
http://example.com/api/hello/text?format=json
これをリソースクラスで受け取るには、メソッド引数に@QueryParam
アノテーションを付与します。
@Path("/text-with-format") @GET @Produces(MediaType.TEXT_PLAIN) public String getTextWithFormat(@QueryParam("format") final String pFormat) { return "Hello, World" + "(format=" + pFormat + ")"; }
ブラウザで次のURLにアクセスしてみてください。
http://localhost:8081/api/hello/text-with-format?format=json確かにクエリパラメータが受け取れていますね。
@FormParamアノテーションでformのパラメータを受け取る
HTMLのformタグで送信するデータはapplication/x-www-form-urlencoded
という形式であり、次のような書式のデータです。
param1=value1¶m2=value2
JAX-RSではこの形式のデータは特別扱いであり、メソッドの引数に@FormParam
アノテーションを付与することで値を受け取ることができます。
@Path("text") @POST @Produces(MediaType.TEXT_PLAIN) public void postText(@FormParam("text") final String pText) { System.out.println(pText); }
少し横道・JAX-RSのクライアントAPIによるテスト
RESTインターフェイスのテストを実施する際、GETであればブラウザで手軽にテストできますが、POSTやPUT、DELETEはそうはいきません。ところがJAX-RSにはクライアント用のAPIが備わっており、手軽にリソースクラスをテストすることができます。
JAX-RSのクライアント APIを使ってPOSTを送るサンプルを掲載しておきます。
public static void main(final String[] pArgs) { final Response response = ClientBuilder.newClient() // .target("http://localhost:8081/api") // 実行するWeb APIのエントリポイント .path("/hello/text") // リクエストを投げるURLのパス部分 .request(MediaType.TEXT_PLAIN_TYPE) // 受け入れ可能なレスポンス形式(HTTPヘッダでいうAcceptに相当) // application/x-www-form-urlencoded形式でデータを作成(HTTPヘッダでいうContent-Typeに相当)し、リクエストを送信. .post(Entity.<String> entity("text=hoge", MediaType.APPLICATION_FORM_URLENCODED_TYPE)); System.out.println(response.getStatusInfo()); }
パスパラメータを受け取る
URLの一部がパラメータになっているケースがよくあります。例えばよくあるブログエントリのURLがこれに相当します。
http://example.com/blog/2015/12/17
2015年12月17日のエントリという意味ですね。JAX-RSでこのようなパラメータを受け取るには、2つのアノテーションを併用します。
- メソッドの@PathアノテーションでURLパラメータを表現
- メソッド引数に@PathParamアノテーションを付与し値を受け取る
以下、サンプルです。
@Path("/date/{year}/{month}/{date}") @GET @Produces("text/plain; charset=UTF-8") public String getDateText( // @PathParam("year") final int pYear // , @PathParam("month") final int pMonth // , @PathParam("date") final int pDate // ) { return String.format("%d年%d月%d日", pYear, pMonth, pDate); }
@Path
アノテーションに{パラメータ名}
というスタイルでパラメータを表現しているのが分かると思います。
ブラウザで次のURLにアクセスしてみてください。
http://localhost:8081/api/hello/date/2015/12/17HTTPリクエストのヘッダ情報を受け取る
時にはクライアントから送信されてきたHTTPヘッダの情報が必要な時があります。この場合、HttpHeaders
型のメソッド引数を作り@Context
アノテーションを付与します。するとHttpHeaders
を通してヘッダ情報が得られます。
@Path("/headers/{headerName}") @GET @Produces(MediaType.TEXT_PLAIN) public String getRequestHeaderValue( // @PathParam("headerName") final String pHeaderName // , @Context final HttpHeaders pHeaders) { return String.valueOf(pHeaders.getRequestHeader(pHeaderName)); }
ブラウザで次のURLにアクセスしてみてください。
http://localhost:8081/api/hello/headers/acceptServlet APIにアクセスする
リソースクラスの中でServlet APIにアクセスしたい場合、メソッド引数にServlet APIのオブジェクトを指定し、そこに@Context
アノテーションを付与します。
@Path("/text") @GET @Produces(MediaType.TEXT_PLAIN) public String asText( // @Context final HttpServletRequest pRequest // , @Context final ServletContext pContext) { System.out.println(pRequest); System.out.println(pContext); return "Hello, World"; }
上記のように、HttpServletRequest
とServletContext
は取得可能ですが、HttpSession
はこの方法では取得できません。本来、RESTとHttpSessionは相容れないものですのでこの仕様は妥当と言えますが、どうしてもHttpSessionが必要な場合は、次のようにして取得してください。
@Path("/session-info") @GET @Produces(MediaType.TEXT_PLAIN) public String sessionInfo(@Context final HttpServletRequest pRequest) { return String.valueOf(pRequest.getSession()); }
CDIの適用
リソースクラスはJavaEEコンテナ管理であるためCDI連携が可能です。ただ、CDI対象であることをコンテナに示すために、クラス宣言に@Dependent
アノテーションを付与する必要があることに注意してください。
このことを踏まえると、リソースクラスは下記のようなコードになります。
@Path("/hello") @Dependent public class HelloResource { @Inject HogeService hogeService;
なおCDIの詳しいことについては本ブログの「JavaEE屈指の便利機能、CDIを触ってみよう」というエントリをご参照ください。
JSONでデータをやり取りする
最後に、実践的な内容としてJSONでデータをやり取りする方法をご紹介します。
昨今のWebAPIの多くはJSON形式でデータをやり取りしますが、JAX-RSはデフォルトではJSONを扱えません。多少の設定追加が必要ですので、この章で見て行きましょう。
データ変換用のクラスを作成する
JAX-RSには、Content-Typeに応じてJavaオブジェクトを変換する処理を選択/実行する機能が備わっています。
ここではContent-Typeがapplication/json
の時に動作する変換用クラスを作成します。MessageBodyReader
とMessageBodyWriter
インターフェイスを実装し、@Provider
アノテーションを付与するのがポイントです。
実装すべきメソッドは5つあります。各メソッドの意味はコメントに書いておきました。どれも引数が多く見にくいかもしれませんが、実際のメソッドの中身はどれも1行ですので、臆さず読んでみてください。
package sandbox.jax_rs; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import javax.ws.rs.Consumes; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.MessageBodyWriter; import javax.ws.rs.ext.Provider; import net.arnx.jsonic.JSON; @Provider @Consumes(MediaType.APPLICATION_JSON) // リクエストのContent-Typeがapplication/jsonの時に当クラスが実行される @Produces(MediaType.APPLICATION_JSON) // レスポンスのContent-Typeがapplication/jsonの時に当クラスが実行される public class JsonConverter implements MessageBodyReader<Object>, MessageBodyWriter<Object> { /** * クライアントに返すデータの長さを返しますが、不明な場合は-1を返します. */ @Override public long getSize(final Object pT, final Class<?> pType, final Type pGenericType, final Annotation[] pAnnotations, final MediaType pMediaType) { return -1; } /** * クライアントから送られてきたデータのContent-Typeが、このクラスで処理可能かどうかを返します. <br> * このクラスの場合、application/jsonを処理するようにします. */ @Override public boolean isReadable(final Class<?> pType, final Type pGenericType, final Annotation[] pAnnotations, final MediaType pMediaType) { return pMediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE); } /** * クライアントに送り返すデータのContent-Typeが、このクラスで処理可能かどうかを返します. <br> * このクラスの場合、application/jsonを処理するようにします. */ @Override public boolean isWriteable(final Class<?> pType, final Type pGenericType, final Annotation[] pAnnotations, final MediaType pMediaType) { return pMediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE); } /** * クライアントからのデータをJavaオブジェクトに変換して返します. <br> * ここではクライアントからのデータをJSONと決め打ちして処理します. <br> */ @Override public Object readFrom(final Class<Object> pType, final Type pGenericType, final Annotation[] pAnnotations, final MediaType pMediaType, final MultivaluedMap<String, String> pHttpHeaders, final InputStream pEntityStream) throws IOException, WebApplicationException { return JSON.decode(pEntityStream, pType); } /** * Javaオブジェクトを変換してクライアントに返します. <br> * ここではJSONに変換します. <br> */ @Override public void writeTo(final Object pT, final Class<?> pType, final Type pGenericType, final Annotation[] pAnnotations, final MediaType pMediaType, final MultivaluedMap<String, Object> pHttpHeaders, final OutputStream pEntityStream) throws IOException, WebApplicationException { JSON.encode(pT, pEntityStream); } }
なおこJSONとJavaオブジェクトの変換にはJSONICというライブラリを使っています。
リソースクラスでJSONレスポンスを返す
それではリソースクラスにJSON形式でデータを返すメソッドを追加しましょう。
@Path("/json") @GET @Produces(MediaType.APPLICATION_JSON) public Map<String, Object> asJson() { final Map<String, Object> ret = new HashMap<>(); ret.put("response", "Hello, World"); return ret; }
@Produces(MediaType.APPLICATION_JSON
)
というアノテーションにより、レスポンスをJSON形式にすることを示しています。
それではブラウザで次のURLにアクセスして下さい。
http://localhost:8081/api/hello/json
レスポンスがJSON形式のデータであることや、レスポンスヘッダのContent-Typeがapplication/jsonになっていることが確認できます。
リソースクラスでJSONを受け取る
次にJSON形式のデータをPOSTで受け取るメソッドを作ってみましょう。
@Path("/json") @POST @Consumes(MediaType.APPLICATION_JSON) public void postJson(final Map<String, Object> pJson) { System.out.println(pJson.get("request")); }
@Consumes(MediaType.APPLICATION_JSON
)
というアノテーションで、リクエストのContent-Typeがapplication/jsonである場合にこのメソッドが動作することを示しています。
クライアントAPIで動作確認をしてみましょう。
public static void main(final String[] pArgs) { final Response response = ClientBuilder.newClient() // .target("http://localhost:8081/api") // .path("/hello/json") // .request() // .post(Entity.<String> entity("{ \"request\": \"This is json request.\" }", MediaType.APPLICATION_JSON)); System.out.println(response.getStatusInfo()); }
このメソッドを実行すると、リソースクラスのpostJson()メソッドが動作することが確認できるはずです。
まとめ
駆け足でJAX-RSを見てきました。実にシンプルな上、RESTの考え方にマッチした分かりやすい仕様になっているため、学びやすいと思います。
本エントリでは紹介しきれなかった機能の中には、サブリソースやキャッシュ制御、レスポンスの詳細な制御など重要なものがあります。幸い、オライリーから良いJAX-RSの指南書が出ています。分量が少なく読みやすいため、ぜひご一読をおすすめします。
冒頭に書いたように、Web APIの重要性は増すばかりです。JAX-RSを使ってRESTフルなAPIをどんどん開発し、Webサービスの価値を高めましょう。