AspectJを使ってAnnotationを活用しよう 後編

前回の記事はこちら。

前回の記事では、簡単なAnnotationの作り方と使い方を説明したわけですが、今回はAspectJを使ってメソッドに設定されたAnnotationを取得する方法と、取得した値を保持して別の場所で呼び出す方法を説明します。
今回の記事では、SpringベースのWebアプリケーションをもとに説明します。一から十まで説明するとたぶん長くなるので、かいつまんで説明します。なので、端折られている部分は脳内補完とかよその記事を参考にしていただくということで・・・。

※ 書き終わってから思ったけど、別にwebappじゃなくてもいいですね・・・
まずは、Mavenで作成したWebアプリケーションのEclipseプロジェクトを用意しといてください。例えば、以下のコマンドを実行するとか。

$ mvn archetype:generate
以下のarchetypeを選べばOK
org.apache.maven.archetypes:maven-archetype-webapp
$ cd [作ったプロジェクトフォルダ]
$ mvn eclipse:eclipse

できあがったプロジェクトに「src/main/java」フォルダを追加して、ビルドパスを通しておくのをお忘れなく。
用意したプロジェクトは、Spring Frameworkと連携します。まずはpom.xmlDependencyを追加。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.atphis.webapp</groupId>
  <artifactId>jupitris-webapp</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>
  <dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>3.0.6.RELEASE</version>
        <type>jar</type>
        <scope>compile</scope>
    </dependency>
    <dependency>
    	<groupId>org.slf4j</groupId>
    	<artifactId>jcl-over-slf4j</artifactId>
    	<version>1.6.4</version>
    	<type>jar</type>
    	<scope>compile</scope>
    </dependency>
    <dependency>
    	<groupId>org.slf4j</groupId>
    	<artifactId>slf4j-api</artifactId>
    	<version>1.6.4</version>
    	<type>jar</type>
    	<scope>compile</scope>
    </dependency>
    <dependency>
    	<groupId>ch.qos.logback</groupId>
    	<artifactId>logback-classic</artifactId>
    	<version>1.0.0</version>
    	<type>jar</type>
    	<scope>compile</scope>
    </dependency>
    <dependency>
    	<groupId>ch.qos.logback</groupId>
    	<artifactId>logback-core</artifactId>
    	<version>1.0.0</version>
    	<type>jar</type>
    	<scope>compile</scope>
    </dependency>
    <dependency>
    	<groupId>aspectj</groupId>
    	<artifactId>aspectjrt</artifactId>
    	<version>1.5.4</version>
    	<scope>compile</scope>
    </dependency>
    <dependency>
    	<groupId>aspectj</groupId>
    	<artifactId>aspectjweaver</artifactId>
    	<version>1.5.4</version>
    	<scope>compile</scope>
    </dependency>
    <dependency>
    	<groupId>cglib</groupId>
    	<artifactId>cglib</artifactId>
    	<version>2.2.2</version>
    	<type>jar</type>
    	<scope>compile</scope>
    </dependency>
    <dependency>
    	<groupId>junit</groupId>
    	<artifactId>junit</artifactId>
    	<version>4.10</version>
    	<type>jar</type>
    	<scope>test</scope>
    </dependency>
    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-test</artifactId>
    	<version>3.0.6.RELEASE</version>
    	<type>jar</type>
    	<scope>test</scope>
    </dependency>
  </dependencies>
</project>

次は、「src/main/resources」配下にspring-beans.xmlファイルを作成。中身はこんな感じで。ちなみに、Eclipseのビルドパスでは、「src/main/resources」はExcludedされているので解除しておくこと。

<?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:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

    <aop:aspectj-autoproxy />
    
    <context:component-scan base-package="jp.example.test">
        <context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>
    </context:component-scan>

</beans>

前回の記事で作成したクラスを追加(AspectJを使ってAnnotationを活用しよう 前編 - Jupitris on Laboratory)。LogicクラスだけはDIするので、下記のように@Serviceアノテーションをつけておいてください。

@Service
public class SampleLogic {

    @SampleAnnotation(SampleEnum.SAMPLE2)
    public void sample(String s) {
        System.out.println("arg=" + s);
    }

}

これで、ひと通りの準備は完了。あとは、Interceptorを作って、アノテーションを取得してみましょう。Interceptorの実装はこんな感じで。

@Aspect
public class SampleInterceptor {

    @Around("bean(*Logic) && (@annotation(annotation))")
    public Object
            sampleInvoke(ProceedingJoinPoint proceedingJoinPoint, SampleAnnotation annotation)
                    throws Throwable {
        String arg = proceedingJoinPoint.getArgs()[0].toString();
        String s = String.format("%s - aspect logging.", arg);
        Object[] args = { s };
        return proceedingJoinPoint.proceed(args);
    }
}

