PlayFrameworkでSpringFrameworkを使う

こんばんは。Play! framework Advent Calendar 2011 jp #play_jaの12月23日の記事になります。よろしくお願いします。

さて、Spring Module知ってますか?

Play!にはSpring Moduleなるものがあります。これによってPlay!でもSpringFrameworkを使うことが可能になります。
Springを使うことのメリットとしては、それなりに大規模な開発において、インタフェースクラスを確実にきって、責務の所在を明らかにした実装を行うケースかと思います。
そして、Springを使うことでフィールドに依存ロジックをhaveする実装になり、外部から依存ロジックを注入可能になることで結果的にテスタビリティが上がるので個人的に好きだったりします。

そもそもPlay!が「そういう面倒なことしなくていいから、ささっと作って動かして確認してしまおうよ」という思想なのは百も承知ですが、AdventCalendarネタがかぶりまくってネタに困っているので、最後の手段でSpringModuleに手を出したわけでPlay!とSpringでなんかできないかなぁと思い試しに使ってみましたので、その情報を共有させていただきます。

例えば以下のような利用シーンはどうでしょうか。
・対外システム接続があり、外部への接続モジュールはスタブと容易に差し替えを可能にしておきたい場合

Springモジュールのインストール

ではやってみましょう。

$play install spring
~        _            _ 
~  _ __ | | __ _ _  _| |
~ | '_ \| |/ _' | || |_|
~ |  __/|_|\____|\__ (_)
~ |_|            |__/   
~
~ play! 1.2.4, http://www.playframework.org
~
~ Will install spring-1.0.2
~ This module is compatible with: 1.2.x
~ Do you want to install this version (y/n)? y
~ Installing module spring-1.0.2...
~
~ Fetching http://www.playframework.org/modules/spring-1.0.2.zip
~ [--------------------------100%-------------------------] 92670.2 KiB/s    
~ Unzipping...
~
~ Module spring-1.0.2 is installed!
~ You can now use it by adding it to the dependencies.yml file:
~
~ require:
~     play -> spring 1.0.2

するとPlay!のフォルダ配下のmodulesにspring-1.0.2が入ります。
そこで、気になるのがSpringのバージョンです。
playルート/modules/spring-1.0.2/lib/META-INF/MANIFEST.MFで中のjarのバージョンを確認してみます。

   :
   :
Spring-Version: 2.5.5
Implementation-Title: Spring Framework
Implementation-Version: 2.5.5
Tool: Bnd-0.0.208
Bundle-Name: spring-core
Created-By: 1.6.0_06 (Sun Microsystems Inc.)
   :
   :

どうやら、Springは2.5系統が組み込まれているようです。しかし、最新のSpringは3ですね。どうせなら、3にバージョンアップしてしまいましょう。
やり方は簡単で、playルート/modules/spring-1.0.2/lib/配下のSpring関連のjarを最新のSpringのjarに入れ替えればよいです。
このlibフォルダにはplay実行時にクラスパスが張られるため、jarのファイル名等は気にしなくて問題ありません。

spring-beans.jar
spring-context.jar
spring-core.jar

元々入っていた上記のjarを以下に置き換えました。

aopalliance-1.0.jar
asm-3.1.jar
aspectjweaver.jar
org.springframework.aop-3.0.4.RELEASE.jar
org.springframework.asm-3.0.4.RELEASE.jar
org.springframework.aspects-3.0.4.RELEASE.jar
org.springframework.beans-3.0.4.RELEASE.jar
org.springframework.context-3.0.4.RELEASE.jar
org.springframework.context.support-3.0.4.RELEASE.jar
org.springframework.core-3.0.4.RELEASE.jar
org.springframework.expression-3.0.4.RELEASE.jar
org.springframework.instrument-3.0.4.RELEASE.jar
org.springframework.instrument.tomcat-3.0.4.RELEASE.jar
org.springframework.jdbc-3.0.4.RELEASE.jar
org.springframework.jms-3.0.4.RELEASE.jar
org.springframework.orm-3.0.4.RELEASE.jar
org.springframework.oxm-3.0.4.RELEASE.jar
org.springframework.test-3.0.4.RELEASE.jar
org.springframework.transaction-3.0.4.RELEASE.jar
org.springframework.web-3.0.4.RELEASE.jar
org.springframework.web.portlet-3.0.4.RELEASE.jar
org.springframework.web.servlet-3.0.4.RELEASE.jar
org.springframework.web.struts-3.0.4.RELEASE.jar

Springのリビジョンがやや古いのは気にしないで下さい。手元にあった一式がこれだったので。
これで、晴れてSpring3.xの機能が使えるようになります。

ControllerにBeanをインジェクション

