Spring AOP(Aspect-Oriented Programming)

DIとともにSpring Frameworkの中核機能となるのが、AOPという技術です。ここでは、外部からクラスへ処理を挿入するAOPの仕組みと、基本的な使い方について説明します。

AOPとは

Spring Frameworkには、DI(Dependency Injection、依存性注入)とともに重要な基盤となるAOPという技術があります。

AOPはAspect-Oriented Programming(アスペクト指向プログラミング)の略です。Aspectとは一般に横断的関心事を意味します。つまり、AOPは問題を見る観点を基準にプログラミングする手法です。

  • 問題を解決するための中核的な関心事と、全体に適用される共通の関心事を分けてプログラミングすることで、共通モジュールを複数のコードへ容易に適用できます。
  • AOPで重要な概念は、横断的関心事の分離です。
  • OOPをよりOOPらしくしてくれます。

オブジェクト指向プログラムは、クラスを基準に作成されます。それぞれのクラスごとに、そのクラスに必要な機能をメソッドとして実装します。この方法は考え方としてはよくできていますが、逆に「クラスごとに完全に決まっていなければならない」ため、非常に面倒になることもあります。

たとえば、プログラムの開発中に動作状況を確認するため、あちこちにSystem.out.println文を書いて値を出力することは誰でもよく行う方法です。しかし、よく考えるとこれはかなり面倒な方法です。多数のクラスがある場合、各クラスの各メソッドにprintlnを書いていかなければなりません。また、プログラムが完成した後は、すべてのprintlnを削除しなければなりません。

このような「多数のクラスにまたがって共通して必要な処理」が横断的関心事です。もし複数のクラスのメソッドにprintln文を自動で挿入できる機能があれば、かなり便利ではないでしょうか。そして不要になったら自動で削除できれば、さらに便利です。これがAOPの考え方です。

DIが依存性、つまり値の注入であるなら、AOPは処理の注入と言ってもよいでしょう。外部からクラスの特定部分へ、あらかじめ用意しておいた処理を挿入したり削除したりすることをAOPで実現できます。

pom.xmlの準備
まず、プロジェクトにAOP関連ライブラリを追加します。pom.xmlを開き、<dependencies>タグの中に次の内容を追加します。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>4.3.10.RELEASE</version>
</dependency>

ここで追加するのはSpring AOPライブラリです。groupIdorg.springframeworkを指定し、artifactIdspring-aopを指定します。また、バージョンはSpring Framework 4.3.10に合わせて指定しています。Spring Frameworkのバージョンが異なる場合は、それに合わせてバージョンを調整してください。

AOPを利用するBeanクラスの作成

それではAOPを使ってみましょう。AOPは、特定の処理を外部からクラスへ挿入する機能です。そのためには、次のような準備が必要です。

  • AOPの対象となるクラス。一般的なBeanクラスを用意します。
  • AOPで挿入する処理を行うクラス。ここに挿入する処理を用意します。
  • AOPの設定情報。これはBean設定ファイル、または設定クラスを使用して用意します。

まず、AOPの対象となるクラスを用意します。今回はcom.devkuma.spring.aopというパッケージを用意し、その中に必要なクラスをまとめます。SampleAopBeanというクラスを次のように作成します。

package com.devkuma.spring.aop;
 
public class SampleAopBean {
    private String message;
 
    public SampleAopBean() {
        super();
    }
    public SampleAopBean(String message) {
        this.message = message;
    }
     
    public String getMessage() {
        return message;
    }
 
    public void setMessage(String message) {
        this.message = message;
    }
 
    public void printMessage() {
        System.out.println("message:[" + message + "]");
    }
}

これは以前作成したSampleBeanとほぼ同じです。メッセージを保存するmessageプロパティ、コンストラクタ、そしてprintMessageというメソッドを作成しました。このように、使用するBean自体は非常に単純なPOJOクラスである点がSpring Frameworkの特徴です。

MethodBeforeAdviceクラスの作成

続いて、SampleAopBeanへAOPで挿入する処理を用意します。これももちろんJavaクラスとして定義します。

com.devkuma.spring.aopパッケージにSampleMethodAdviceという名前でクラスを作成します。そして、次のようにコードを書きます。

package com.devkuma.spring.aop;
 
import java.lang.reflect.Method;
 
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
 
