Spring Boot DI/AOP

Spring BootでfinalメソッドにAOPを適用してみた

AOP(Aspect Oriented Programming)を利用する際は、そのAOP指定対象となるメソッドがfinalメソッドの場合はNullPointerExceptionが発生してしまうことがわかったので、今回はそのサンプルプログラムを共有する。

AOPについては、以下の記事に記載しているので参照のこと。

Spring BootでAOPを利用してみたSpringフレームワークの基本として、AOP(Aspect Oriented Programming)という概念がある。 AOP...

AOPの指定対象となるメソッドがfinalメソッドで、そのfinalメソッド内で何らかのDIオブジェクトを参照しようとすると、finalメソッドのサブクラスでDIオブジェクト(=proxy)を作成しようとして作成できないため、NullPointerExceptionが発生してしまう。その詳細については以下のサイトを参照のこと。
https://backpaper0.github.io/2018/02/22/spring_proxy.html

そのため、AOPの指定対象となるメソッドに、finalメソッドを含まないよう注意する必要がある。



前提条件

下記記事の実装、及び、user_dataテーブルへのデータの登録が完了していること

Spring BootでAjaxを利用してみたSpring BootのWEB画面上では、Ajax通信も行うことができる。今回は、jQueryを利用しない形で、Ajax通信を含むサンプ...

サンプルプログラムの作成

作成したサンプルプログラムの構成は以下の通り。
サンプルプログラムの構成
なお、上記の赤枠は、前提条件のプログラムから変更または追加したプログラムである。

まず、AOPを適用しているクラスの内容は以下の通りで、コントローラクラス/サービスクラスそれぞれでアノテーションの位置を切り替えられるようにしている。

package com.example.demo;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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 DemoInvocation {

    //ログ出力のためのクラス
    private Logger logger = LogManager.getLogger(DemoInvocation.class);

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

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

}



次に、コントローラクラスの内容は以下の通りで、途中サービスクラスを利用してユーザーデータを取得するようになっている。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class DemoController {

    /**
     * Demoサービスクラスへのアクセス
     */
    @Autowired
    private DemoService demoService;

    /**
     * 初期表示画面に遷移する
     * @return 初期表示画面へのパス
     */
    @GetMapping("/")
    public String index(){
        return "index";
    }

    /**
     * ユーザーデータを取得し、final未使用_確認用画面に遷移する
     * @param model Modelオブジェクト
     * @return final未使用_確認用画面へのパス
     */
    @GetMapping("/index_nofinal")
    public String index_nofinal(Model model){
        String str = demoService.getUserData();
        model.addAttribute("userData", str);
        return "index_nofinal";
    }

    /**
     * ユーザーデータを取得し、final使用_確認用画面に遷移する
     * @param model Modelオブジェクト
     * @return final使用_確認用画面へのパス
     */
    @GetMapping("/index_final")
    public String index_final(Model model){
        String str = demoService.getUserDataByFinal();
        model.addAttribute("userDataByFinal", str);
        return "index_final";
    }
}



また、サービスクラスの内容は以下の通りで、ユーザーデータを取得するメソッドを、finalを使う場合と使わない場合それぞれで定義している。

package com.example.demo;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DemoService {

    /**
     * ユーザーデータテーブル(user_data)へアクセスするリポジトリ
     */
    @Autowired
    private UserDataRepository repository;

    /**
     * ユーザーデータを取得し、そのJSON文字列を返す
     * @return ユーザーデータ(JSON)
     */
    public String getUserData(){
        UserData userData = repository.findUserDataById(1L);
        return getJson(userData);
    }

    /**
     * finalメソッドにより、ユーザーデータを取得し、そのJSON文字列を返す
     * @return ユーザーデータ(JSON)
     */
    public final String getUserDataByFinal(){
        UserData userData = repository.findUserDataById(1L);
        return getJson(userData);
    }

    /**
     * 引数のUserDataオブジェクトをJSON文字列に変換する
     * @param userData UserDataオブジェクト
     * @return 変換後JSON文字列
     */
    private String getJson(UserData userData){
        String retVal = null;
        ObjectMapper objectMapper = new ObjectMapper();
        try{
            retVal = objectMapper.writeValueAsString(userData);
        } catch (JsonProcessingException e) {
            System.err.println(e);
        }
        return retVal;
    }
}



HTMLファイルの内容は以下の通りで、初期表示画面「index.html」、final未使用_確認画面「index_nofinal.html」、final使用_確認画面「index_final.html」をそれぞれ定義している。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index page</title>
</head>
<body>
    <form method="get" th:action="@{/index_nofinal}">
        <input type="submit" value="final未使用_確認用画面に遷移" />
    </form>
    <br/><br/>
    <form method="get" th:action="@{/index_final}">
        <input type="submit" value="final使用_確認用画面に遷移" />
    </form>
</body>
</html>
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index page</title>
</head>
<body>
    finalをもたないメソッドから取得した値:<br/>
    <span th:text="${userData}"></span>
</body>
</html>
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index page</title>
</head>
<body>
    finalをもつメソッドから取得した値:<br/>
    <span th:text="${userDataByFinal}"></span>
</body>
</html>



また、build.gradleの内容は以下の通り。

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'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	//lombokを利用するための設定
	compileOnly 'org.projectlombok:lombok:1.18.10'
	annotationProcessor 'org.projectlombok:lombok:1.18.10'
	//oracleを利用するための設定
	compile files('lib/ojdbc6.jar')
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	//AOPを利用するための設定
	compile group: 'org.aspectj', name: 'aspectjweaver', version: '1.6.10'
}

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



サンプルプログラムの実行結果

DemoInvocation.javaで、サンプルプログラム記載通りに、finalメソッドを含まないコントローラクラスがAOP対象の場合の実行結果は以下の通りで、「final使用_確認画面」が問題なく参照できることが確認できる。

1) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスし、「final未使用_確認画面に遷移」ボタンを押下
実行結果_1

2) ユーザーデータが取得され、JSON形式で表示されることが確認できる
実行結果_2

3) 1)と同じ画面上で、「final使用_確認画面に遷移」ボタンを押下
実行結果_3

4) 2)と同様に、ユーザーデータが取得され、JSON形式で表示されることが確認できる
実行結果_4

また、DemoInvocation.javaで、@Before/@Afterの各アノテーションの指定を、finalメソッドを含むサービスクラスに変更した場合の実行結果は以下の通りで、「final未使用_確認画面」は参照できるものの、「final使用_確認画面」が参照できないことが確認できる。

5) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスし、「final未使用_確認画面に遷移」ボタンを押下
実行結果_5

6) ユーザーデータが取得され、JSON形式で表示されることが確認できる
実行結果_6

7) 5)と同じ画面上で、「final使用_確認画面に遷移」ボタンを押下
実行結果_7

8) 正常な画面遷移が行えず、エラーが表示されることが確認できる
実行結果_8_1

また、このときのコンソールログの内容は以下の通りで、getUserDataByFinalメソッド内の「repository.findUserDataById(1L)」を実行する際に、NullPointerExceptionが発生していることが確認できる。
実行結果_8_2

要点まとめ

  • AOPの指定対象となるメソッドがfinalメソッドの場合、NullPointerExceptionが発生する場合があるので、AOPの指定対象となるメソッドには、finalを含まないようにする必要がある。