最近の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サービスの価値を高めましょう。






