Maven の archetype:create ゴールは非推奨 (Deprecated)

Maven でプロジェクトを作る方法に archetype:create ゴールを使うやり方がありますが、これは 2012/01 現在、非推奨 (Deprecated) とされています。今後は archetype:generate ゴールを使いましょう。
http://maven.apache.org/archetype/maven-archetype-plugin/create-mojo.html
http://maven.apache.org/archetype/maven-archetype-plugin/generate-mojo.html

Maven は割と頻繁に使い方の変更が入るフレームワークです。この内容も、いつかは非推奨になったり、全く使えなくなったりするかもしれません。やはり最後は公式ドキュメントを参照しないといけないですね。

RabbitMQ を OSX + Homebrew + Java で使ってみる

RabbitMQ というメッセージ指向ミドルウェアが便利らしい、という話を聞いて使ってみました。メッセージ指向ミドルウェアそのものの説明は他におまかせして、公式の Tutorial を試して便利に思った機能について書いてみます。

準備

RabbitMQ は使う前にマシンへのインストールが必要です。今回は OSX で試しました。インストールには Homebrew を使いました。

$ brew install rabbitmq

RabbitMQ のサーバは rabbitmq-server コマンドで実行します。

$ rabbitmq-server

Hello World

まずは単純にキューにメッセージを入れて取り出すだけのプログラムです。

キューにメッセージを入れるプログラムです。RabbitMQ (AMQP?) の用語でプロデューサというようです。

package test.rabbitmq.helloworld;

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;

public class Send {

	public static void main(String[] args) throws Exception {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();

		channel.queueDeclare("hello", false, false, false, null);
		String message = "Hello World!";
		channel.basicPublish("", "hello", null, message.getBytes());
		System.out.println(" [x] Sent '" + message + "'");

		channel.close();
		connection.close();
	}
}

やっていることは localhost で動作する RabbitMQ の "hello" という名前のキューに対して "Hello, World!!" という文字列のメッセージを詰めるだけです。

次にキューからメッセージを取り出して表示するプログラムです。こちらはコンシューマというようです。

package test.rabbitmq.helloworld;

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer;

public class Recv {

	public static void main(String[] argv) throws Exception {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();

		channel.queueDeclare("hello", false, false, false, null);
		System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

		QueueingConsumer consumer = new QueueingConsumer(channel);
		channel.basicConsume("hello", true, consumer);

		while (true) {
			QueueingConsumer.Delivery delivery = consumer.nextDelivery();
			String message = new String(delivery.getBody());
			System.out.println(" [x] Received '" + message + "'");
		}
	}
}

こちらも、やっていることは "hello" という名前のキューからメッセージを取り出して String に変換してコンソールに出力しているだけです。

プロジェクトは Maven で作ったのでプログラムの実行には exec-maven-plugin プラグインを使いました。pom.xml を貼っておきます。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>test.rabbitmq.helloworld</groupId>
	<artifactId>test-rabbitmq-helloworld</artifactId>
	<version>1.0-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>test-rabbitmq-helloworld</name>
	<url>http://maven.apache.org</url>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>exec-maven-plugin</artifactId>
				<version>1.2.1</version>
			</plugin>
		</plugins>
	</build>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.10</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.rabbitmq</groupId>
			<artifactId>amqp-client</artifactId>
			<version>2.7.1</version>
		</dependency>
	</dependencies>
</project>

あとは Recv (コンシューマ) を実行した状態で Send (プロデューサ) を実行するだけです。もちろん RabbitMQ を動作させておくのもお忘れなく。

まずは Recv を実行します。

$ mvn clean compile exec:java -Dexec.mainClass=test.rabbitmq.helloworld.Recv

別のコンソールで Send を実行します。

$ mvn clean compile exec:java -Dexec.mainClass=test.rabbitmq.helloworld.Send

Recv を実行した側のコンソールに "Hello, World!!" が表示されれば成功です。

 ---
 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'Hello World!'

Round-robin dispatching

処理を複数のサーバに振り分けて負荷を分散させるのも、自分で実現するとなると大変です。なんと RabbitMQ では先ほどの Recv を 2 つ実行しておくだけでキューに入ったメッセージをラウンドロビンでディスパッチしてくれます。すごい!

Fair dispatch

Round-robin dispatching ではコンシューマへ常に均等にメッセージをディスパッチします。メッセージ毎の処理時間が均等なら問題ないかもしれませんが、なかなかそうはいかないものです。RabbitMQ では、処理の空いたコンシューマに優先的にメッセージをディスパッチして負荷を分散することもできます。

負荷に応じてディスパッチされていることを確認するため、先ほど作ったプロデューサに手を加えました。

package test.rabbitmq.helloworld;

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;

public class Send {

	public static void main(String[] args) throws Exception {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();

		channel.queueDeclare("hello", false, false, false, null);
		String message = getMessage(args);
		channel.basicPublish("", "hello", null, message.getBytes());
		System.out.println(" [x] Sent '" + message + "'");

		channel.close();
		connection.close();
	}
	
