前回の記事でJAX-RSのAPIがCoC (Convention over Configuration)的な思想で作られているのが分かったのではないかと思います。
このCoC的なAPIは必要最低限のコードだけを書けば良い半面、「実はこんなアノテーションをつけるとこんな事ができるんだよー」という隠しコマンドのようなものがたくさん出てきてしまいます。
古くからある Servlet APIでは、doGet()メソッドに HttpServletRequestとHttpServletResponseオブジェクトが入っています。基本的な操作はこの2つのオブジェクトを通じて行なうため、そのオブジェクトのメソッドやプロパティを探れば「あー、こんな事ができそうだな」という予想が立てやすいです。今時はみなさんIDEを使って開発していると思いますが、IDEがあればメソッドの候補を出してくれたりしますし、ドキュメントを読ま込んでおかなくてもある程度の事は推測して掘って行く事ができます。
例えば、HTTPリクエストの User-Agentヘッダの値を読みたいとします。Servlet APIでは HttpServletRequestオブジェクトのメソッドを眺めれば、getHeader()というそれらしいメソッドがすぐに見つかります。
一方、JAX-RSの仕様に則って前回作った getCard() というメソッドでは、引数に idの文字列しか渡ってこない為、何をどうしたらHTTPヘッダの値を調べられるのか、全く見当がつきません。
@GET
@Path("/{id}")
Card getCard(@PathParam("id") String id);
昔ながらの方法になれていると「もやっと」した印象を受けてちょっと気持ち悪い気もしてしまうのですが、近年はこっちのCoC的なスタイルの方がトレンドのようです。理由は「テストがしやすい」からです。
JAX-RSのメソッドの方がServletよりテストしやすい
上記 getCard()メソッドは引数に id文字列、返り値が Cardオブジェクトとなっており、「idを入れたら対応するCardオブジェクトが取れる」という仕様がすぐに理解できます。この実装クラスをテスト際も、idの値を色々変えて実際にgetCard()メソッドを呼び出して期待するオブジェクトが返ってくるかをテストすれば良いだけですので、どこにもHTTP仕様は出てきません。
このメリットは、同様のサービスを提供する GetCardServlet というサーブレットをテストする事を考えてみるとよくわかります。
GetCardServletの doGetをテストする為には、HttpServletRequestとHttpServletResponseオブジェクトを作って doGet メソッドに渡す必要があります。GetCardServletが正しく動作するためには、それらにプロパティを事前にセットする必要がありますが、沢山あるプロパティのどれに何をセットしておけば良いのかを知らないといけません。
こういった理由から、近年の新しいAPIではHttpServletReqeuestのような「何でも取れる四次元ポケット的オブジェクト」は使わない方向になっています。
JAX-RSでHTTPリクエストヘッダを調べる
今回のメインのお題です。JAX-RSでHTTPヘッダを調べたい場合はどうすれば良いのでしょうか?
以下に2つの方法を紹介します。
@HeaderParamアノテーション
ある特定のHTTPヘッダの値が必要なのであれば、メソッドの引数に@HeaderParamアノテーションを付ければ取得できます。getCard メソッドの @PathParamと同じような感じです。
@GET
@Path("/{id}")
Card getCard(@PathParam("id") String id, @HeaderParam("User-Agent") String ua);
getCard メソッドに User-Agentヘッダの値を受け渡せるようにしました。このようにする事で例えば特定のUser-Agent文字列を持ったクライアントからしかアクセスを許さない、といった動作をさせる事ができます。
ヘッダを全部渡すのではなく、特定のヘッダだけを引数に渡すのは、先ほどの「テストがしやすい」という話にも合っています。上記 getCard メソッドのテストケースを考える人は、メソッドを見ただけで、idと uaの2つの値を振ってテストすれば良いと言う事がすぐにわかります。逆に言うと User-Agent以外のヘッダは一切見ていない事が保証されるので、Refererヘッダを変えてテストする必要が無いわけです。
HttpHeaders型の引数
しかし、HTTPのヘッダ名を事前に限定できないケースもあります。ヘッダの数を表示したい、ヘッダ一覧をログに出したいといったケースです。そういう場合は、@Contextというアノテーションと共に、HttpHeaders型の引数を用意してあげればOKです。
@GET
@Path("/{id}")
Card getCard(@PathParam("id") String id, @Context HttpHeaders headers);
JAX-RSで使えるその他のパラメータ
すでにご紹介した@PathParamや@HeaderParam以外にも、JAX-RSで使えるパラメータはいろいろあります。
@QueryParam
URLのクエリパラメータ、つまり ?a=b というパラメータの値を取得する際に使用します。例えば、猫の写真が以下のURLから取得できるとします。
GET /animal/cat/picture
例えば formatというパラメータを指定すると、写真のフォーマットを指定できるようにしたい場合などがあります。
GET /animal/cat/picture?format=png
このような場合は次のようにして簡単にformatの値”png”を取得できます。
@GET
@Path("/animal/{type}/picture")
StreamingOutput getPicture(@PathParam("type") String type, @QueryParam("format") String format);
@MatrixParam
マトリックスパラメータの値を取得したい場合に使用します。マトリックスパラメータというのは、URLのパスの中にセミコロン(;)で区切って入れられるパラメータの事です。例えば猫の写真の例で、猫の毛の色を指定したい場合に、次のように指定します。
GET /animal/cat;color=black/picture
この color=black というのがマトリックスパラメータです。
@GET
@Path("/animal/{type}/picture")
StreamingOutput getPicture(@PathParam("type") String type, @MatrixParam("color") String color);
クエリパラメータとは何が違うのでしょう?ポイントは、マトリックスパラメータはパスの途中、つまりスラッシュ(“/”)で区切られたパスの要素に対してパラメータを付加できるところにあります。上記例であれば、colorというパラメータは catに対して付けたオプションだと言う事がよくわかりますが、?color=black というオプションが後ろにある場合だとすこし意味があいまいになります。
@FormParam
HTMLのフォームがPOSTされた際に、それに含まれているパラメータを取得する際に使用します。HTMLのフォームがポストされると、Content-Typeが application/x-www-form-urlencoded のリクエストエンティティが送られてきます。その中に入っているフォームの値を取得できるわけですね。
@CookieParam
送られてきたクッキーの値を取得する際に使用します。CookieはHTTPヘッダに入っているので、@HeaderParamや HttpHeadersなどを使って調べる事もできますが、先ほどご説明したように、テストのしやすさの観点から、特定のクッキーが必要なのであれば @CookieParam を使うべきです。
その他の便利な機能
同じ名前のパラメータが複数ある場合
@QueryParamや、@HeaderParam、@FormParamでもそうですが、もし該当するidのパラメータが複数あった場合、どうしたら良いでしょう?次のようなケースです。
GET /animal/cat/picture?name=tama&name=mike
tama と mike の名前がどちらも nameというパラメーで渡されています。この両方を知りたい場合は、単に引数の型を List<String> にするだけです!
@GET
@Path("/animal/{type}/picture")
StreamingOutput getPicture(@PathParam("type") String type, @QueryParam("name") List<String> names);
便利すぎます、というか空気読め過ぎですね。
パラメータがない場合のデフォルト値
パラメータの値がない場合、nullが渡ってきてしまいますが、@DefaultValueというアノテーションを使うとデフォルト値をセットしてくれるようになります。
@GET
@Path("/animal/{type}/picture")
StreamingOutput getPicture(@PathParam("type") String type, @DefaultValue("jpeg") @QueryParam("format") String format);
これで ?format= という指定がない場合は jpegが返るようになります。
ちょっとアノテーションがごてごてしすぎな感じもしますが、インターフェースを見ただけで動作が分かるという点では非常に優れていると思います。
認証と認可
JAX-RSのメソッドの呼び出しにユーザ認証を掛けたい場合や、ログインしているユーザごとに結果を変えたい場合があると思います。例えば、
GET /profile
という呼び出しでログインしているユーザの情報を取得したい場合があるとおもいます。あるいは、
GET /profile/{userid}
というようにして、ユーザのIDを明示的に指定する方法もありますが、他人のプロフィールを見られないようにしたい場合は良くあります。@CookieParamや@HeaderParamなどでクッキーやHTTPヘッダからユーザを区別する事も可能ですが、JAX-RS的には、@RolesAllowedアノテーションや @SecurityContextアノテーションを使うのが素直です。
@RolesAllowedアノテーション
メソッドやクラスに@RolesAllowd(“role名”) というアノテーションを付けると、特定のロールに属したユーザからのアクセスの時だけメソッドが呼び出され、それ以外の時は 401 Unauthorizedが返るようになります。
@SecurityContextアノテーション
@SecurityContextアノテーションはメソッドの引数に与えるもので、引数の型には javax.ws.rs.core.SecurityContext型を使います。
@GET
@Path("/{id}")
Card getCard(@SecurityContext SecurityContext sec, @PathParam("id") String id);
このようにするとsecオブジェクトから、ユーザやロールを取得する事ができるようになるので、メソッドの実装の中で正しいユーザがアクセスしているかを検査する事ができるようになります。
より柔軟なユーザ認証を行なうには
ここで言うユーザやらロールやらというのは、Java EE のアプリケーションサーバ(コンテナ)が認識しているユーザ/ロールの事で、例えばTomcatであれば、tomcat-users.xmlなどで指定したユーザ/ロールになります。
ただ、コンテナが提供してくれる認証は使いづらい部分もあり、細かく制御したい場合は Servlet フィルタや HttpServletRequestWrapper などを駆使する必要があります。要は、JAX-RSは HttpServletRequestの isUserInRole()メソッドや、getRemoteUser()、getUserPrincipal()などを呼び出しているだけなので、それらが適切な動作をするようにラップしてください。
このあたりは余力がある時にでも解説したいと思います。
次回
HTTPリクエストをうにゃうにゃいじる方法はだいたいこんな感じでOKでしょうか。
次回はHTTPレスポンスをいじりたい場合について解説したいと思います。

Twitter