JAX-RS のリファレンス実装 Jersey のフィルタを使う方法

JAX-RS を使っていると、リソースの処理が呼ばれる前か後に何らかの処理をはさみたい、ということがあると思います。Jersey ではそういったニーズにフィルタで対応できます。

Jersey のフィルタにはリクエスト/レスポンスを一律処理するフィルタ (ContainerRequestFilter, ContainerResponseFilter) と、各リソースで個別に設定できるフィルタ (ResourceFilter) の二種類があります。

ContainerRequestFilter

まず ContainerRequestFilter ではリクエストに対して一律、リソースの処理が実行される前の処理が記述できます。例として、実用性皆無ですが HTTPメソッド をすべて PUT に書き換えるフィルタを書いてみます。

package test.jersey.filter;

import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.container.ContainerRequestFilter;

public class MethodReplaceFilter implements ContainerRequestFilter {
	
	public ContainerRequest filter(ContainerRequest request) {
		request.setMethod("PUT");
		return request;
	}
	
}

完成したフィルタを web.xml で登録します。フィルタは Jersey のサーブレットに init-param com.sun.jersey.spi.container.ContainerRequestFilters で渡します。ちなみに複数のフィルタを登録する場合はセミコロン ";" で区切ります。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	id="WebApp_ID" version="2.5">
	<display-name>Jersey Filter Test</display-name>
	<servlet>
		<servlet-name>Jersey Web Application</servlet-name>
		<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
		<init-param>
			<param-name>javax.ws.rs.Application</param-name>
			<param-value>test.jersey.filter.MyWebApplication</param-value>
		</init-param>
		<init-param>
			<param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name>
			<param-value>test.jersey.filter.MethodReplaceFilter</param-value>
		</init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>Jersey Web Application</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>

</web-app>

動作の確認用のリソースは以下のように作りました。

package test.jersey.filter;

import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;

import test.jersey.filter.TestResourceFilterFactory.FilterTarget;

@Path("/")
public class TestResource {

	@GET
	@Produces("text/plain")
	public Response get() {
		return Response.status(200).entity("GET").build();
	}
	
	@PUT
	@Produces("text/plain")
	public Response put() {
		return Response.status(200).entity("PUT").build();
	}
}

さきほどの Webアプリケーション をサーブレットコンテナにデプロイした状態で API を HTTP GET で叩いてみます。フィルタが有効に動作していれば HTTPメソッド が GET にもかかわらず内部的には PUTアノテーション で修飾した処理 (API) が呼ばれるはずです。

HTTP GET を呼びましたが、フィルタで HTTPメソッド が上書きされたためレスポンスには PUT が返って来ました。フィルタが上手く動作したようです。動作テストには Firefox plugin の REST client for Firefox を使っています。

ContainerResponseFilter

次に ContainerResponseFilter です。またもや実用性皆無ですが、エンティティの内容を String で上書きするフィルタを書いてみます。フィルタは ContainerRequestFilter と同様 init-param で com.sun.jersey.spi.container.ContainerResponseFilters で渡します。

package test.jersey.filter;

import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.container.ContainerResponse;
import com.sun.jersey.spi.container.ContainerResponseFilter;

public class EntityReplaceFilter implements ContainerResponseFilter {

	public ContainerResponse filter(ContainerRequest request,
			ContainerResponse response) {
		response.setEntity("Overwrited!!");
		return response;
	}

}

動作テストするとレスポンスの内容が上書きされていることがわかります。

ResourceFilter

最後に ResourceFilter です。インタフェース ResourceFilter を実装したフィルタを ResourceFilterFactory で返して使います。
ResourceFilterFactory は何らかのルールに従ってフィルタを生成します。「何らかのルール」にはアノテーションの修飾を使うことが多いと思います。
例ではフィルタで処理する API のメソッドを FilterTarget アノテーションで修飾して識別することにします。フィルタの処理自体は ContainerResponseFilter と同様、レスポンスのエンティティを上書きするだけです。

package test.jersey.filter;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.List;