では、試しに何か適当に実行してみましょう。
確認としてApplicationクラス(Controller)に対して、Twitterに接続するためのクラスTwitterAccessor(実装はTwitterAccessorImpl)をインジェクションしてみます。
@Injectアノテーションをつけることで、application-context.xmlに定義したbeanが自動的にインジェクションされます。

Applicationクラスは以下の通り。

public class Application extends Controller {

	@Inject
	static TwitterAccessor twitterAccessor;

	public static void tweet(String userId, String message) {
		twitterAccessor.tweet(userId, message);
		renderJSON("success");
	}
}

application-context.xml

	<bean id="twitterAccessor" class="models.TwitterAccessorImpl"
		scope="singleton">
	</bean>

TwitterAccessorImpl.java

public class TwitterAccessorImpl implements TwitterAccessor {
	public void tweet(String twitterId, String message) {
		System.out.println("tweet!");
	}
}

実行結果は以下になります。

tweet!

正しくApplicationクラスにTwitterAccessorImplがインジェクションされていることがわかります。

SpringのAOPを動作させる

インジェクションが実現出来るとSpring使い的に次ぎにやりたくなるのがAOPです。そこで、AOP確認用に以下のクラスを用意します。単純にメソッドの実行の前後にログを出力するだけのインターセプターです。

public class LoggingInterceptor implements MethodInterceptor {
	public Object invoke(MethodInvocation invocation) throws Throwable {
		Logger log = Logger.getLogger(invocation.getMethod().getName());
		log.debug(">>>>>proccess_start");

		Object ret = null;
		try {
			ret = invocation.proceed();
		} catch (Exception e) {
			log.error(">>>>>proccess_exception");
			throw e;
		} finally {
			log.debug(">>>>>proccess_finished");
		}

		return ret;
	}
}

application-context.xmlには以下を追記します。TwitterAccessorImplの何らかのメソッドがコールされるタイミングでAOPによるLoggingInterceptorを差し込みます。

	<bean id="logging" class="aop.LoggingInterceptor" />
	<aop:config>
		<aop:advisor
			pointcut="execution(* models.TwitterAccessorImpl.*(..))"
			advice-ref="logging" />
	</aop:config>

実行結果は以下です。

tweet!

実は、@InjectではAOPによるProxyクラスを経由したオブジェクトがインジェクションされるのではなく、そのままのインジェクションになります。そのため、@Injectでは、AOPは効きません。

そこで別の方法でBeanを取得してみます。Play!のSpringモジュールにはSpringクラスというものがあります。

public class Application extends Controller {
                             :
                             :
	public static void tweetWithAOP(String userId, String message) {
		TwitterAccessor ta = (TwitterAccessor) Spring.getBean("twitterAccessor");
		ta.tweet(userId, message);
		renderJSON(result);
	}
}

実行結果は以下です。これでもダメです。

tweet!

しょうがないので、Play!のSpringモジュール内のクラス経由でApplicationContextにアクセスするのではなく、純粋にApplicationContextをインスタンス化して、そこから取り出すことにしました。これなら確実に出来るはず。

public class Application extends Controller {
                             :
                             :
	public static void tweetWithPureSpringAOP(String userId, String message) {
		ApplicationContext ac = new FileSystemXmlApplicationContext("/"
				+ Play.applicationPath + "/conf/application-context.xml");
		TwitterAccessor ta = (TwitterAccessor) ac.getBean("twitterAccessor");
		ta.tweet(userId, message);
	}
}

実行結果は以下になります。想定通りAOPによるログ出力処理が実行出来ました。

2011-12-23 04:12:28,740 DEBUG [play-thread-1](LoggingInterceptor:invoke) >>>>>proccess_start
tweet!
2011-12-23 04:12:28,740 DEBUG [play-thread-1](LoggingInterceptor:invoke) >>>>>proccess_finished

@InjectでSpringのAOPを動作させる(未動作確認)

@InjectでAOPをかけることが出来ないか調べてみました。実際に実行して試してはいないのですが、多分いけると思うのでメモとして残しておきます。

SpringモジュールにはSpringPluginというクラスがあります。PlayPluginはモジュールを作成する際に継承するクラスで、onApplicationStartメソッド等のイベントメソッドがきられていて、アプリケーションの起動時、終了時等様々な契機で処理を実行できます。SpringPluginではアプリケーション起動時の最後にInjector.inject(this);という処理をコールしています。

