リソースクラスにGuiceでオブジェクトをDIする(Jersey版)

JerseyGuiceの連携の例。

Jerseyを使うとJAX-RS準拠のRESTfulなHTTPサービスを作ることができる。

この時リソースクラス(クラス宣言に@Pathをつけたクラス)は、自動でHTTPリソースとして登録されるので、特別インスタンスを取得することはない。ただ、DIコンテナとしてGuiceを使っている場合、リソースクラスはGuiceの管理下におけない。そんな場合にも、ビジネスロジックやStringなどのインスタンスをリソースクラスにDIする時のやり方についてまとめておく。

準備

Jerseyの各種jarファイルと、Guiceのjarファイル(今回はguice-3.0-no_aop.jarとjavax.inject.jarのみ)をビルドパスに通しておく。

DIなしの場合

まずはメインクラス。

package sample;

import com.sun.jersey.api.container.httpserver.HttpServerFactory;
import com.sun.net.httpserver.HttpServer;

public class Sample {

	public static void main(String[] args) throws Exception {
		HttpServer server = HttpServerFactory.create("http://localhost:8888/");
		server.start();
	}
}

続いてリソースクラス。

package sample;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;

@Path("/hello")
public class HelloResource {

	@GET
	@Path("/")
	@Consumes(MediaType.TEXT_PLAIN)
	public String hello() {
		return "***hello***";
	}
}

たったこれだけで、mainを実行して、http://localhost:8888/helloにアクセスすると、"***hello***"が取得できるはず。

大体チュートリアルとかサンプル通りなので、ここまではOK。

GuiceでDI

Guiceを使う場合のメインクラス。

package sample2;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.sun.jersey.api.container.httpserver.HttpServerFactory;
import com.sun.net.httpserver.HttpServer;

public class Sample {

	public static Injector injector;

	public static void main(String[] args) throws Exception {
		injector = Guice.createInjector(new AbstractModule() {
			@Override
			protected void configure() {
				bind(String.class).toInstance("Hello, Guice!");
			}
		});

		HttpServer server = HttpServerFactory.create("http://localhost:8888/");
		server.start();
	}
}

最初にGuiceのInjectorを作っている。今回はStringに対して"Hello, Guice!"という文字列をフィールドインジェクションしたいので、そのようなモジュールを渡している。injectorがpublic staticになっているのは後述。

続いてリソースクラス。2つリソースクラスがあってどっちも同じ@Pathだとエラーになるので、@Path("/hello2")としている。

で、messageというフィールドを@Inject宣言。ここの@Injectは、javax.inject.Injectではなく、com.google.inject.Injectなので注意。

package sample2;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;

import com.google.inject.Inject;

@Path("/hello2")
public class HelloResource2 {

	@Inject
	private String message;

	@GET
	@Path("/")
	@Consumes(MediaType.TEXT_PLAIN)
	public String hello() {
		return "***" + message + "***";
	}
}

さて、これでメインを起動して、http://localhost:8888/hello2にアクセスすると、"***null***"が取得できるはず。リソースクラスにmessageがインジェクションされていないので、当然。

想定通りにDIしたい場合、以下のようなクラス(プロバイダ)を定義すればよい。上記のメインクラスとリソースクラスは変更不要。

package sample2;

import java.lang.reflect.Type;

import javax.ws.rs.ext.Provider;

import com.google.inject.Inject;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;

@Provider
public class MessageProvider implements InjectableProvider<Inject, Type> {

	public Injectable<String> getInjectable(ComponentContext ctx,
			Inject annotation, Type type) {
		if (type.equals(String.class)) {
			return new Injectable<String>() {
				public String getValue() {
					return Sample.injector.getInstance(String.class);
				}
			};
		}

		return null;
	}

	public ComponentScope getScope() {
		return ComponentScope.Singleton;
	}
}

@Providerがついているクラスは自動的に取り込まれるので、これでメインを起動して、http://localhost:8888/hello2にアクセスすると、"***Hello, Guice!***"が取得できる。めでたしめでたし。

これで、Guiceの管理下にないリソースクラスでも、他のビジネスロジックのように@Injectをつけてインジェクションすることができることが分かった。

どれくらい役に立つのか分からないけど、簡易RESTサーバなんだけど、ちょっとDIとかで疎結合にしたい、って時には役に立つかもしれないメモ。

補足1

先ほどメインクラスにinjectorをpublic static宣言してたのは、このMessageProviderのInjectableの中で、インジェクタを参照できるようにしたかったからだった。

補足2

Guiceでは、@Singletonをつけたオブジェクトのシングルトンは、VM毎ではなくインジェクタ毎に一つである。そのため、本当にVMでただひとつのインスタンスとして管理したい場合を想定して、メインクラスに唯一のインジェクタを持つようにしている。とは言え、あんまり格好はよくないなぁ(Guiceのベタープラクティスとして、インジェクタってどう管理するのがいいんだろうか…?)

と思って調べると、以下の記事が見つかった。

http://blog.iparissa.com/googles-app-engine-java/jersey-guice-on-google-app-engine-java/

この記事の場合、リソースクラスでinjectorを持っておくようにしているみたい。で、必要になったらinjectorからオブジェクトを取得する、というアプローチ。なので、リソースクラスに対して@Injectでインジェクション設定する、ってのとはちょっと違うかな…。

補足3

リソースクラスに対してインジェクションするためのアノテーションは、@com.google.inject.InjectにしたからGuiceライクになっただけで、実際はなんでもよい。InjectableProviderのジェネリック型で決められるので、制限はなし。むしろ、InjectableProviderのサンプルとかを見ると、独自にアノテーションを定義してそれを使っているくらいだし。