Google Guice(その3) AOP

何ヶ月か前にGuiceを見た時AOPが見当たらなかったので「これじゃSpringの代替としては使えん」と思っていたのですが、AOPが使えるようになっています。または、当時見つけられなかった私にも見つけられるようになりました。

その1で書いたように、GuiceAOPは非常にシンプル、その分高速に作ってあるそうです。AOP Alliance APIに準拠していますので、Springのインターセプタが使えるとの事。これは嬉しいですね。

という事で簡単なサンプルとして、AOPのお約束であるログ出力を。GuiceのUser's Guideでは後ろの方に数行しか記述が無いので、テストコードを見つつやってみました。

まずログを出力するインターセプタを新たに作成。

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class LoggingInterceptor implements MethodInterceptor {
	public Object invoke(MethodInvocation methodInvocation) throws Throwable {
		String methodName = methodInvocation.getMethod().getName();
		
		System.out.println("before: " + methodName);
		Object obj = methodInvocation.proceed();	// 本来の処理を実行
		System.out.println("after: " + methodName);
		
		return obj;
	}
}

そして、インターセプタを挟む。

import static com.google.inject.matcher.Matchers.*;	// 追加

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;

public class Main {
	public static void main(String[] args) {
		Injector injector = Guice.createInjector(new MyModule());
		Client client = injector.getInstance(Client.class);
		client.execute();
	}
	
	static class MyModule extends AbstractModule {
		@Override
		protected void configure() {
			bind(Service.class).to(ServiceImpl.class);
			bindInterceptor(any(), any(), new LoggingInterceptor());	// 追加
		}
	}
}

「追加」とコメントしたところが、今回追加した部分です。*1
bindInterceptor()でAOPの設定をしています。このメソッド、GuiceAPIを見ると

protected void bindInterceptor(Matcher<? super Class<?>> classMatcher, Matcher<? super Method> methodMatcher, MethodInterceptor... interceptors)

となっていてえらい難しそうですが、staticインポートしているMatchersに便利メソッドが用意されているので、それを使います。

という事で実行結果

before: execute
before: getResponse
after: getResponse
Re: Hello
after: execute

…いや、これは違う。executeの前後にも実行されているのは(少なくとも私には)想定外です。
injector.getInstance()したクラスにも適用されるんだと学んだところで、Serviceのメソッドにだけ適用するように修正します。
bindInterceptor()の第一引数が適用クラスの指定で、今のMatchers#any()だと全クラスになるようです。そこで
Matchers#inPackage()というメソッドを使ってみます。

bindInterceptor(inPackage(Service.class.getPackage()), any(), new LoggingInterceptor());

実行すると

before: getResponse
after: getResponse
Re: Hello

めでたくServiceのメソッドにだけ適用されました。
Matchersには他にもsubclassesOf()とかannotatedWith()といったメソッドがあるので、今後試してみたいと思います(後者は@Transactionなどで使うようです)。

ちなみにこれまで、サンプルを全部1つのパッケージで作っていたのですが、上記Serviceのみ適用をする為に別パッケージに移しました。その際、IDEリファクタリング機能一発で移動が完了し、Javaのソースで書いてると定義とずれる心配が無いというメリットを実感しました。(Springプラグイン等を使っていればクラスが存在しないとXML定義でエラーを出してくれますが、たぶん自動では直せないので→「おのぼり」さんからコメント頂いた通り、Eclipseリファクタリング機能はテキストに対しても修正をかけられるので、それで目的は果たせるようです。失礼しました)。

*1:こういう時はてな記法でソースを部分的にハイライトするってできるんでしょうか?誰か知ってたら教えて下さい