import com.sun.jersey.api.model.AbstractMethod;
import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.container.ContainerRequestFilter;
import com.sun.jersey.spi.container.ContainerResponse;
import com.sun.jersey.spi.container.ContainerResponseFilter;
import com.sun.jersey.spi.container.ResourceFilter;
import com.sun.jersey.spi.container.ResourceFilterFactory;

public class TestResourceFilterFactory implements ResourceFilterFactory {

	public List<ResourceFilter> create(AbstractMethod am) {
		// メソッドが FilterTarget で修飾されていればフィルタする
		if (am.getAnnotation(FilterTarget.class) != null) {
			return Collections.<ResourceFilter>singletonList(new Filter());
		}
		// フィルタしない
		return null;
	}
	
	public static class Filter implements ContainerResponseFilter, ResourceFilter {

		@Override
		public ContainerRequestFilter getRequestFilter() {
			return null;
		}

		@Override
		public ContainerResponseFilter getResponseFilter() {
			return this;
		}
		
		@Override
		public ContainerResponse filter(ContainerRequest request,
				ContainerResponse response) {
			response.setEntity("Overwrited!!");
			return response;
		}

	}
	
	@Inherited
	@Target({ElementType.METHOD})
	@Retention(RetentionPolicy.RUNTIME)
	public @interface FilterTarget {

	}

}

リソースの修正は一箇所だけ、フィルタ処理の対象にしたい API のメソッドにアノテーションをつけるだけです。

	@FilterTarget // このメソッドがフィルタ対象
	@GET
	@Produces("text/plain")
	public Response get() {
		return Response.status(200).entity("GET").build();
	}

web.xml には init-param com.sun.jersey.spi.container.ResourceFilters に ResourceFilterFactory を実装したクラスを指定します。

		<init-param>
			<param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>
			<param-value>test.jersey.filter.TestResourceFilterFactory</param-value>
		</init-param>

動作テストすると GET だけがフィルタの対象になっていることがわかります。

ResourceFilterFactory と ResourceFilter の組み合わせは、フィルタの対象やフィルタの種類を細かく制御したいときに便利だと思います。

まとめ

Jersey では二種類のフィルタを自分でカスタマイズして使うことができます。一律処理したいときは ContainerRequestFilter, ContainerResponseFilter が便利です。細かく制御したいのであれば ResourceFilter, ResourceFilterFactory を使います。

また、現在 JAX-RS の次のバージョンとして 2.0 (JSR 339) の仕様の策定が進んでいます。Early Draft の内容を見るかぎり Jersey のフィルタによく似た機能として RequestFilter, ResponseFilter が規定されているようです。やはりフィルタは便利、というより必要なので仕様に盛り込んだ、ということでしょうか。

Jersey OAuth を使って Twitter API にアクセスする

Jersey には OAuth で認証するためのモジュールが用意されています。今回はそれを使ってみた時のメモです。内容的には JAX-RS を使ったことがあることを想定しています。

まず、あらかじめ Twitter Developers でアプリケーションを登録して Consumer Key と Secret Key を手に入れておきます。また、OAuth サービスプロバイダ (今回は Twitter) での認証後にリダイレクトしてもらう URI を登録しておきましょう。
https://dev.twitter.com/

OAuth を使うのに必要な jar は以下の 3 つ。Jersey の Web サイトや、あるいは Maven を使ってリモートリポジトリから手に入れておきます。
http://jersey.java.net/

oauth-client-{version}.jar
oauth-server-{version}.jar
oauth-signature-{version}.jar

JavaDoc は以下にあるようです。
http://jersey.java.net/nonav/apidocs/1.10/contribs/jersey-oauth/oauth-signature/index.html?com/sun/jersey/oauth/signature/package-summary.html

方法は幾つか用意されているようですが、今回は OAuthClientFilter を使う方法を試してみました。OAuthClientFilter は Jersey Client と併せて使います。

