AOP(Aspect Oriented Programming)を利用する際は、そのAOP指定対象となるメソッドがfinalメソッドの場合はNullPointerExceptionが発生してしまうことがわかったので、今回はそのサンプルプログラムを共有する。
AOPについては、以下の記事に記載しているので参照のこと。
AOPの指定対象となるメソッドがfinalメソッドで、そのfinalメソッド内で何らかのDIオブジェクトを参照しようとすると、finalメソッドのサブクラスでDIオブジェクト(=proxy)を作成しようとして作成できないため、NullPointerExceptionが発生してしまう。その詳細については以下のサイトを参照のこと。
https://backpaper0.github.io/2018/02/22/spring_proxy.html
そのため、AOPの指定対象となるメソッドに、finalメソッドを含まないよう注意する必要がある。
前提条件
下記記事の実装、及び、user_dataテーブルへのデータの登録が完了していること
サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。

なお、上記の赤枠は、前提条件のプログラムから変更または追加したプログラムである。
まず、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未使用_確認画面に遷移」ボタンを押下

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

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

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

また、DemoInvocation.javaで、@Before/@Afterの各アノテーションの指定を、finalメソッドを含むサービスクラスに変更した場合の実行結果は以下の通りで、「final未使用_確認画面」は参照できるものの、「final使用_確認画面」が参照できないことが確認できる。
5) Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスし、「final未使用_確認画面に遷移」ボタンを押下

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

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

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

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

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





