その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

GuiceとSpringを共存させたい

もともとGuiceを使っているプロジェクトがあってそれをSpringに移植したい,だとか,Guiceをバリバリ使っているコンポーネントをSpringのアプリに組み込むことで資源の再利用をしたい,だとか,そういう事になることがある.あるのです.この前実際になった.

で,"spring guice together" とかそういう適当な検索ワードで調べると大体「そんな事はするな」「そうしたい意味がわからん」「そう考える時点でおかしいのでは」などといった,「とにかくやるな」という結果が引っかかる.確かに.しかしやらなければならない時があるのだ……というわけでそのメモを記す.

TL;DR

やらないで済むならやらないほうが良い.

本編

ご存知の通りGuiceGoogle製のDI Frameworkで,一方のSpring frameworkはWeb Application Frameworkである.この二者間になぜ共存の問題が発生するかというと,Spring Frameworkもその内部にDI Frameworkを持っているから,というかそもそもDI機構がSpringの中核を担っているからである.つまりDI Frameworkが2個ある状態となるなのでそれらを矛盾なく管理する必要が出てくる.どうすれば良いのか.

アプローチ

SpringのDIをメインで利用して,Guiceはサブとして扱うという方法を今回は採用した.簡単に言うと,SpringのDIコンテナに対してGuiceのInjectorをInjectするという手法.
こんな感じ.

@Configuration
public class GuiceModuleConfig {
    @Bean
    Injector injector() {
        return Guice.createInjector(new AppModule());
    }
}

AppModuleGuiceのModuleで,このModuleを元にinjectorを作成し,そのinjectorをSpringのDIにinjectしてSpring DIから取得可能なようにしておく.そんでもって以下のようにAppModuleで提供されているinjecteeをSpringのDIを通じて取得できるようにしてやる.

@Bean
DataSource dataSource(final Injector injector) {
    return injector.getInstance(DataSource.class);
}

@Bean
FluentLogger fluentLogger(final Injector injector) {
    return injector.getInstance(FluentLogger.class);
}

つまりはSpringのDIにinjectしたGuiceのinjectorをSpring DI経由で引っ張ってきて,更にそのinjectorからGuice経由で任意のinstanceを取ってくるという感じ.とりあえずこれで基本的なGuiceSpring FrameworkのDI機構を共存させることが可能となる.なおSpringのDIについてはGuiceがあろうとなかろうといつものように利用できる.

Guiceで言うところのServletScopes.REQUEST使いたいときにどうするのか

@RequestScopeを使う.以下のように.

@Bean
@RequestScope
Connection connection(final Injector injector) {
    return injector.getInstance(Connection.class);
}

しかしGuice経由でinstanceを取ってきている場合はこれだけでは不十分で,GuiceServletContextListenerを継承したClassをServletContextListenerとして登録してやる必要がある.

@WebListener
public class AppGuiceConfig extends GuiceServletContextListener {
    private ServletContext servletContext;

    @Override
    public void contextInitialized(final ServletContextEvent servletContextEvent) {
        servletContext = servletContextEvent.getServletContext();
        super.contextInitialized(servletContextEvent);
    }

    @Override
    protected Injector getInjector() {
        return Guice.createInjector(new AppModule());
    }
}

なおSpring-Boot等でweb.xmlを利用しない場合は上記のように@WebListenerを用いてListenerとして認識させてやる必要がある.

雑感

2つのDI Frameworkを共存させ,管理する事となるので複雑さとコストが増すように感じる.また,適切に双方のinjecteeのライフサイクルのハンドリングも正しく行わなければならないのでその点においても複雑さが増大する.やらなくて済むのであればやらないに越したことは無いと思う.

以上です.我々はやっていっています.

[追記]

https://github.com/spring-projects/spring-guice

これ使えば楽に出来そうな雰囲気があるんだけど,maven centralに上がっていなくて面倒だったので採用を見送ったという経緯があります.未検証.