public class SampleMethodAdvice 
        implements MethodBeforeAdvice, AfterReturningAdvice {
 
    @Override
    public void before(Method method, Object[] args, 
            Object target) throws Throwable {
        System.out.println("*before: " + method.getName() + "[" + target + "]");
    }
 
    @Override
    public void afterReturning(Object returnValue, Method method, 
            Object[] args, Object target) throws Throwable {
        System.out.println("*after: " + method.getName() + "[" + target + "]");
    }
 
}

今回作成したSampleMethodAdviceは、2つのインターフェースを実装しています。これらのインターフェースは、処理の挿入に関するメソッドを追加します。それぞれを簡単にまとめると次のとおりです。

MethodBeforeAdvice
これは、メソッドが実行される前に処理を挿入するためのインターフェースです。beforeというメソッドを1つ持ち、次のように定義されています。

public void before (Method method, Object [] args, Object target)
    throws Throwable

methodには対象メソッド、argsにはその引数、targetには対象となるオブジェクト、つまりインスタンスが渡されます。これらの引数により、どのインスタンスのどのメソッドを呼び出す前にこの処理が実行されたのかを知ることができます。

AfterReturningAdvice
これは、メソッドの実行が終わり、呼び出し元へ戻るときに挿入する処理のインターフェースです。afterReturningというメソッドが用意されています。これは次のように定義されています。

public void afterReturning (Object returnValue, Method method,
    Object [] args, Object target) throws Throwable

メソッドの戻り値、メソッド、メソッドに渡された引数、対象インスタンスが引数として渡されます。戻り値以外は上記のbeforeと同じなので、ほぼ同じ感覚で処理できます。

ここでは、各メソッドとターゲットをSystem.out.printlnで出力しているだけです。AOPは処理の挿入と説明しましたが、どこにでも自由に挿入できるわけではありません。「このタイミングで挿入する」という仕組みが、あらかじめいくつか用意されています。

まずは、この2つのインターフェースを理解すれば、メソッド呼び出しの前後に処理を挿入できることがわかります。AOPの基本を理解するには十分です。

bean.xmlの作成

次に行うことは、必要なBeanの設定を用意することです。まずはBean設定ファイルを使用してみます。

resourcesフォルダに作成したbean.xmlを開き、次のように記述します。これで必要なライブラリがそろいます。

<?xml version="1.0" encoding="UTF-8"?>
<beans
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd">
     
    <!-- aop bean... -->
    <bean id="sampleAopBean" class="com.devkuma.spring.aop.SampleAopBean">
        <property name="message" value="this is AOP bean!" />
    </bean>
    
    <bean id="sampleMethodAdvice"
        class="com.tuyano.libro.aop.SampleMethodAdvice" />
 
    <bean id="proxyFactoryBean"
            class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="sampleAopBean"/>
        <property name="interceptorNames">
            <list>
                <value>sampleMethodAdvice</value>
            </list>
        </property>
    </bean>
             
</beans>

今回は合計3つのBeanを登録します。それぞれの内容は次のとおりです。

SampleAopBean
先ほど作成したBeanです。ここではid="sampleAopBean"という名前を指定して用意します。

SampleMethodAdvice
先ほど作成したAOP処理クラスです。これはid="sampleMethodAdvice"という名前で用意します。

ProxyFactoryBean
ここがポイントです。これはorg.springframework.aop.frameworkパッケージに含まれるSpring AOPのクラスです。このようにライブラリに含まれるクラスも、Bean設定ファイルによってインスタンスを自動生成できます。

このProxyFactoryBeanでは、<property>タグを使って2つのプロパティを追加しています。それぞれ次のとおりです。

target: AOPの対象となるBeanを指定します。ここではsampleAopBean、つまり<bean id="sampleAopBean">で用意したものを指定しています。

interceptorNames: AOPで挿入する処理Beanを指定します。複数指定できるように<list>というリストタグを指定し、その中の<value>タグでBean名を指定します。ここでは先ほど作成したsampleMethodAdviceを指定しています。

したがって、AOPの対象となるBean、AOP処理を実行するBean、そしてこれらの関係をプロパティとして設定したProxyFactoryBeanの3つが必要になります。

AOPを実行する

これでようやく準備が整いました。それでは実際にAOPを使用してみましょう。com.devkuma.spring.aopパッケージにAppクラスを作成し、次のようにソースコードを書きます。