public class SpringPlugin extends PlayPlugin implements BeanSource {
       :
       :
    @Override
    public void onApplicationStart() {
        URL url = Play.classloader.getResource(Play.id + ".application-context.xml");
        if (url == null) {
            url = Play.classloader.getResource("application-context.xml");
        }
        if (url != null) {
            InputStream is = null;
            try {
                Logger.debug("Starting Spring application context");
                applicationContext = new GenericApplicationContext();
                applicationContext.setClassLoader(Play.classloader);
                XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(applicationContext);
                if (Play.configuration.getProperty(PLAY_SPRING_NAMESPACE_AWARE,
                                                   "false").equals("true")) {
                    xmlReader.setNamespaceAware(true);
                }
                xmlReader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE);

                if (Play.configuration.getProperty(PLAY_SPRING_ADD_PLAY_PROPERTIES,
                                                   "true").equals("true")) {
                    Logger.debug("Adding PropertyPlaceholderConfigurer with Play properties");
                    PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer();
                    configurer.setProperties(Play.configuration);
                    applicationContext.addBeanFactoryPostProcessor(configurer);
                } else {
                    Logger.debug("PropertyPlaceholderConfigurer with Play properties NOT added");
                }
                //
                //	Check for component scan 
                //
                boolean doComponentScan = Play.configuration.getProperty(PLAY_SPRING_COMPONENT_SCAN_FLAG, "false").equals("true");
                Logger.debug("Spring configuration do component scan: " + doComponentScan);
                if (doComponentScan) {
                    ClassPathBeanDefinitionScanner scanner = new PlayClassPathBeanDefinitionScanner(applicationContext);
                    String scanBasePackage = Play.configuration.getProperty(PLAY_SPRING_COMPONENT_SCAN_BASE_PACKAGES, "");
                    Logger.debug("Base package for scan: " + scanBasePackage);
                    Logger.debug("Scanning...");
                    scanner.scan(scanBasePackage.split(","));
                    Logger.debug("... component scanning complete");
                }

                is = url.openStream();
                xmlReader.loadBeanDefinitions(new InputSource(is));
                ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
                Thread.currentThread().setContextClassLoader(Play.classloader);
                try {
                    applicationContext.refresh();
                    startDate = System.currentTimeMillis();
                } catch (BeanCreationException e) {
                    Throwable ex = e.getCause();
                    if (ex instanceof PlayException) {
                        throw (PlayException) ex;
                    } else {
                        throw e;
                    }
                } finally {
                    Thread.currentThread().setContextClassLoader(originalClassLoader);
                }
            } catch (IOException e) {
                Logger.error(e, "Can't load spring config file");
            } finally {
                if (is != null) {
                    try {
                        is.close();
                    } catch (IOException e) {
                        Logger.error(e, "Can't close spring config file stream");
                    }
                }
            }
        }
        Injector.inject(this);
    }

    public <T> T getBeanOfType(Class<T> clazz) {
        Map<String, T> beans = applicationContext.getBeansOfType(clazz);
        if (beans.size() == 0) {
            return null;
        }
        return beans.values().iterator().next();
    }
}

Injectorは以下になります。やっていることは各フィールドをリフレクションでアノテーションを取得し、Injectアノテーションの場合は、SpringPluginのgetBeanOfType()をコールするだけです。

public class Injector {
    /**
     * For now, inject beans in controllers
     */
    public static void inject(BeanSource source) {
        List<Class> classes = Play.classloader.getAssignableClasses(ControllerSupport.class);
        classes.addAll(Play.classloader.getAssignableClasses(Mailer.class));
        classes.addAll(Play.classloader.getAssignableClasses(Job.class));
        for(Class<?> clazz : classes) {
            for(Field field : clazz.getDeclaredFields()) {
                if(Modifier.isStatic(field.getModifiers()) && field.isAnnotationPresent(Inject.class)) {
                    Class<?> type = field.getType();
                    field.setAccessible(true);
                    try {
                        field.set(null, source.getBeanOfType(type));
                    } catch(RuntimeException e) {
                        throw e;
                    } catch(Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}

つまり、SpringPluginのgetBeanOfTypeメソッドをオーバーライドして型ベースではなくBeanIdをもとにgetBeanするように修正すれば、@InjectでAOPを効かせることも可能だと思われます。(未検証ですみません><)

Play! + Spring + MyBatis連携

Spring3が動くようになったついでに、MyBatisも組み込んでみようと思い試してみました。
MybatisジェネレータでデータベースからORMapperを自動生成して、Spring-Mybatis連携モジュールを使って、正常に動作しました。
Play!でSpring3が動けばあとは単純にSpringとMyBatisの連携をするだけなので、従来通りのやり方で可能でした。
MyBatis-Spring連携の説明

さいごに

まとめると、
・Play!でもSpringが最新バージョンで動く
・@InjectそのままではAOPは効かないけど、やりようによってはAOPもかけれる
・MyBatisも使えるよ

次は@hina0118さんです。よろしくお願いします!