JavaEEでもできる!JAX-RSでお手軽REST開発


最近の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つ。

  1. @ApplicationPath("/api")というアノテーションにより/api以下がJAX-RSで扱うURLになります
  2. 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_first

リソースクラスのポイント

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というパスの階層毎に対応するアノテーションをまとめてみましたので、イメージしてみてください。

jax-rs_url_path_mapping

メソッドに付与された@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

jax-rs_resource-class_queryparameter

確かにクエリパラメータが受け取れていますね。

@FormParamアノテーションでformのパラメータを受け取る

HTMLのformタグで送信するデータはapplication/x-www-form-urlencodedという形式であり、次のような書式のデータです。

param1=value1&param2=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つのアノテーションを併用します。

  1. メソッドの@PathアノテーションでURLパラメータを表現
  2. メソッド引数に@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/17

jax-rs_pathparam

HTTPリクエストのヘッダ情報を受け取る

時にはクライアントから送信されてきた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/accept

jax-rs_header

Servlet 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";
}

上記のように、HttpServletRequestServletContextは取得可能ですが、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の時に動作する変換用クラスを作成します。MessageBodyReaderMessageBodyWriterインターフェイスを実装し、@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

jax-rs_json_01

 

レスポンスが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サービスの価値を高めましょう。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>