OAuth の認証では、まず OAuth サービス・プロバイダの URI にユーザをリダイレクトします。リダイレクトする先の URI は、先ほど手に入れたアプリケーションの Consumer Key と Secret Key を使って手に入れます。コード例では XXX にしてあります。
せっかく Jersey の OAuth モジュールを使うので JAX-RS で WebAPI の形式で作ってみました。

	@GET
	public Response auth() throws OAuthSignatureException, URISyntaxException {
		OAuthParameters params = new OAuthParameters()
				.signatureMethod("HMAC-SHA1")
				.consumerKey("XXX").version();
		OAuthSecrets secrets = new OAuthSecrets()
				.consumerSecret("XXX");
		OAuthClientFilter filter = new OAuthClientFilter(client.getProviders(),
				params, secrets);
		WebResource resource = Client.create()
				.resource("https://api.twitter.com/oauth/request_token");
		resource.addFilter(filter);
		String redirectQuery = resource.get(String.class);
		return Response
				.status(302)
				.location(
						new URI("https://api.twitter.com/oauth/authorize?"
								+ redirectQuery)).entity("redirect").build();
	}

先ほど作った WebAPI の URI に HTTP GET すると Twitter の認証ページにリダイレクトされます。ユーザがリダイレクト先のページで認証すると、あらかじめ Twitter Developer に登録しておいた URI にリダイレクトされて戻ってきます。もちろんリダイレクトされる URI はあらかじめ作っておく必要があります。

リダイレクトされる URI ではクエリパラメータの形で Request Token が得られます。JAX-RS を使うのでクエリパラメータの値を @QueryParam アノテーションを使って得ます。得られた Request Key は Access Key に交換します。

	@GET
	@Path("/redirect")
	@Produces("text/plain; charset=UTF-8")
	public Response redirect(@QueryParam("oauth_token") String oauthToken, @QueryParam("oauth_verifier") String oauthVerifier) {
		OAuthParameters params = new OAuthParameters().signatureMethod("HMAC-SHA1").
		consumerKey("XXX").version();
		params.setToken(oauthToken);
		params.setVerifier(oauthVerifier);
		OAuthSecrets secrets = new OAuthSecrets().consumerSecret("XXX");
		Client client = Client.create();
		OAuthClientFilter filter = new OAuthClientFilter(client.getProviders(), params, secrets);
		WebResource resource = client.resource("https://api.twitter.com/oauth/access_token");
		resource.addFilter(filter);
		String response = resource.get(String.class);

この String response が Access Key です。クエリパラメータの形式で得られるため、パースしてセッションにでも保存しておきましょう。必要な値は "oauth_token" と "oauth_token_secret" です。

この Access Key の "oauth_token" と "oauth_token_secret" を使って Twitter API にアクセスできます。ここでは Map queryMap に Access Key が保存されているとします。例えば Home timeline を取得するコードは以下のようになります。

		Client client = Client.create();
		OAuthParameters params = new OAuthParameters().signatureMethod("HMAC-SHA1").
		consumerKey("XXX").version();
		params.setToken(queryMap.get("oauth_token"));
		OAuthSecrets secrets = new OAuthSecrets().consumerSecret("XXX").tokenSecret(queryMap.get("oauth_token_secret"));
		OAuthClientFilter filter = new OAuthClientFilter(client.getProviders(), params, secrets);
		WebResource resource = client.resource("https://api.twitter.com/1/statuses/home_timeline.json");
		resource.addFilter(filter);
		String response = resource.get(String.class);

この例では String 形式でマーシャリングしていますが、実際には各 API で得られる表現ごとに Java Bean を定義して、それを使うことになります。

ちなみに今回は Google App Engine を使ってサンプルアプリケーションを作ってみました。ホームタイムラインをただ JSON 形式で表示するだけなので見てもなんのこっちゃという感じですが…。はじめて Google App Engine を使ってみましたが HelloWorl まで 30 分くらいでできました。便利すぎてやばいです。
http://jersey-oauth-test.appspot.com/

以下のサイトを参考にしました。
OAuth - Jersey: RESTful Web services made easy - wikis.sun.com
ゼロから学ぶOAuth:第1回 OAuthとは?―OAuthの概念とOAuthでできること