	private static String getMessage(String[] args) {
		if (args.length < 1) {
			return "Hello, World!!";
		}
		return args[0];
	}
}

修正後は、実行時の引数でメッセージの内容を変更できるようにしてあります。

続いてコンシューマの Recv にも手を加えます。

package test.rabbitmq.helloworld;

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer;

public class Recv {

	public static void main(String[] argv) throws Exception {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();

		channel.queueDeclare("hello", false, false, false, null);
		System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

		channel.basicQos(1);

		QueueingConsumer consumer = new QueueingConsumer(channel);
		channel.basicConsume("hello", false, consumer);

		while (true) {
			QueueingConsumer.Delivery delivery = consumer.nextDelivery();
			String message = new String(delivery.getBody());
			System.out.println(" [x] Received '" + message + "'");
			doWork(message);
			System.out.println(" [x] Done '" + message + "'");
			channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
		}
	}

	private static void doWork(String task) throws InterruptedException {
		for (char ch : task.toCharArray()) {
			if (ch == '.') {
				Thread.sleep(1000);
			}
		}
	}
}

時間のかかる処理を表現するために、キューから取り出したメッセージに含まれる"." (ドット) 1 つ毎に 1 秒間のスリープをかけるようにしてあります。

先ほどと同様に 2 つ Recv を実行した上で Send を実行します。今回は、まず時間のかかる処理を入れてから時間のかからない処理を複数入れます。

$ mvn clean compile exec:java -Dexec.mainClass=test.rabbitmq.helloworld.Send -Dexec.args="...................."
$ mvn clean compile exec:java -Dexec.mainClass=test.rabbitmq.helloworld.Send -Dexec.args="1"
$ mvn clean compile exec:java -Dexec.mainClass=test.rabbitmq.helloworld.Send -Dexec.args="2"
$ mvn clean compile exec:java -Dexec.mainClass=test.rabbitmq.helloworld.Send -Dexec.args="3"
$ mvn clean compile exec:java -Dexec.mainClass=test.rabbitmq.helloworld.Send -Dexec.args="4"
$ mvn clean compile exec:java -Dexec.mainClass=test.rabbitmq.helloworld.Send -Dexec.args="5"
$ mvn clean compile exec:java -Dexec.mainClass=test.rabbitmq.helloworld.Send -Dexec.args="6"

Recv 側の出力は以下のようになりました。

 [*] Waiting for messages. To exit press CTRL+C
 [x] Received '....................'
 [x] Done '....................'
 [x] Received '6'
 [x] Done '6'
 [*] Waiting for messages. To exit press CTRL+C
 [x] Received '1'
 [x] Done '1'
 [x] Received '2'
 [x] Done '2'
 [x] Received '3'
 [x] Done '3'
 [x] Received '4'
 [x] Done '4'
 [x] Received '5'
 [x] Done '5'

最初にキューに入った時間のかかる処理を片方の Recv が処理している間、他方の Recv が時間のかからない処理を複数こなしていることが分かります。上手く負荷が分散されているみたいです。

プログラムのポイントは Recv の以下のコードです。

		channel.basicQos(1);
...
		channel.basicConsume("hello", false, consumer);
…
		channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

Channel#basicQos() ではコンシューマが処理するメッセージの最大数を設定します。Channel#basicConsume() は第二引数を true から false に変更しています。これは自動でメッセージの確認応答 (ACK) を返すかを示す真偽値です。これを false にするときは、自分で確認応答を伝えないといけません。確認応答は Channel#basicAck() で伝えます。

確認応答を返すまではメッセージの処理中とみなされるようです。コンシューマが処理するメッセージの最大数を制限した上で、自分で処理の完了時に確認応答を返すようにすることでメッセージの処理中に新たなメッセージがディスパッチされないようになるようです。

Message acknowledgment

確認応答を自分で返すようにすると、更に良いことがあります。コンシューマが処理中に何らかの原因で落ちてしまった場合にも、確認応答を返していない限りはメッセージが失われることがありません。

先ほどのプログラムで確認してみます。先ほどと同様に Recv を 2 つ実行した上で Send で時間のかかる処理をキューに入れます。処理中の Recv を Ctrl+C で落としてみます。

Send を実行します。

$ mvn clean compile exec:java -Dexec.mainClass=test.rabbitmq.helloworld.Send -Dexec.args=".........."

メッセージがディスパッチされた Recv を処理中に Ctrl+C で落とします。

 [*] Waiting for messages. To exit press CTRL+C
 [x] Received '..........'
^C%                    

もう一つの Recv を見るとメッセージがディスパッチされ直していることが分かります。

 [*] Waiting for messages. To exit press CTRL+C
 [x] Received '..........'
 [x] Done '..........'

処理に失敗してもメッセージが消失しないのは安心できますね。

Message durability

