(その11) Springframeworkとの統合(1) Spring管理のBeanをGuiceに一括登録

GuiceはSpringと統合する為のクラスを用意しています。その名もずばりSpringIntegration。統合と言っても様々な形態が考えられると思いますが、現時点でGuiceが用意しているのは、Springのコンテナで管理されているBeanをGuiceから取得/injectできるようにするものです。それほど凝った事はしていなくて、シンプルなBeanFactoryのラッパーとなっています。
SpringIntegrationクラスにはpublicメソッドが2つしか無いので、両方見てみます。今回はbindAll()メソッド。
SpringIntegration#bindAllメソッドを呼び出すと、Springに名前で登録されているBeanが全てGuiceに登録されます。その際、Guiceコンテナに登録されるインデックス(Keyクラス)は「型と名前のセット」になっています。JavaDocには

For a Spring bean named "foo", this method creates a binding to the bean's type and @Named("foo").

とあります。私はこれでちょっとはまったので、それについても書いていきます。
なお、GuiceSeasar2の統合手段として、SpringIntegrationクラスをS2用に移植したものを、id:shot6さんが公開されていますので、S2な方はそちらを試して頂ければと。恐らく動きは同じだと思います(違ったらごめんなさい)。

試してみる

まず、既存のSpring資産をそのまま統合するのが目的という事で、Springの定義をXMLで行います*1

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:aop="http://www.springframework.org/schema/aop"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
	
	<bean id="bar" class="sample.guice.springintegration.Beans$BarImpl">
		<constructor-arg index="0">
			<ref local="tee" />
		</constructor-arg>
	</bean>
	
	<bean id="tee" class="sample.guice.springintegration.Beans$TeeImpl">
		<constructor-arg index="0">
			<value>test</value>
		</constructor-arg>
	</bean>
</beans>

クラスとインタフェースは想像がつくと思うので省略。で、Guiceに登録。

// XML定義ファイルから、SpringのBeanFacotryを生成
final ApplicationContext context
		= new ClassPathXmlApplicationContext("/sample/guice/springintegration/applicationContext.xml");
Injector injector = Guice.createInjector(new AbstractModule() {
	@Override
	protected void configure() {
		SpringIntegration.bindAll(binder(), context);
	}
});

えらく簡単です。さっそくインスタンスを取得してみます。

Bar bar = injector.getInstance(Bar.class);
System.out.println(bar.getTee().getS());

実行結果。

Exception in thread "main" com.google.inject.ConfigurationException: Missing binding to sample.guice.springintegration.Beans$Bar.

ouch! Barで登録されていないと。そうでした、「型と名前」でbindされているので、型だけ(Bar.class)じゃ取得できないですね。名前をつけて宣言的にinjectするのははその10でやりましたが、プログラム的に取得する場合は下記のようになります。

// この指定は「@Names("bar")アノテーションがつけられたBar.class」というKeyを取得している。
Key<Bar> barKey = Key.get(Bar.class, Names.named("bar"));
Bar bar = injector.getInstance(barKey);
System.out.println(bar.getTee().getS());

実行結果。

Exception in thread "main" com.google.inject.ConfigurationException: Missing binding to sample.guice.springintegration.Beans$Bar annotated with @com.google.inject.name.Named(value=bar).

ouch! これはどこを間違っているんでしょうか…。
というところでちょっとはまったのですが、SpringIntegrationのソースを見たら判明。インデックスになっている「型と名前」の「型」は、beanFactory.getBean()で取得したインスタンスのClassそのものでした。つまり、Bar.class(インタフェース)ではなく、BarImpl.class(実装クラス)じゃないといかんのですね。

Key<BarImpl> barKey = Key.get(BarImpl.class, Names.named("bar"));
Bar bar = injector.getInstance(barKey);
System.out.println(bar.getTee().getS());

実行すると

Hero

おおっ、やっと正しく取得できました*2。これでSpringとGuiceを一緒に使えます。

しかし

…って、実装クラスを指定しなきゃいけなかったら、

@Inject
void inject(@Named("bar") Bar bar) {

だとエラーで、

@Inject
void inject(@Named("bar") BarImpl bar) {

にしないとダメなんじゃ?*3それ、なんか意味無くない?
何か間違っているのではと思って試してみましたが、やはりインタフェース指定では取得できません。まあ考えてみればSpringの定義ファイルには実装クラスしか書いていないから、インタフェースで登録されていないのは当然かもしれません。しかし使いたいのは実装クラスじゃなくてインタフェースな場合が大半です。
そこで、あるべき姿なのかどうかはともかく、bindAll()に下記のようなコードを追加してみました。

  for(Class<?> ifc : type.getInterfaces()) {
    bindBean(binder, beanFactory, name, ifc);
  }

これで強引ながらもBar.classに対してBarImpl.classをinjectできました。
…しかしやってはみたものの、これだと使わないものを含めて全インタフェースを登録してしまいます。また、そもそもinjectする箇所ごとに@Named("foo")と書くのは、タイプセーフなGuiceのメリットを殺しているので好ましくありません。
という事で、一見非常にお手軽に見えたbindAll()はいまいち使い難いようです。そこで次回は、もう1つの統合手段であるfromSpring()を見てみます。

User's Guideの翻訳

ところで、id:iad_otomamayさんがGuiceUser's Guideをえらい勢いで翻訳されています(もう殆ど終わりかけてます。すごい)。ご覧あれ。

*1:Guiceのテストコードだとプログラム的にBeanFactoryを作ってますが、普通やらないですよね

*2:この表示が正しいかどうかは、読んでる方には分からないですが、正しいと思って下さい

*3:実際ダメでした