package com.devkuma.spring.aop;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class App {
 
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("bean.xml");
         
        SampleAopBean bean1 = (SampleAopBean) app.getBean("sampleAopBean");
        bean1.printMessage();
 
        System.out.println("--------------------");
 
        SampleAopBean bean2 = (SampleAopBean) app.getBean("proxyFactoryBean");
        bean2.printMessage();
    }
 
}

実行すると、bean.xmlからSampleAopBeanを取得してprintMessageを実行しますが、よく見ると2回繰り返しています。

1つ目のSampleAopBeanは、getBean("sampleAopBean")でBeanを取得しています。これはこれまでどおりの方法です。そして2つ目は、getBean("proxyFactoryBean")でBeanを取得しています。これで取得されるBeanはProxyFactoryBeanのように思えますが、正しくSampleAopBeanへキャストされ、SampleBeanとして機能します。

これはProxyFactoryBeanの便利な機能です。このBeanは、targetプロパティに指定されたBeanとして取得できるのです。

では、取得したSampleAopBeanprintMessage呼び出しは、どのような出力になるのでしょうか。見ると次のようになっています。

message : [this is AOP bean!]
--------------------
* before : printMessage [com.devkuma.spring.aop.SampleAopBean@de3a06f]
message : [this is AOP bean!]
* after : printMessage [com.devkuma.spring.aop.SampleAopBean@de3a06f]

1つ目のSampleAopBeanは、単にprintMessageの出力だけです。しかし2つ目のSampleAopBeanでは、printMessageの実行前後に、SampleMethodAdviceクラスが提供するbeforeafterReturningの実行結果が挿入されていることがわかります。メソッドの実行前後に、別の処理が自動的に追加されているのです。

これがAOPの力です。getBeanで取得するBeanをProxyFactoryBeanにすると、このような形で自動処理を追加できます。不要になったら、getBeanの引数をSampleAopBeanへ戻せばよいのです。

アノテーションでAOP設定クラスを作成する

これで基本は理解できました。今度はbean.xmlをクラスへ書き換えてみましょう。Spring Frameworkでは、Bean設定ファイルを使用せず、設定用クラスで同じことができます。

それでは、com.devkuma.aopパッケージにSampleAopConfigというクラスを作成します。そして、次のようにソースコードを書きます。

package com.devkuma.spring.aop;
 
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class SampleAopConfig {
    private SampleAopBean sampleAopBean = 
        new SampleAopBean("this is message bean.");
    private SampleMethodAdvice sampleMethodAdvice = 
        new SampleMethodAdvice();
     
    @Bean
    SampleAopBean sampleAopBean() {
        return sampleAopBean;
    }
     
    @Bean
    SampleMethodAdvice sampleMethodAdvice() {
        return sampleMethodAdvice;
    }
     
    @Bean
    ProxyFactoryBean proxyFactoryBean() {
        ProxyFactoryBean bean = new ProxyFactoryBean();
        bean.setTarget(sampleAopBean);
        bean.setInterceptorNames("sampleMethodAdvice");
        return bean;
    }
     
}

作成したら、bean.xmlを使用していたものをSampleAopConfigクラスを使用するように、Appクラスのコードを修正します。次の文を変更します。

Appの修正

ApplicationContext app = new ClassPathXmlApplicationContext("bean.xml");

    ↓

ApplicationContext app = new AnnotationConfigApplicationContext (SampleAopConfig.class);

これでAppを実行すると、先ほどと同じように出力されます。bean.xmlに記述したものと同じBeanを、SampleAopConfigから取得できることがわかります。

ここでは、@Configurationアノテーションを付けてSampleAopConfigクラスを宣言し、Beanインスタンスを返すメソッドに@Beanアノテーションを付けて、Beanを取得できるようにしています。注目すべきなのは、SampleMethodAdviceインスタンスを作成しているsampleMethodAdviceメソッドです。インスタンス作成後に、次のように必要なプロパティを設定しています。

bean.setTarget(sampleAopBean);
bean.setInterceptorNames ("sampleMethodAdvice");

これは、setTargetsetInterceptorNamesが、先ほどbean.xmlで記述していた<property name="target"><property name="interceptorNames">に相当する処理であることを意味します。