Aroundアノテーションにつけているpointcutは、アノテーションが設定されているなんらかのLogicBeanが実行されたらインターセプトするという意味です。インターセプトしたら、引数を編集して再度対象メソッドを実行しています。

テストコードを書いて、ほんとうにAspectされているか確認してみよう。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:spring-beans.xml" })
public class SampleLogicTest {

    @Resource
    private SampleLogic sampleLogic;

    @Test
    public void testSample01() {
        sampleLogic.sample("test for aspectj");
    }
}

実行してみると、コンソールにずらずらと表示されるログの中に、一行だけAspectされた内容の出力があります。

・・・省略・・・
02:33:28.087 [main] DEBUG o.s.b.f.annotation.InjectionMetadata - Processing injected method of bean 'jp.example.test.sample.SampleLogicTest': ResourceElement for private jp.example.test.sample.SampleLogic jp.example.test.sample.SampleLogicTest.sampleLogic
02:33:28.087 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'sampleLogic'
02:33:28.100 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'sampleInterceptor'
ココ!=>arg=test for aspectj - aspect logging.
02:33:28.135 [main] DEBUG o.s.t.c.s.DirtiesContextTestExecutionListener - After test method: context [[TestContext@21aed5f9 testClass = SampleLogicTest, locations = array<String>['classpath:spring-beans.xml'], testInstance = jp.example.test.sample.SampleLogicTest@5d61dfb5, testMethod = testSample01@SampleLogicTest, testException = [null]]], class dirties context [false], class mode [null], method dirties context [false].
02:33:28.136 [main] DEBUG o.s.t.c.s.DirtiesContextTestExecutionListener - After test class: context [[TestContext@21aed5f9 testClass = SampleLogicTest, locations = array<String>['classpath:spring-beans.xml'], testInstance = [null], testMethod = [null], testException = [null]]], dirtiesContext [false].
02:33:28.164 [Thread-2] INFO  o.s.c.s.GenericApplicationContext - Closing org.springframework.context.support.GenericApplicationContext@1cec6b00: startup date [Wed Feb 01 02:33:27 JST 2012]; root of context hierarchy
・・・省略・・・

やっとAspectするところまできました。あとはAnnotationの値を取得して保持するだけ。まずは、値を保持しておくクラスの作成から。

public class SampleHolder {

    private static ThreadLocal<SampleEnum> holder = new ThreadLocal<SampleEnum>();

    public static void setHolder(SampleEnum value) {
        Assert.notNull(value, "value cannot be null.");
        holder.set(value);
    }

    public static SampleEnum getHolder() {
        if (holder == null) {
            holder.set(SampleEnum.SAMPLE1);
        }
        return holder.get();
    }

    public static void clearHolder() {
        holder.remove();
    }
}

ThreadLocalを使用してAnnotationの値を保持する仕組みです。次はInnterceptorに、以下の一文を追加してください。

・・・省略・・・
    @Around("bean(*Logic) && (@annotation(annotation))")
    public Object
            sampleInvoke(ProceedingJoinPoint proceedingJoinPoint, SampleAnnotation annotation)
                    throws Throwable {
        // set SampleEnum to ThreadLocal
        SampleHolder.setHolder(annotation.value());

        String arg = proceedingJoinPoint.getArgs()[0].toString();
・・・省略・・・

さっき作成したテストクラスで、SampleHolder#getHolderを呼び出してみましょう。

・・・省略・・・
    @Test
    public void testSample01() {
        sampleLogic.sample("test for aspectj");
        logger.debug(SampleHolder.getHolder().getValue());
    }
・・・省略・・・

テストを実行してログを見てみると・・・

02:45:29.542 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'sampleInterceptor'
arg=test for aspectj - aspect logging.
ココ!=>02:45:29.579 [main] DEBUG j.e.test.sample.SampleLogicTest - sample2
02:45:29.580 [main] DEBUG o.s.t.c.s.DirtiesContextTestExecutionListener - After test method: context [[TestContext@5d61dfb5 testClass = SampleLogicTest, locations = array<String>['classpath:spring-beans.xml'], testInstance = jp.example.test.sample.SampleLogicTest@44a613f8, testMethod = testSample01@SampleLogicTest, testException = [null]]], class dirties context [false], class mode [null], method dirties context [false].

うまくいきましたね!*1

今回はSpringとAspectJアノテーションによる手法を紹介しました。前回の記事とあわせて、Annotationを有効活用していただければ。ほんとはSpringAOPを利用した手法も紹介したかったけど、長くなってしまったのでこの記事はいったん終了。
次回、ちょろっと説明します。お待ちくださいませ。

*1:作りながら書いているので、うまくいくのは当然だけど