技術空間

Spring

SpringAOPの導入&サンプルプログラム


TOP > Spring > SpringAOPの導入&サンプルプログラム



■SpringAOPとは

■SpringAOPの導入
jarの入手

Springに必要なjarについてはSpringの導入&サンプルプログラムを参照。(SpringAOPのコアとなるのはspring-aop-4.2.4.RELEASE.jar、spring-aspects-4.2.4.RELEASE.jar)

また下記のjarも必要。

両方ともMaven Repositoryのサイト(http://mvnrepository.com/)からダウンロードできる。(それぞれ"aopalliance"、"AspectJ Weaver"などで検索すると表示される。)

■サンプル実行
設定ファイルの作成

Springの設定ファイルを作成する。

ApplicationContext.xml
<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-4.2.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-4.2.xsd
    http://www.springframework.org/schema/aop 
    http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

    <-- ①component-scanの定義 -->
    <context:component-scan base-package="aoptest" />
    
    <-- ②AspectJを有効にする設定 -->
    <aop:aspectj-autoproxy />
    
    <-- ③Aspectクラスの定義 -->
    <bean id="myAspect" class="aoptest.SpringSampleAspect" />

</beans>
[説明]

①component-scanの定義

<context:component-scan base-package="aoptest" />

component-scanを利用して、aoptestパッケージ配下のクラスを自動的にインジェクションさせる。Springの一般的な定義。


②AspectJを有効にする設定

<aop:aspectj-autoproxy />

AspectJのアノテーションスタイルが利用できるようにする設定。ソースに@Configuration@EnableAspectJAutoProxyを付与すれば省略可能(後述)。


③Aspectクラスの定義

<bean id="myAspect" class="aoptest.SpringSampleAspect" />

挿入する処理を持つAspectクラス(のちに自作)のbean定義。①の自動スキャン対象外のため、別途bean定義が必要。id属性は省略してもよい。


Javaの作成

まずは処理が挿入される通常のメソッドを作成する。サンプルでは単純な文字列を出力するoutメソッドにする。本クラスは@Componentアノテーションが付与されているため、component-scanによって自動インジェクションされる。

SpringSample.java
package aoptest;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;

@Component
public class SpringSample {
    
    public static void main(String[] args){
        ApplicationContext context = 
                   new ClassPathXmlApplicationContext("ApplicationContext7.xml");
        SpringSample ss = context.getBean(SpringSample.class);
        ss.out();
    }
    
    // 文字列を出力するメソッド
    public void out(){
        System.out.println("SpringAOP Test");
    }
}

[補足]
クラス宣言に以下のアノテーションを付与すれば、Spring設定ファイルの<aop:aspectj-autoproxy />の記述は不要となる。但し、クラス個別に付けなくてはいけないので<aop:aspectj-autoproxy />の方が楽ではある。

package aoptest;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;

@Component
@Configuration
@EnableAspectJAutoProxy
public class SpringSample {
Aspectクラスの作成

前述で作成したSpringSampleクラスのoutメソッドの前後に処理を挿入してみる。下記のアノテーションを使用する。

SpringSampleAspect.java
package aoptest;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

// ①クラス宣言に@Aspectを付加
@Aspect
public class SpringSampleAspect {
    
    // ②メソッドの前で実行する処理
    @Before("execution(* aoptest.*.*(..))")
    public void before(JoinPoint joinPoint){
        System.out.println("before");
    }
    
    // ③メソッドの後で実行する処理
    @After("execution(* aoptest.*.*(..))")
    public void after(JoinPoint joinPoint){
        System.out.println("after");
    }
}
[説明]

①クラス宣言に@Aspectを付加

@Aspect
public class SpringSampleAspect {

クラス宣言の上部に@Aspectアノテーションを付ける。

なお@Aspectは@Componentと違い、Springの自動インジェクション対象とはならないため、ApplicationContext.xmlにbean定義を記述してインジェクトさせる。

<bean id="myAspect" class="aoptest.SpringAopSample" />

②メソッドの前で実行する処理

@Before("execution(* aoptest.*.*(..))")
public void before(JoinPoint joinPoint){
    System.out.println("before");
}

メソッドの実行前に処理を挿入させたい場合、@Beforeアノテーションを付与する。@Beforeのパラメータにはexecutionに続けて挿入対象となるメソッド(の条件)を記述する。上記サンプルの場合aoptestパッケージ配下のメソッドが対象となる。

上記の記述を順番に分解すると、

となる。

ワイルドカード「*」やメソッド引数の「..」は何でもOKを意味する。パッケージ、クラス名、メソッド名は「"execution(* *(..))"」のようにまとめて「*」にしてもよい。

なお、全てフルで記述した場合、以下のような記述になる。(※サンプルと違って戻り値、引数ありの例)

@Before("execution(String aoptest.SpringSample.out(String, String))")

この記述方式は@Afterや後述する@Aroundなどでも同じ。

なお、execution以外にも、within、target、argsなどで対象のメソッドを絞ることができる。

表現例説明
within(test.*)パッケージで絞る
target(test.SampleInterface)インタフェースで絞る
args(java.lang.String)引数で絞る

最後にbeforeメソッドの引数にはJoinPointクラスを指定する。これに実行するメソッドの情報(シグニチャや引数の値など)が格納されている。特に使用しない場合は引数に指定しなくてもよい。

public void before(JoinPoint joinPoint){

③メソッドの後で実行する処理

@After("execution(* aoptest.*.*(..))")
public void after(JoinPoint joinPoint){
    System.out.println("after");
}

メソッドの後で実行させたい場合、@Afterアノテーションを付与する。記述方式やメソッド引数などは@Beforeと同じ。

なお、アノテーションには@Before、@After含めて以下の種類がある。@Around@AfterReturning@AfterThrowingは後述する。

アノテーション説明
@Beforeメソッド実行前に呼ばれる
@Afterメソッド実行後に呼ばれる
@Aroundメソッドは自分で実行し、前後に処理を記述する。
@AfterReturningメソッド実行後によばれる。例外スロー時は呼ばれない。
@AfterThrowing例外スロー時に呼ばれる。


実行結果の確認

ApplicationContext.xml、SpringSample.java、SpringSampleAspect.javaが揃ったところでSpringSampleのmainメソッドを実行してみる。SpringSampleのoutメソッドの前後で、それぞれ"before"、"after"が出力されているのがわかる。

出力結果
before
SpringAOP Test
after
■@Aroundでメソッドの前後に処理を入れる

@Beforeで前処理、@Afterで後処理を記述していたが、@Aroundを使えば1メソッドだけで実現できる。下記のように記述する。

SpringSampleAspect.java
// ①@Aroundアノテーション記述&メソッド宣言
@Around("execution(* aoptest.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{

    // -- 前処理 --
    System.out.println("around before");

    // ②メソッドの実行
    Object ret = joinPoint.proceed();

    // -- 後処理 --
    System.out.println("around after");
    
    // ③戻り値の返却
    return ret;
}
[説明]

①@Aroundアノテーション記述&メソッド宣言

@Around("execution(* aoptest.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{

メソッド宣言の前に@Aroundアノテーションを付与する。記述方法は@Beforeや@Afterと同じ。

メソッドの引数にはProceedingJoinPointクラスを指定する。ProceedingJoinPointはJoinPointのサブクラスで、実行するメソッドの情報が取得できるほか、メソッド自体の実行も行える。戻り値とthrows節については後述。


②メソッドの実行&③戻り値の返却

System.out.println("around before");

Object ret = joinPoint.proceed();

System.out.println("around after");

return ret;

ProceedingJoinPointクラスのproceed()を呼ぶ。

proceed()を実行することで、実際のメソッドが呼ばれる。前処理、後処理は、このproceed()の前後に記述する。

proceed()の戻り値には実際のメソッドの戻り値が格納されている。 aroundメソッドの戻り値には、この値をそのまま返却する(編集して返却も可能)。aroundメソッドの戻り値をvoidにすると、呼び出し元にはnullが返却される。逆に戻り値を返却しても実際のメソッド戻り値がvoidだった場合、返却値は無視される。

なおproceed()はThrowableをスローするため、明示的にcatchしないのであれば、aroundメソッドのthrows節を記述する。


実行結果の確認

メソッド実行後の出力結果は以下のとおり。

出力結果
around before
SpringAOP Test
around after
■@AfterReturning、@AfterThrowingでメソッドの正常終了、異常終了時で処理を分ける
@AfterReturning

@AfterReturningは、メソッドが正常終了した場合(例外がスローされなかった場合)に呼ばれる。

@AfterReturningのパラメータはpointcutreturningの2つあるので、key=value形式で記述する。

@AfterReturning(
    pointcut = "execution(* aoptest.*.*(..))",
    returning= "ret")
public void afterReturning(JoinPoint joinPoint, Object ret){
    System.out.println("afterReturning");
}

pointcutパラメータは対象メソッドの条件を記述する。@Beforeなどと同じようにexecution(...)形式で記述。

returningパラメータには実際のメソッド戻り値を格納する変数名を記述する。afterReturningメソッドの引数にこの名前で変数を記述しておくと、実際のメソッド戻り値を格納してくれる。


@AfterThrowing

@AfterThrowingは、異常終了した場合(例外がスローされた場合)に呼ばれる。

@AfterThrowing(
    pointcut = "execution(* aoptest.*.*(..))",
    throwing= "ex")
public void afterThrowing(JoinPoint joinPoint, Throwable ex){
    System.out.println("afterThrowing");
}

@AfterThrowingもパラメータは2つある。pointcutパラメータは@AfterReturningと同じ。throwingパラメータは実際のメソッドでスローされた例外を格納する変数名を記述する。afterThrowingメソッドの引数にこの名前で変数を記述しておくと、実際のメソッドでスローされた例外を格納してくれる。

@AfterReturning、@AfterThrowingともに利用頻度は少ないかもしれないが、例外発生時の処理を切り分けたい場合などに便利。

■@Before、@After、@Around、@AfterReturning、@AfterThrowingの実行順

@Before、@After、@Around、@AfterReturning、@AfterThrowingのすべてを実装して、各メソッドで標準出力してみると、以下の順に出力される。

出力結果
around before
before
Spring Test
around after
after
afterReturning

出力結果より、以下の順に実行されるのがわかる。

@Around(proceed呼び出し前)
@Before
メソッド実行(proceedで呼び出される)
@Around(proceed呼び出し後)
@After
@AfterReturning

例外が発生していないので、@AfterThrowingは呼ばれない。例外を発生させると以下のようになる。

出力結果
around before
before
Spring Test
after
afterThrowing

出力結果より、以下の順に実行されるのがわかる。

@Around(proceed呼び出し前)
@Before
メソッド実行(proceedで呼び出される)
@After
@AfterThrowing

@AfterReturningの代わりに、@AfterThrowingが呼ばれる。また@Aroundで例外を捕捉しない場合、@Around(proceed呼び出し後)は呼ばれない。

■実行対象メソッドの条件を共有する

実行対象メソッドの条件はexecuteに記述するが、下記サンプルのように"execution(* aoptest.*.*(..))"を何度も記述するとメンテナンス性が損なわれる。

package aoptest;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class SpringSampleAspect {
    
    @Before("execution(* aoptest.*.*(..))")
    public void before(){
        System.out.println("before");
    }
    
    @After("execution(* aoptest.*.*(..))")
    public void after(){
        System.out.println("after");
    }
    
    @AfterReturning("execution(* aoptest.*.*(..))")
    public void afterReturning(){
        System.out.println("afterReturning");
    }
    
    @AfterThrowing("execution(* aoptest.*.*(..))")
    public void afterThrowing(){
        System.out.println("afterThrowing");
    }
    
    @Around("execution(* aoptest.*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
        System.out.println("around before");
        Object ret = joinPoint.proceed();
        System.out.println("around after");
        return ret;
    }
}

@Pointcutアノテーションを使用することにより、条件を共有(再利用)できる。@Pointcutを使った修正版が以下である。

package aoptest;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class SpringSampleAspect {

    @Pointcut("execution(* aoptest.*.*(..))")
    public void myPointcut(){};
    
    @Before("aoptest.SpringSampleAspect.myPointcut()")
    public void before(){
        System.out.println("before");
    }
    
    @After("aoptest.SpringSampleAspect.myPointcut()")
    public void after(){
        System.out.println("after");
    }
    
    @AfterReturning("aoptest.SpringSampleAspect.myPointcut()")
    public void afterReturning(){
        System.out.println("afterReturning");
    }
    
    @AfterThrowing("aoptest.SpringSampleAspect.myPointcut()")
    public void afterThrowing(){
        System.out.println("afterThrowing");
    }
    
    @Around("aoptest.SpringSampleAspect.myPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
        System.out.println("around before");
        Object ret = joinPoint.proceed();
        System.out.println("around after");
        return ret;
    }
}

まず適当なメソッドを1つ用意し、@Pointcutアノテーションを付与する。パラメータには条件(execute)を記述する。@Pointcutが付与されたメソッドは何も実装は必要ない。これはメソッド名を単にIDのような形で利用するためである。

@Pointcut("execution(* aoptest.*.*(..))")
public void myPointcut(){};

次に@Before、@After、@Around、@AfterReturning、@AfterThrowingのパラメータに、@Pointcutが付いたメソッドのパッケージ付きクラス名.メソッド名()を指定する。 これで@Pointcutで指定した条件が共有できる。

@Before("aoptest.SpringSampleAspect.myPointcut()")
■JoinPoint、Pointcut、Adviceの用語説明

SpringAOPでは、JoinPointPointcutAdviceという言葉がよくでてくるが、それぞれ以下のような意味を持つ。

用語説明
Advice挿入する処理(ふるまい)。@Before、@Afterなどが付いたメソッドのこと。
Pointcut処理が挿入されるメソッドの条件。"execution(* aoptest.*.*(..))"などの対象メソッドを特定する表現のこと。
JoinPoint処理が挿入される候補のメソッドのこと。「public void out()」などのこと。

PointcutとJoinPointは似ているが、Pointcutが対象メソッドを絞り込むための表現なのに対し、JoinPointは個々のメソッドだと思えばよい。Aspectクラスの各メソッドでも引数のJoinPointクラスから具体的なメソッドの情報が取得できる。

またJoinPointの説明で候補のメソッドと書いたが、どうやらJoinPointはPointcutで指定可能な全てのメソッドのことを指すらしい。つまり全てのJoinPointの中から、絞り込むための条件がPointcutである。(なんともわかりづらいし、用語もイメージしにくい。。)

なお、対象メソッドが以下の場合、処理の挿入はできないようである。(JoinPointになれない)

- Springの入門本 -



TOP > Spring > SpringAOPの導入&サンプルプログラム

Tweet ̃Gg[͂ĂȃubN}[Nɒlj
技術空間