【Java・SpringBoot】Spring AOP実践

#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タグ内に以下のコードを追加

```html: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(<戻り値><パッケージ名>.<クラス名>.<メソッド名>(<引数>)”`**
    * 正規表現の使い方
        * *：任意の文字列
        * **..**：任意（０以上）のパッケージ、メソッドの引数では、任意（０以上）の引数
        * **+**：クラス名の後に指定すると、指定クラスのサブクラス／実装クラスが含まれる

```java
@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メソッドが呼ばれるたびに、コンソールにログをだす**ことができました〜〜^^

```java
//コンソール画面
メソッド開始： String com.example.demo.login.controller.LoginController.getLogin(Model)
メソッド終了： String com.example.demo.login.controller.LoginController.getLogin(Model)
```

#@Before・@Afterの引数を変更
* コントローラークラスの**すべてのメソッドの開始・終了ログ**が出るように変更します


###executionの指定方法
* **`"execution(<戻り値><パッケージ名>.<クラス名>.<メソッド名>(<引数>)”`**

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

#SpringBootを起動、ログイン画面とユーザー登録画面にアクセス
* コントローラークラスの**すべてのメソッドの開始・終了ログ**がコンソールに出ました^^

```java
//コンソール画面
メソッド開始： 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
* ユーザー登録画面にアクセス
* ログインコントローラーの**コントローラークラスのすべてのメソッドの開始・終了ログ**をコンソールに出すことができました〜〜^^

```java
//コンソール画面
メソッド開始： 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の対象を指定する
* **bean**：**DIに登録されている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;
        }
    }
```