google guiceのAOPで一括設定

唐突にgoogle guiceでなんなのですが。

google guiceAOPというと、私の勝手なイメージですが、

上記のユーザーズガイドの「Intercepting Methods」の項にあるサンプルコードの、

binder.bindInterceptor(
  any(),
  annotatedWith(Transactional.class),
  new TransactionInterceptor()
);

にあるようなイメージです。ようするに、

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
@BindingAnnotation
public @interface CalledLogging {
	// NOB.
}

こんなアノテーションがあったとして、

public interface Service {
	public void execute();
}

こんなサービスのインターフェースがあったとすると、

public class ServiceImpl implements Service {
	@CalledLogging
	public void execute() {
		System.out.println("サービスメソッド実行");
	}	
}

こんな風に、AOP対象メソッドにアノテーションでマーキングするイメージです。これはこれでシンプルで分かりやすいのですが、上記のようにロギングをAOPで刷り込む場合、正直

アノテーションでマーキングするのも直接log4jでロギングするのも対して変わらん

と思うんですよね。トランザクション管理とかならともかく。業務Webアプリでは、とりあえず業務機能の実装を始めといて、要件が決まり次第後から監査ログ的なものを織り込むとかいうこともままあると思うのですが、そういう場合に後からロギング対象の全メソッドにいちいちアノテーションを入れて回るのも正直相当面倒くさいと思います。

面倒くさいだけならいいのですが、人のやることですからアノテーションを入れ忘れることもある訳ですし、ちょっと属人性にたより気味なところも個人的にはちょっぴり微妙です。

要するに

シンプルで分かりやすいアノテーションベースのAOPもいいですけど、設定ベースでがっつり一括で業務機能は修正すること無しにAOPってのもやっぱり必要だと思う今日この頃な訳です。

そんな訳で

ちょっぴり試してみました。要件的には、

  • 「ServiceImpl」で終わるクラス名の、「Excecute」で終わるメソッドの実行前後のみにログ出力する

ということをやってみたいと思います。

まず、第一のサービスのインターフェースとして、

public interface FirstService {
    public String doExecute();
    
    public String run();
}

こんなものを用意し、その実装として、

public class FirstServiceImpl implements FirstService {
    private SecondService secondService = null;
    
    @Inject
    public void setSecondService(SecondService secondService) {
        this.secondService = secondService;
    }
            
    public String doExecute() {
        return run();
    }

    public String run() {
        return secondService.doExecute();
    }
}

こんなものを用意します。この「FirstServiceImpl」が利用している「SecondService」のインターフェースは、

public interface SecondService {
    public String doExecute();
    
    public String run();
}

こんなものを用意し、実装は、

public class SecondServiceImpl implements SecondService {
    private ThirdDao thirdDao = null;
    
    @Inject
    public void setThirdDao(ThirdDao thirdDao) {
        this.thirdDao = thirdDao;
    }
    
    public String doExecute() {
        return run();
    }

    public String run() {
        return thirdDao.doExecute();
    }
}

こんな感じにします。で、この「SecondServiceImpl」が利用している「ThirdDao」のインターフェースは

public interface ThirdDao {
    public String doExecute();
    
    public String run();
}

こんな感じにし、その実装は

public class ThirdDaoImpl implements ThirdDao {
    public String doExecute() {
        return run();
    }

    public String run() {
        return "Third dao executed.";
    }
}

こんな感じであったとします。

ロギングを行うメソッドインターセプターは、AOPアライアンスが提供する「MethodInterceptor」をimplementsして、

public class AroundLoggingInterceptor implements MethodInterceptor {
    public Object invoke(MethodInvocation invocation) throws Throwable {
        String methodName = invocation.getMethod().getDeclaringClass().getName() + "#" + invocation.getMethod().getName();

        // 開始ログ
        System.out.println("called : " + methodName);

        // メソッド実行
        Object returnObj = invocation.proceed();

        // 終了ログ
        System.out.println("end : " + methodName);

        return returnObj;
    }
}

こんなのを作成したとします。


肝心なのは、これらの依存性を解決するためのModuleをどう作成するかな訳ですが。とりあえず、インターフェースと実装は、

public class SampleModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(FirstService.class).to(FirstServiceImpl.class).in(Scopes.SINGLETON);
        bind(SecondService.class).to(SecondServiceImpl.class).in(Scopes.SINGLETON);
        bind(ThirdDao.class).to(ThirdDaoImpl.class).in(Scopes.SINGLETON);
    }
}

こんな感じでバインドするとして、問題はAOPの方です。

google guiceにおいては一般的に、AOPはAbstractModuleが提供するbindInterceptor()メソッドを用いて設定するみたいですが、

上記のJavaDocによると、bindInterceptor()の引数は以下のようになっているようです。

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

即ち、第一引数でAOP対象となるクラスの条件を指定し、第二引数でAOP対象となるメソッドを指定し、第三引数で実際にメソッドの実行前後に織り込むインターセプターを指定するようです。

そこで

classMatcherとして

public class ServiceClassMatcher extends AbstractMatcher<Class> {
    public boolean matches(Class clazz) {
        if (clazz.getName().endsWith("ServiceImpl")) {
            return true;
        }
        return false;
    }
}

こんなクラスを作成し、methodMatcherとして

public class ServiceMethodMatcher extends AbstractMatcher<Method> {
    public boolean matches(Method t) {
        if (t.getName().endsWith("Execute")) {
            return true;
        }
        return false;
    }
}

としてこんなクラスを作成し、AbstractModule拡張クラスを最終的に

public class SampleModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(FirstService.class).to(FirstServiceImpl.class).in(Scopes.SINGLETON);
        bind(SecondService.class).to(SecondServiceImpl.class).in(Scopes.SINGLETON);
        bind(ThirdDao.class).to(ThirdDaoImpl.class).in(Scopes.SINGLETON);
        bindInterceptor(new ServiceClassMatcher(), new ServiceMethodMatcher(), new AroundLoggingInterceptor());
    }
}

こんな風にしてみたとします。そして、これらを利用するクライアントコードとして、

public class Client {
    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new SampleModule());

        FirstService service = injector.getInstance(FirstService.class);

        System.out.println(service.doExecute());
    }
}

こんなものを作って実行してみると・・・

called : org.tiba.impl.FirstServiceImpl#doExecute
called : org.tiba.impl.SecondServiceImpl#doExecute
end : org.tiba.impl.SecondServiceImpl#doExecute
end : org.tiba.impl.FirstServiceImpl#doExecute

おぉぉぉぉぉぉぉぉぉつぉおぉつおぉぉぉぉ!!

見事に、「ServiceImpl」で終わるクラス名の、「Execute」で終わるメソッド名の実行前後にのみ、ロギングを織り込めました。

今回は、「AbstractMatcher」拡張クラスを静的に定義してみましたが、無名クラスを使ってAOPの条件を指定してみてもなかなか面白そうです。