LoginSignup
4
8

More than 3 years have passed since last update.

【Java・SpringBoot】Spring AOP実践

Posted at

AOP(AspectOrientedProgramming)とは?

  • AOPとは、アスペクト指向プログラミング
    • 各クラスで共通する処理を抜き出して、まとめて管理すること
  • 例えば、各クラスのメソッドでいちいち開始ログと終了ログを書くのは面倒。。。。
    • 書き忘れる、コードの修正量がとても多くなることも。。。
  • AOPを使えば、共通の処理をまとめて一ヶ所に書いておき、共通処理をどのクラスの、どのメソッドに適用するかを選択することができる!!嬉しい!
    • 共通処理の例:ログ出力、セキュリティ、トランザクション、例外処理、キャッシュリトライなどなど。
  • そうすることで、各クラスでは本来書くべきコードに集中でき、コードの可読性もUP👍

AOPの用語

  • Advice:AOPで実行する処理
  • Pointcut:処理を実行する場所(クラスやメソッド)
  • JoinPoint:処理を実行するタイミング
JoinPoint Advice実行タイミング
Before メソッドが実行される前に、Advice実行
After メソッドが実行された後に、Advice実行
AfterReturning メソッドが正常終了した場合だけ、Advice実行
Around メソッド実行の前後に、Advice実行
AfterThrowing メソッドが異常終了した場合のみ、Advice実行

AOPの内部の仕組み

  • AOPの仕組みはとっても簡単🌟
  • 例えばBeanとしてLoginControllerが登録されていてLoginControllerクラスのメソッドを呼び出す場合、
    • 1.DIコンテナに登録されているBeanのメソッドを呼び出す
    • 2.Proxyが自動生成され、Proxy経由でBeanのメソッドを呼ぶBeanのメソッドを直接呼び出さない!
    • 3.Beanのメソッドを呼び出す前後に、Advice(AOPの処理)を実行する

AOP実装の準備

  • 各コントローラークラスのメソッドが呼び出されるたびに、開始ログと終了ログを出力する
  • AOPを使うためには、pom.xmlのdependenciesタグ内に以下のコードを追加
pom.xml
<!-- Spring AOP -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
</dependency>
<!-- AspectJ -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Pointcutの指定方法

  • PointcutでどのクラスにAOPを適用するかを指定する方法
Pointcut 対象
execution 正規表現を使って任意のクラス、メソッドなどをAOPの対象に指定
bean DIコンテナに登録されているBean名でAOPの対象に指定
@annotation 指定したアノテーションが付いているメソッドがAOPの対象
@within 指定したアノテーションが付いているクラスの全てのメソッドがAOPの対象

Before・Afterの実装

コントローラークラスのログ出力用アスペクトを作成

@Aspectアノテーション

  • AOPのクラスには、@Aspectアノテーションを付ける
  • 同時にDIコンテナへBean定義をするので@Componentアノテーションも付ける

AOPの実装

  • AOP実行するメソッドには@Before@Afterアノテーションを付ける
  • executionでどのクラスのどのメソッドが実行されたときに、このメソッドが呼び出されるかを指定
  • executionの指定方法
    • "execution(<戻り値><パッケージ名>.<クラス名>.<メソッド名>(<引数>)”
    • 正規表現の使い方
      • *:任意の文字列
      • ..:任意(0以上)のパッケージ、メソッドの引数では、任意(0以上)の引数
      • +:クラス名の後に指定すると、指定クラスのサブクラス/実装クラスが含まれる
@Before("execution(*com.example.demo.login.controller.LoginController.getLogin(..))")
  • 上の例では、LoginControllerクラスのgetLoginメソッドをPointcutに指定
LogAspct.java
package com.example.demo.login.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogAspct {

    //AOP実装
    @Before("execution(* com.example.demo.login.controller.LoginController.getLogin(..))")
    public void startLog(JoinPoint jp){
        System.out.println("メソッド開始: " + jp.getSignature());
    }
    //AOP実装
    @After("execution(* com.example.demo.login.controller.LoginController.getLogin(..))")
    public void endLog(JoinPoint jp) {
        System.out.println("メソッド終了: " + jp.getSignature());
    }

}

ログイン画面にアクセスしてコンソールログを確認

  • http://localhost:8080/login
  • ログインコントローラーのgetLoginメソッドが呼ばれるたびに、コンソールにログをだすことができました〜〜^^
//コンソール画面
メソッド開始 String com.example.demo.login.controller.LoginController.getLogin(Model)
メソッド終了 String com.example.demo.login.controller.LoginController.getLogin(Model)

@Before@Afterの引数を変更

  • コントローラークラスのすべてのメソッドの開始・終了ログが出るように変更します

executionの指定方法

  • "execution(<戻り値><パッケージ名>.<クラス名>.<メソッド名>(<引数>)”
//クラス名の最後に"Controller"が付くクラスの全てのメソッドをAOPの対象にする
@Before("execution(* *..*.*Controller.*(..))")
@After("execution(* *..*.*Controller.*(..))")
  • 戻り値*:全ての戻り値を指定
  • パッケー名∗..∗:全てのパッケージが対象
  • クラス名∗Controller:末尾にControllerと付くクラスが対象
  • メソッド名*:全ての戻り値を指定
  • 引数..:全ての引数が対象

