Spring Boot DI/AOP

Spring BootでAOPを利用してみた

Springフレームワークの基本として、AOP(Aspect Oriented Programming)という概念がある。

AOPは日本語で「アスペクト志向言語」といい、複数のプログラムに共通する処理を、アスペクト(Aspect)と呼ばれる別のプログラムに集約することができる。そのため、同一の処理を複数の箇所に分散して記載することを防ぐことができるため、ソフトウェアの保守性を高めることにつながる。

AOPは、Springを利用したJavaの開発現場でよく使われるが、「spring-boot-starter-aop」をあらかじめ追加しておく必要があることや、AOPのアノテーション付与方法・その内部で使われるPointcut式の指定方法についてきちんと理解しておくことが、開発をスムーズに進めるために必要であると思うため、防備録として残しておくことにした。

今回は、処理の前後にログ出力するサンプルプログラムを通して、AOPの具体的な実装方法について共有する。



前提条件

以下の記事のSpring BootのWEB画面用アプリが作成済であること。

IntelliJ IDEA上でGradleを使ってWeb画面のSpring Bootプロジェクトを作成してみたSpring Bootのプロジェクトを新規作成を「IntelliJ IDEA」のメニューから実施しようとしたところ、無料の「Commun...

やってみたこと

  1. aspectjweaverライブラリの追加
  2. ログ出力定義の追加
  3. アスペクトの追加と動作検証

aspectjweaverライブラリの追加

Spring BootでAOPによるアノテーションを利用するには、aspectjweaverライブラリが必要であるが、Spring Boot Webアプリケーション起動時に含まれている「spring-boot-starter-web」には入っていないので、「spring-boot-starter-aop」を追加する。「spring-boot-starter-web」を追加すれば、aspectjweaverライブラリも追加される。その手順は以下の通り。

1) build.gradleの「dependencies」タグ内に、「spring-boot-starter-aop」の依存関係を追加

plugins {
   id 'org.springframework.boot' version '2.1.7.RELEASE'
   id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
   mavenCentral()
}

dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
   implementation 'org.springframework.boot:spring-boot-starter-web'
   implementation 'org.springframework.boot:spring-boot-starter-aop'
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

2) build.gradleを編集すると、下図のように、右下にインポートが必要である旨が表示されるので、「変更をインポート」リンクを押下
spring-boot-aop-starterの追加
ちなみに、「自動インポートを使用可能にする」リンクを選択すると、この操作を行わなくても、自動的にこのインポート処理を実施してくれる。

3) 2)のインポート完了後、左側の「外部ライブラリー」を開くことで、下図のように、aspectjweaverライブラリの追加が確認できる。
aspectjweaverライブラリの確認

ログ出力定義の追加

今回はAOPを利用したログ出力を行うため、Spring BootによるWEB画面作成時に配置されている「application.properties」に、ログ出力定義を追加する。追加後のapplication.propertiesは以下の通り。

server.port = 8084
logging.level.root = INFO
logging.level.com.example.demo = DEBUG
logging.file = C:/work/logs/demo.log

上記のうち、下3行がログ出力定義となる。この定義により、demoアプリケーションについてはデバッグレベルのログが、「C:/work/logs/demo.log」というファイルに出力されることになる。

また、今回作成したプログラムの構成は以下の通り。なお、「DemoInvocation.java」は、アスペクトを利用したプログラムで、内容については後述する。
AOPを利用したプログラムの構成



アスペクトの追加と動作検証

AOPでは、複数のプログラムに共通する処理を、アスペクト(Aspect)と呼ばれる別のプログラムに集約するが、そのサンプルが次の「DemoInvocation.java」である。