先ほどの例ではコンシューマが落ちたときもメッセージが消失しないことを確認しました。とはいえ RabbitMQ のサーバ自体が落ちるケースもあります。サーバが落ちている間に処理できないのは仕方ないとしても、既にキューに入ったメッセージが消失してしまうのは困ります

RabbitMQ のサーバが落ちたとき、キューに入ったメッセージが消失しないようにするには永続化の機構を使います。

メッセージを永続化するようにした Send です。

package test.rabbitmq.helloworld;

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;

public class Send {

	public static void main(String[] args) throws Exception {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();

		channel.queueDeclare("durable", true, false, false, null);
		String message = getMessage(args);
		channel.basicPublish("", "durable", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
		System.out.println(" [x] Sent '" + message + "'");

		channel.close();
		connection.close();
	}
	
	private static String getMessage(String[] args) {
		if (args.length < 1) {
			return "Hello, World!!";
		}
		return args[0];
	}
}

プログラムのポイントは以下です。

	channel.queueDeclare("durable", true, false, false, null);
	...
	channel.basicPublish("", "durable", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

Channel#queueDeclare() の第二引数を false から true に変更しています。この真偽値はキューがメッセージを永続化するか示しています。そして Channel#basicPublish() の第三引数で永続化の方式を指定しています。キューの名前が変わっているのは、既に定義済みのキューを途中から永続化するように変更できない制約があるためです。

Recv はキューの名前を変更した以外に変わっていません。

package test.rabbitmq.helloworld;

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer;

public class Recv {

	public static void main(String[] argv) throws Exception {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();

		channel.queueDeclare("durable", true, false, false, null);
		System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

		channel.basicQos(1);

		QueueingConsumer consumer = new QueueingConsumer(channel);
		channel.basicConsume("durable", false, consumer);

		while (true) {
			QueueingConsumer.Delivery delivery = consumer.nextDelivery();
			String message = new String(delivery.getBody());
			System.out.println(" [x] Received '" + message + "'");
			doWork(message);
			System.out.println(" [x] Done '" + message + "'");
			channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
		}
	}

	private static void doWork(String task) throws InterruptedException {
		for (char ch : task.toCharArray()) {
			if (ch == '.') {
				Thread.sleep(1000);
			}
		}
	}
}

今度は Send でメッセージをキューに入れた上で RabbitMQ のサーバを再起動します。メッセージが永続化されていれば、サーバを落としてもキューにメッセージが残り続けるため、サーバが再起動した後に Recv を実行すればメッセージがディスパッチされるはずです。

Send を実行します。

$ mvn clean compile exec:java -Dexec.mainClass=test.rabbitmq.helloworld.Send

RabbitMQ を再起動します。

broker running
^C
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
a
...
$ rabbitmq-server

Recv を実行します。

$ mvn clean compile exec:java -Dexec.mainClass=test.rabbitmq.helloworld.Recv

メッセージがディスパッチされました!

 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'Hello, World!!'
 [x] Done 'Hello, World!!'

まとめ

システムのコンポーネント間でいかにデータをやり取りするかはいつも悩みの種です。そうした時、可用性や完全性を保ちつつスケーラビリティの高いシステムを構築する上で RabbitMQ (AMQP) は、力強い手助けになるのではないでしょうか。

Jersey Test Framework を使って WebAPI の単体テストを書いてみる

フロントエンドの HTML やアプリケーションと WebAPI を別々に開発すると、ビューとロジックをほぼ完全に分離できます。それぞれの責務が絞られるので、もともと不具合が生じにくいというのもありますが、何より WebAPI は単体テストが書きやすいという点で優れていると思います。

とはいえ、単体テストを実行する環境を整えるのは意外と面倒かもしれません。以前のエントリでも書きましたが、アプリケーションをデプロイしたサーバを立ち上げてテストを走らせてサーバを落とす、という一連の流れはなるべく簡単に実行したいものです。

もし WebAPI を Jersey (JAX-RS) で開発するのであれば、そうしたニーズに答えるためテスト用のフレームワークが用意されています。

テストする対象を用意する

まずはテストする対象を作らないと始まりません。今回準備したリソースは以下です。

package test.jersey.test.framework.resources;

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


@Path("/test")
public class TestResource {

	@GET
	public Response get() {
		return Response.status(200).entity("Hello, World!!").build();
	}
	
}

リソースを作ったら web.xmlサーブレットとして登録します。今回はパッケージから自動的にリソースやプロバイダを見つけてもらうように設定しました。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
	<display-name>test-jersey-test-framework</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>com.sun.jersey.config.property.packages</param-name>
			<param-value>test.jersey.test.framework.resources</param-value>
		</init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>Jersey Web Application</servlet-name>
		<url-pattern>/*</url-pattern>
	</servlet-mapping>
</web-app>

プロジェクトは Maven で作りました。使った pom.xml は以下の通り。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>test.jersey.test.framework</groupId>
	<artifactId>test-jersey-test-framework</artifactId>
	<packaging>war</packaging>
	<version>1.0-SNAPSHOT</version>

	<name>test-jersey-test-framework</name>
	<url>http://maven.apache.org</url>

	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.10</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
			<version>2.5</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>jsp-api</artifactId>
			<version>2.1</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>com.sun.jersey.jersey-test-framework</groupId>
			<artifactId>jersey-test-framework-grizzly2</artifactId>
			<version>1.10</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.sun.jersey</groupId>
			<artifactId>jersey-bundle</artifactId>
			<version>1.10</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.mortbay.jetty</groupId>
				<artifactId>jetty-maven-plugin</artifactId>
				<version>8.0.4.v20111024</version>
			</plugin>
		</plugins>
	</build>
</project>

動作を確認する

一足飛びは失敗の元です。まずは普通に動作を確認します。動作の確認には Maven の Jetty プラグインを使いました。プロジェクトのディレクトリに移動した上で Jetty プラグインのゴールを実行します。

mvn clean compile jetty:run

コンソールに Jetty が開始したことを示すログが表示されたら、ブラウザなどから先ほど作った リソースの URI に GET をかけます。動作確認には REST Client for Firefox プラグインを使いました。

単体テストを書く

動作確認が済んだのでいよいよ単体テストです。単体テストに使うクラスは JerseyTest クラスを継承して作ります。コンストラクタにリソースのパッケージのパスを渡したら、あとは Jersey Client を使ってテストコードを書くだけです。

package test.jersey.test.framework.resources;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.test.framework.JerseyTest;
import junit.framework.Assert;
import org.junit.Test;

public class TestResourceTest extends JerseyTest {
	
	public TestResourceTest() {
		super("test.jersey.test.framework.resources");
	}
	
	@Test
	public void test() {
		Client client = Client.create();
		WebResource resource = client.resource("http://localhost:9998/test");
		ClientResponse response = resource.get(ClientResponse.class);
		String responseEntity = response.getEntity(String.class);
		Assert.assertEquals(responseEntity, "Hello, World!!");
	}
}

接続先のポート番号が 9998 になっていることに注意してください。

Jersey Test Framework は JUnit 4.x と互換性を保つように作られているようなので Maven であれば適切な場所 (${project.home}/src/test/) にテストコードを配置した上で、以下のコマンドで実行できます。

mvn clean compile test

単体テストが成功すれば以下のような出力が得られるはずです。

…(省略)...
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.416 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
…(省略)...

より細かく設定する

先ほどの例では単体テストに渡した情報はパッケージのパスだけでした。とはいえ実際に開発していると色々と設定したい箇所が出てくると思います。例えば web.xml の init-param で渡しているパラメータですね。

こういう時は、とりあえず JavaDoc を読んでみます。
http://jersey.java.net/nonav/apidocs/1.10/jersey-test-framework/jersey-test-framework-core/com/sun/jersey/test/framework/JerseyTest.html

どうやら抽象クラス AppDescriptor を引数に取るコンストラクタを使えば良いみたいです。AppDescriptor の実装は複数あって、テストに使うサーブレットコンテナの種類によって使い分けるみたいですね。今回はテストに使うサーブレットコンテナに Grizzly2 を使っているので WebAppDescriptor.Builder クラスを使います。

先ほどとやっていること自体は同じですが web.xml で init-param を使う場合と同様の記述に変更してみます。

package test.jersey.test.framework.resources;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.test.framework.JerseyTest;
import com.sun.jersey.test.framework.WebAppDescriptor;
import java.util.HashMap;
import java.util.Map;
import junit.framework.Assert;
import org.junit.Test;

public class TestResourceTest extends JerseyTest {
	
	private static final Map<String, String> initParams = new HashMap<String, String>();
	
	static {
		initParams.put("com.sun.jersey.config.property.packages",
					"test.jersey.test.framework.resources");
	}
	
	public TestResourceTest() {
		super(new WebAppDescriptor.Builder(TestResourceTest.initParams).build());
	}
	
	@Test
	public void test() {
		Client client = Client.create();
		WebResource resource = client.resource("http://localhost:9998/test");
		ClientResponse response = resource.get(ClientResponse.class);
		String responseEntity = response.getEntity(String.class);
		Assert.assertEquals(responseEntity, "Hello, World!!");
	}
}

テストの手順等は先程と変わりません。

mvn clean compile test

実行すると、単体テストが成功しました。

まとめ

今回は Jersey で作った WebAPI の単体テストを Jersey Test Framework を使って書いてみました。Jersey Test Framework を使うと、単体テストが少ないコマンドで実行できて便利そうです。

Jetty8のWebSocketサーバ/クライアントを使ってみる

ついに RFC6455 (WebSocket) が出ましたね!ぼくも興味津々です。
ということで今回は Jetty の WebSocket サーバ/クライアント実装を使ってみます。

サーバ

今回作ったサーバのソースは以下の通りです。

import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocket.Connection;
import org.eclipse.jetty.websocket.WebSocketServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JettyWebSocketServlet extends WebSocketServlet {

	Logger log = LoggerFactory.getLogger(JettyWebSocketServlet.class);

	// string には WebSocket のサブプロトコルが入る
	@Override
	public WebSocket doWebSocketConnect(HttpServletRequest hsr, String string) {
		// WebSocket インタフェースを実装したクラスのインスタンスを返す
		return new WebSocket.OnTextMessage() {
			
			protected Connection connection;

			// テキストのメッセージが届いたとき
			@Override
			public void onMessage(String data) {
				log.info("onMessage: " + data);
				// エコーする
				this.send(data);
			}

			// コネクションが開かれたとき
			@Override
			public void onOpen(Connection connection) {
				log.info("onOpen");
				this.setConnection(connection);
				this.send("Hello");
			}

			// コネクションが閉じられたとき
			@Override
			public void onClose(int closeCode, String message) {
				this.send("Bye");
				log.info("onClose");
			}

			public Connection getConnection() {
				return connection;
			}

			public void setConnection(Connection connection) {
				this.connection = connection;
			}

			protected void send(String message) {
				try {
					this.getConnection().sendMessage(message);
				} catch (IOException ex) {
					ex.printStackTrace();
				}
			}
		};
	}
}

Jetty の WebSocket サーバ実装では、まず WebSocketServlet クラスが軸になります。この WebSocketServlet は HttpServlet を継承しているので、普通のサーブレットとして web.xml に登録するだけで Jetty にデプロイできます。
ただし WebSocketServlet クラスは WebSocketFactory.Acceptor インタフェースを実装していますが、抽象クラスになっていて実際には doWebSocketConnect メソッドを実装していません。この doWebSocketConnect メソッドは WebSocket のリクエストがあったときに呼び出されます。
この doWebSocketConnect メソッドの返り値は WebSocket インタフェースになっています。つまり WebSocket インタフェースを実装した自分好みの具象クラスを作ってリクエストごとに返してやるわけです。

WebSocket インタフェースには onOpen, onClose メソッドが定義されています。はて、クライアント (対向) からデータを受け取るメソッドはいずこ。ソースコードで既に正解が出ていますが WebSocket インタフェースのフィールドにネストする形で OnTextMessage インタフェースが定義されていて、これを使うみたいです。同列に OnBinaryMessage インタフェースや OnFrame インタフェースとかもあるので、必要なメソッドを選んで実装していくんでしょうね。
あとは WebSocket インタフェースを好きに実装したクラスを作るだけです。今回はめんどくさいので無名クラスで済ませました。

念のため web.xml も貼っておきます。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

	<display-name>test-jetty-websocket</display-name>

	<servlet>
		<servlet-name>JettyWebSocketServlet</servlet-name>
		<servlet-class>test.jetty.websocket.JettyWebSocketServlet</servlet-class>
	</servlet>
	
	<servlet-mapping>
		<servlet-name>JettyWebSocketServlet</servlet-name>
		<url-pattern>/websocket/*</url-pattern>
	</servlet-mapping>
	
</web-app>

普通のサーブレットと全く変わりませんね。

クライアント

世の中を見渡すとクライアントに HTML/JavaScript を使ったサンプルが溢れています。が、ぼくは主にテストの観点からサーバと同じ言語/実装でクライアントも用意されていないと使う気になれません。その点 Jetty はクライアントも用意されているので完璧です。

import java.net.URI;
import java.util.concurrent.Future;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocket.Connection;
import org.eclipse.jetty.websocket.WebSocketClient;
import org.eclipse.jetty.websocket.WebSocketClientFactory;
import org.junit.Test;

public class JettyWebSocketServletTest {

	@Test
	public void test() throws Exception {
		WebSocketClientFactory webSocketClientFactory = new WebSocketClientFactory();
		webSocketClientFactory.start();
		WebSocketClient client = webSocketClientFactory.newWebSocketClient();
		Future<Connection> futureConnection = 
		    client.open(new URI("ws://localhost:8080/websocket/"), new WebSocket.OnTextMessage() {

			@Override
			public void onMessage(String data) {
			}

			@Override
			public void onOpen(Connection connection) {
			}

			@Override
			public void onClose(int closeCode, String message) {
			}
		});
		Connection connection = futureConnection.get();
		connection.sendMessage("Hello, WebSocket!!");
		connection.disconnect();
	}
}

クライアントでは WebSocketClientFactory から WebSocketClient を手に入れて使います。WebSocketClientFactory はあらかじめ start メソッドを叩いておかないと使えないみたいなので注意です。
WebSocketClient を手に入れたら open メソッドで Connection インタフェースを手に入れます。open メソッドには WebSocket サーバのエンドポイントと WebSocket インタフェースを実装したクラスを渡します。
Connection インタフェースのインスタンスは Future インタフェースの中に入った状態で得られるので get メソッドで取り出した上で使います。きっと WebSocket のコネクションが開いて使えるようになるまでブロックするんでしょうね。
今回はシンプルにコネクションを開いて一言喋ってサヨナラする、ほぼピンポンダッシュの実装にしました。

pom.xml

今回のプロジェクトは Maven で作りました。使った pom.xml は以下です。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>test.jetty.websocket</groupId>
	<artifactId>test-jetty-websocket</artifactId>
	<packaging>war</packaging>
	<version>1.0-SNAPSHOT</version>

	<name>test-jetty-websocket</name>
	<url>http://maven.apache.org</url>

	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.10</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
			<version>2.5</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>jsp-api</artifactId>
			<version>2.1</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.eclipse.jetty</groupId>
			<artifactId>jetty-websocket</artifactId>
			<version>8.0.4.v20111024</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.6.4</version>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-core</artifactId>
			<version>1.0.0</version>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>1.0.0</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.mortbay.jetty</groupId>
				<artifactId>jetty-maven-plugin</artifactId>
				<version>8.0.4.v20111024</version>
			</plugin>
		</plugins>
	</build>
</project>

実行する

あとは2つコンソールを開いて Maven を実行するだけです。1つはサーバの起動、1つはテストの実行に使います。
こういうとき、サーバを裏で起動しておいてテストを走らせて終わったら落とすみたいなのを一発でやるのってどうやると良いんですかね?ご存知の方がいらっしゃったら教えてください!

$ mvn clean compile jetty:run
$ mvn test

上手くいけばサーバ側のロガー出力に以下のような出力が出るはずです。(ロガーとかよく分からんという場合は、サーバ側の log#info() を System.out.println() に書き換えても良いかもです)

23:28:11 [qtp1734564525-22 - /websocket/] INFO t.j.websocket.JettyWebSocketServlet - onOpen
23:28:11 [qtp1734564525-23] INFO  t.j.websocket.JettyWebSocketServlet - onMessage: Hello, WebSocket!!
23:28:11 [qtp1734564525-23] INFO  t.j.websocket.JettyWebSocketServlet - onClose

ピンポンダッシュされてますね!上手く動いてるみたいです!

ドキュメント

今回使った Jetty の WebSocket パッケージの JavaDoc は以下にあるみたいです。
http://download.eclipse.org/jetty/stable-8/apidocs/org/eclipse/jetty/websocket/package-summary.html

まとめ

今回は Jetty の API を使ってサーバとクライアントの両方を実装してみました。Jetty の API はシンプルでキレイですね。個人的には気に入りました。あと、サーバだけでなくクライアントの実装も用意されている点もステキで、開発フェーズのテストがすごくやりやすいだろうなと思います。

Hibernate+Mavenでドメインモデルからスキーマを自動生成する

O/Rマッパーを使ってデータベースを扱うアプリケーションを作るにも色々なアプローチがあると思いますが、今回はドメインモデル (POJO) をまず作ってからそれを元にデータベースのスキーマを自動的に生成する方法についてです。

hibernate3-maven-plugin

Hibernate にはドメインモデルからスキーマを自動的に生成するツールとして hbm2ddl が用意されています。この hbm2ddl を Maven から実行するプラグインとして hibernate3-maven-plugin があります。

早速ですが pom.xml の設定です。

            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>hibernate3-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <verbose>true</verbose>
                    <components>
                        <component>
                            <name>hbm2ddl</name>
                            <implementation>jpaconfiguration</implementation>
                        </component>
                    </components>
                    <componentProperties>
                        <format>true</format>
                        <export>false</export>
                        <outputfilename>schema.ddl</outputfilename>
                    </componentProperties>
                </configuration>
            </plugin>

データベースのアクセスに使う API は implementation タグで指定するようです。今回は JPA (Java Persistence API) を使いました。

ドメインモデルを作る

ありがちですが「人」を表すドメインモデルを作ってみました。

import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Version;

@Entity
public class Person implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    
    @Version
    private Integer version;
    
    private int age;
    
    private String firstname;
    
    private String lastname;

    public Person() {
    }

    public Person(int age, String firstname, String lastname) {
        this.age = age;
        this.firstname = firstname;
        this.lastname = lastname;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getVersion() {
        return version;
    }

    public void setVersion(Integer version) {
        this.version = version;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getFirstname() {
        return firstname;
    }

    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public void setLastname(String lastname) {
        this.lastname = lastname;
    }
}

JPA を設定する

JPA の設定ファイル (persistence.xml) を以下のように作りました。データベースには MySQL を使う設定です。ドメインモデルは class タグで指定しました。exclude-unlisted-classes タグを使わない場合は明示的にドメインモデルを指定しなくてもパスが通っている場所を自動的に探してくれるみたいです。

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
    <persistence-unit name="sample">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <class>test.hibernate.sql.export.domain.Person</class>
        <exclude-unlisted-classes/>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"/>
            <property name="hibernate.connection.username" value="****"/>
            <property name="hibernate.connection.password" value="****"/>
            <property name="hibernate.connection.url" value="jdbc:mysql://localhost/test"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.connection.pool_size" value="1"/>
            <property name="hibernate.current_session_context_class" value="thread"/>
            <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
            <property name="hibernate.use_outer_join" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

ユーザ名やパスワード、データベース名などは自分の環境に併せて変更してください。あとは HibernateJDBCドライバをパスに依存ライブラリに追加するのも忘れずに。

スキーマを自動生成する

あとは Maven を実行するだけです。

$ mvn clean compile hibernate3:hbm2ddl
…(省略)…
    create table Person (
        id integer not null auto_increment,
        age integer not null,
        firstname varchar(255),
        lastname varchar(255),
        version integer,
        primary key (id)
    );
00:12:03,373  INFO org.hibernate.tool.hbm2ddl.SchemaExport - schema export complete

これで ${project.basedir}/target/hibernate3 ディレクトリに schema.ddl ファイルができているはずです。

pom.xml

念のため、今回使った pom.xml を載せておきます。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>test.hibernate.sql.export</groupId>
    <artifactId>test-hibernate-sql-export</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>test-hibernate-sql-export</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>hibernate3-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <verbose>true</verbose>
                    <components>
                        <component>
                            <name>hbm2ddl</name>
                            <implementation>jpaconfiguration</implementation>
                        </component>
                    </components>
                    <componentProperties>
                        <format>true</format>
                        <export>false</export>
                        <outputfilename>schema.ddl</outputfilename>
                    </componentProperties>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>3.6.8.Final</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.6.4</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.18</version>
        </dependency>
    </dependencies>
</project>

まとめ

HibernateMaven を使って、ドメインモデルからスキーマを自動的に生成することができました。Hibernate には hbm2ddl 以外にも、色々と自動化するためのツールが用意されてるみたいです。

WebAPIのステートレスなCSRF対策

Jerseyのバージョン1.9.1で追加されたCSRFをステートレスに防ぐフィルタが興味深かったので、そのメモです。

CSRF対策の手法

通常、CSRF攻撃を防ぐにはトークンを使う方法があります。サーバがクライアントにトークンを発行して、クライアントは発行されたトークンをクエリパラメータなどの形でリクエストに付与します。サーバはトークンが付与されていないリクエストを実行しません。攻撃者は発行されたトークンを知らないため、リクエストを実行できないという寸法です。

CSRF対策とステート

ただし、この方法ではトークンの取得〜リクエストの発行にステートができるため、WebAPIがステートフルになってしまうという問題があります。RESTベースのWebAPIはやはりステートレスに作りたいところです。
そこで、JerseyのCSRF対策フィルタはステートレスになるよう作られています。

JerseyのCSRF対策フィルタ

フィルタのJavaDocソースコードはこちら。
http://jersey.java.net/nonav/apidocs/1.10/jersey/com/sun/jersey/api/container/filter/CsrfProtectionFilter.html
http://java.net/projects/jersey/sources/svn/content/trunk/jersey/jersey-server/src/main/java/com/sun/jersey/api/container/filter/CsrfProtectionFilter.java?rev=5545

フィルタのソースを見ると内容はとてもシンプルです。HTTPリクエストにX-Requested-Byヘッダがあればリクエストをそのまま通す、なければHTTPステータスに400(BadRequest)を返却します。また、HTTPメソッドGET,HEAD,OPTIONSは処理の対象から外しています。

何故これでCSRF攻撃が防げるのか

フィルタのJavaDocで以下のPaperを参照しています。
http://www.nsa.gov/ia/_files/support/guidelines_implementation_rest.pdf
http://seclab.stanford.edu/websec/csrf/csrf.pdf

form, iframe, imageなどからのリクエストではHTTPリクエストに独自のヘッダを付与することができません。独自のヘッダをつけるにはXMLHttpRequestを使うしかないわけです。そしてXMLHttpRequestを使う場合にはSame Origin Policyが適用されるため攻撃者のドメインからHTTPリクエストがくることはない、ということのようです。

フィルタを試す

実際にフィルタを使ってみます。まずフィルタをweb.xmlサーブレットに登録します。

		<init-param>
			<param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name>
			<param-value>com.sun.jersey.api.container.filter.CsrfProtectionFilter</param-value>
		</init-param>

テストに使うリソースは以下の通り。

@Path("/test")
public class TestResource {
	
	@GET
	public Response get() {
		return Response.status(200).entity("Hello, World!").build();
	}
	
	@POST
	public Response post(String message) {
		return Response.status(201).entity(message).build();
	}
	
}

テストは実際にHTMLを書いてAjaxを使うのも良いですがJerseyにはClientの実装があるためそれを使います。

public class TestResourceTest {
	
	@Test
	public void testGetWithoutHeader() {
		Client client = Client.create();
		WebResource resource = client.resource("http://localhost:8080/test-jersey-csrf/test/");
		ClientResponse response = resource.get(ClientResponse.class);
		// GET はフィルタ対象外なので 200 (Success)
		Assert.assertEquals(response.getStatus(), 200);
	}
	
	@Test
	public void testPostWithoutHeader() {
		Client client = Client.create();
		WebResource resource = client.resource("http://localhost:8080/test-jersey-csrf/test/");
		ClientResponse response = resource.entity("hogehoge").post(ClientResponse.class);
		// X-Requested-By がなければ 400 (BadRequest)
		Assert.assertEquals(response.getStatus(), 400);
	}
	
	@Test
	public void testPostWithHeader() {
		Client client = Client.create();
		WebResource resource = client.resource("http://localhost:8080/test-jersey-csrf/test/");
		ClientResponse response = resource.header("X-Requested-By", "true").entity("hogehoge").post(ClientResponse.class);
		// X-Requested-By がついていれば 201 (Created)
		Assert.assertEquals(response.getStatus(), 201);
	}
	
}

アプリケーションをサーブレットにデプロイした状態でテストを実行すると全てPASSしました。

まとめ

CSRFを防ぐ方法として、通常のトークンを使うやり方ではWebAPIがステートフルになってしまうという問題点がありました。それに対し、HTTPリクエストに独自ヘッダをつけるやり方ではステートレスに防ぐことができます。

この方法の注意点としてはWebAPIをコールするのがXMLHttpRequest経由に限られる、ということです。もしXMLHttpRequest以外からもコールされるようなWebAPIを作る場合には、やはりトークンを使ってステートフルにする方法をとるしかないと思います。

また、GET, HEAD, OPTIONSがフィルタ処理の対象外になっていることも注意すべきです。そもそもWebAPIの設計として、それらのHTTPメソッドで副作用のある処理を実行するように作るべきではないです。

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

前回のフィルタの例はあまりにも実用性がなかったので、もう少し実用性のある例を書いてみました。ContainerRequestFilter と ContainerResponseFilter を組み合わせてリソースの処理時間を測るフィルタです。

package test.jersey.filter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.ws.rs.core.Context;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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;

public class ResourceBenchmarkFilter implements ContainerRequestFilter, ContainerResponseFilter {

	private Logger log = LoggerFactory.getLogger(ResourceBenchmarkFilter.class);
	
	@Context
	private HttpServletRequest httpServletRequest;
	
	@Override
	public ContainerRequest filter(ContainerRequest request) {
		// リクエストの時間を記録する
		long requestTime = System.currentTimeMillis();
		// セッションを取得する
		HttpSession session = this.httpServletRequest.getSession();
		// リクエストの時間をセッションに保存する
		session.setAttribute("requestTime", requestTime);
		return request;
	}
	
	@Override
	public ContainerResponse filter(ContainerRequest request, ContainerResponse response) {
		// レスポンスの時間を記録する
		long responseTime = System.currentTimeMillis();
		// セッションを取得する
		HttpSession session = this.httpServletRequest.getSession();
		// セッションからリクエストの時間を取り出す
		long requestTime = (Long)session.getAttribute("requestTime");
		// レスポンスとリクエストの時間差を出力する
		log.info("time: " + (responseTime - requestTime) + "ms");
		return response;
	}

}

やっていること自体は単純で、リクエストの時間とレスポンスの時間をそれぞれ記録して時間差をログに出力しているだけです。リクエストとレスポンスの時間はセッションを使って共有しています。

注目すべきはセッションを取得するのに使われているフィールドの HttpServletRequest です。このインスタンスは @Context を目印に Jersey がインジェクトしてくれます。

 フィルタの登録

おさらいですがフィルタは web.xml で以下のように登録します。

		<init-param>
			<param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name>
			<param-value>test.jersey.filter.ResourceBenchmarkFilter</param-value>
		</init-param>
		<init-param>
			<param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name>
			<param-value>test.jersey.filter.ResourceBenchmarkFilter</param-value>
		</init-param>

実行結果

リソースにアクセスすると以下のようなログが出ます。ロガーの実装には logback を使っています。

23:35:25 [http-bio-8080-exec-3] INFO  t.j.filter.ResourceBenchmarkFilter - time: 7ms

スレッドセーフ

ところで、フィールドにインジェクトされるということは、このフィルタのインスタンスが複数のスレッドから共有されていると競合が起きそうに思えます。そして (仕様としての記述は見当たりませんが) フィルタのインスタンスは複数のスレッドで共有されます。しかし実際には競合は起きません。理由は @Context でインジェクトされたインスタンスは ThreadLocal なためです。ソースは Jersey 開発者の ML でのリプライです。(残念ながらアーカイブの URL は失念してしまいました。)ただ、以前検証したことはあり確かにスレッド間で競合は起きていませんでした。

まとめ

前回のエントリよりはフィルタの用例がイメージしやすいかと思います。インジェクトできるクラスは他にも色々とあるので、組み合わせるとフィルタの用途がぐっと広がりそうです。

ちなみに Jersey には標準で幾つかフィルタが用意されています。詳細は以下の JavaDoc をご覧ください。
http://jersey.java.net/nonav/apidocs/latest/jersey/com/sun/jersey/api/container/filter/package-summary.html