SpringBootを起動、ログイン画面とユーザー登録画面にアクセス

  • コントローラークラスのすべてのメソッドの開始・終了ログがコンソールに出ました^^
//コンソール画面
メソッド開始 String com.example.demo.login.controller.LoginController.getLogin(Model)
メソッド終了 String com.example.demo.login.controller.LoginController.getLogin(Model)
メソッド開始 String com.example.demo.login.controller.SignupController.getSignUp(SignupForm,Model)
メソッド終了 String com.example.demo.login.controller.SignupController.getSignUp(SignupForm,Model)
メソッド開始 String com.example.demo.login.controller.SignupController.postSignUp(SignupForm,BindingResult,Model)
メソッド終了 String com.example.demo.login.controller.SignupController.postSignUp(SignupForm,BindingResult,Model)

Aroundの実装

  • LogAspectクラスを以下のように修正します
  • Aroundを使う場合、アノテーションをを付けたメソッドの中で、AOP対象クラスのメソッドを直接proceedメソッドで実行する
    • →Aroundを使うと、メソッド実行の前後で任意の処理をすることができる!
  • メソッドを直接実行しているため、returnには実行結果の戻り値を指定
    • Around内でメソッドの実行を忘れないようにしよう
LogAspct.java
package com.example.demo.login.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogAspct {

    //AOP実装
    @Around("execution(* *..*.*Controller.*(..))")
    public Object startLog(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("メソッド開始: " + jp.getSignature());
        try {
            //メソッド実行
            Object result = jp.proceed();
            System.out.println("メソッド終了: " + jp.getSignature());
            return result;

        } catch (Exception e) {
            System.out.println("メソッド異常終了: " + jp.getSignature());
            e.printStackTrace();
            throw e;
        }
    }
}

ログイン画面にアクセスしてコンソールログを確認

  • http://localhost:8080/login
  • ユーザー登録画面にアクセス
  • ログインコントローラーのコントローラークラスのすべてのメソッドの開始・終了ログをコンソールに出すことができました〜〜^^
//コンソール画面
メソッド開始 String com.example.demo.login.controller.LoginController.getLogin(Model)
メソッド終了 String com.example.demo.login.controller.LoginController.getLogin(Model)
メソッド開始 String com.example.demo.login.controller.LoginController.postLogin(Model)
メソッド終了 String com.example.demo.login.controller.LoginController.postLogin(Model)
メソッド開始 String com.example.demo.login.controller.SignupController.getSignUp(SignupForm,Model)
メソッド終了 String com.example.demo.login.controller.SignupController.getSignUp(SignupForm,Model)
メソッド開始 String com.example.demo.login.controller.SignupController.postSignUp(SignupForm,BindingResult,Model)
メソッド終了 String com.example.demo.login.controller.SignupController.postSignUp(SignupForm,BindingResult,Model)

Pointcutその他の指定方法

  • 1.Bean名で指定
  • 2.アノテーションが付いているメソッドを指定
  • 3.アノテーションが付いているクラスの全メソッドを指定

1. Bean名でAOPの対象を指定する

  • beanDIに登録されているBean名でAOPの対象を指定
  • bean(<Bean名>)でbeanを指定
    • @Around("bean(*Controller)")
    • 上記では、名前の最後に"Controller"と付くBeanを対象にする
LogAspct.java
//中略
    @Around("bean(*Controller)")

    public Object startLog(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("メソッド開始: " + jp.getSignature());

        try {
        //メソッド実行
            Object result = jp.proceed();
            System.out.println("メソッド終了: " + jp.getSignature());
            return result;

        } catch (Exception e) {
            System.out.println("メソッド異常終了: " + jp.getSignature());
            e.printStackTrace();
            throw e;
        }
    }

2. 任意のアノテーションが付いているメソッドを指定する

  • @annotation指定したアノテーションが付いているメソッド全てを対象とする
    • パッケージ名を含めたクラス名を指定
LogAspct.java
//中略
    @Around("@annotation(org.springframework.web.bind.annotation.GetMapping)")

    public Object startLog(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("メソッド開始: " + jp.getSignature());

        try {
            Object result = jp.proceed();
            System.out.println("メソッド終了: " + jp.getSignature());
            return result;

        } catch (Exception e) {
            System.out.println("メソッド異常終了: " + jp.getSignature());
            e.printStackTrace();
            throw e;
        }
    }

3. アノテーションが付いているクラスの全メソッドを指定する

  • @within:指定したアノテーションが付いているクラスの全てのメソッドが対象
LogAspct.java
//中略
    @Around("@within(org.springframework.stereotype.Controller)")

    public Object startLog(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("メソッド開始: " + jp.getSignature());

        try {
            Object result = jp.proceed();
            System.out.println("メソッド終了: " + jp.getSignature());
            return result;

        } catch (Exception e) {
            System.out.println("メソッド異常終了: " + jp.getSignature());
            e.printStackTrace();
            throw e;
        }
    }
4
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
8