package com.example.demo;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class DemoInvocation{
    //ログ出力のためのクラス
    private static Log log = LogFactory.getLog(DemoInvocation.class);

    /**
     * Beforeアノテーションにより、指定したメソッドの前に処理を追加する
     * Beforeアノテーションの引数には、Pointcut式 execution(戻り値 パッケージ.クラス.メソッド(引数))
     * を指定し、ここではControllerクラスの全メソッドの実行前にログ出力するようにしている
     *
     * @param jp 横断的な処理を挿入する場所
     */
    @Before("execution(* com.example.demo.*Controller.*(..))")
    public void startLog(JoinPoint jp){
        //開始ログを出力
        String signature = jp.getSignature().toString();
        log.debug("start startLog : " + signature);
    }

    /**
     * Afterアノテーションにより、指定したメソッドの後に処理を追加する
     * Afterアノテーションの引数には、Pointcut式を指定
     *
     * @param jp 横断的な処理を挿入する場所
     */
    @After("execution(* com.example.demo.*Controller.*(..))")
    public void endLog(JoinPoint jp){
        //終了ログを出力
        String signature = jp.getSignature().toString();
        log.debug("end endLog : " + signature);
    }

    /**
     * Aroundアノテーションにより、指定したメソッドの前後に処理を追加する
     * Aroundアノテーションの引数には、Pointcut式を指定
     *
     * @param jp 横断的な処理を挿入する場所
     * @return 指定したメソッドの戻り値
     */
    @Around("execution(* com.example.demo.*Controller.*(..))")
    public Object writeLog(ProceedingJoinPoint jp){
        //返却オブジェクトを定義
        Object returnObj = null;
        //指定したクラスの指定したメソッド名・戻り値を取得
        String signature = jp.getSignature().toString();
        //開始ログを出力
        log.debug("start writeLog : " + signature);
        try {
            //指定したクラスの指定したメソッドを実行
            returnObj = jp.proceed();
        }catch(Throwable t){
            log.error("error writeLog : ", t);
        }
        //終了ログを出力
        log.debug("end writeLog : " + signature);
        //指定したクラスの指定したメソッドの戻り値を返却
        //このように実行しないと、Controllerクラスの場合、次画面遷移が行えない
        return returnObj;
    }

}

上記プログラムでは、クラスの先頭に「@Aspect」アノテーションを、メソッドに「@Before」「@After」「@Around」アノテーションを付与することで、処理の前後に共通する処理を集約するAOPを実現している。また、AspectもDIするオブジェクトの生成対象とする必要があるため、クラスの先頭に「@Component」アノテーションも併せて付与している。

AOPの用語については、下記サイトが参考になる。
https://qiita.com/NagaokaKenichi/items/386af61b6866d60964e8

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-aop/demo



先ほどの「DemoInvocation.java」を作成後、Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスした結果は以下の通り。
AOPを利用したプログラムの実行結果

また、その際のコンソール・ログ出力結果は以下の通りで、下図赤枠部分で、画面遷移時のコントローラ「DemoController.java」呼び出しにより、「DemoInvocation.java」で記載したログが出力されていて、AOPが実現できているのが確認できる。

●コンソール
AOPを利用したプログラムのコンソール出力結果

●ログファイル(C:/work/logs/demo.log)
AOPを利用したプログラムのログ出力結果

要点まとめ

  • AOPを利用すると、複数のプログラムに共通する処理を集約することができる。
  • AOPを利用するには、aspectjweaverライブラリが必要。このライブラリは、「spring-boot-starter-aop」により追加される。
  • AOPは、クラスの先頭に「@Aspect」アノテーションを、メソッドに「@Before」「@After」「@Around」アノテーションを付与することで実現できる。さらに、AspectもDIするオブジェクトの生成対象とする必要があるため、「@Component」アノテーション等の付与も必要。
  • 「@Before」「@After」「@Around」アノテーション内部には、どのメソッドにAOPを適用するか指定するPointcut式を指定する。その指定方法は、「execution(戻り値 パッケージ.クラス.メソッド(引数))」となる。
  • ログ出力は、application.propertiesに定義を記載することで行える。