Jerseyを使うとJAX-RS準拠のRESTfulなHTTPサービスを作ることができる。
この時リソースクラス(クラス宣言に@Pathをつけたクラス)は、自動でHTTPリソースとして登録されるので、特別インスタンスを取得することはない。ただ、DIコンテナとしてGuiceを使っている場合、リソースクラスはGuiceの管理下におけない。そんな場合にも、ビジネスロジックやStringなどのインスタンスをリソースクラスにDIする時のやり方についてまとめておく。
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でインジェクション設定する、ってのとはちょっと違うかな…。