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を適用しているクラスの内容は以下の通りで、コントローラクラス/サービスクラスそれぞれでアノテーションの位置を切り替えられるようにしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 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); } } |
次に、コントローラクラスの内容は以下の通りで、途中サービスクラスを利用してユーザーデータを取得するようになっている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 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を使う場合と使わない場合それぞれで定義している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | 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」をそれぞれ定義している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <!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> |
1 2 3 4 5 6 7 8 9 10 11 | <!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> |
1 2 3 4 5 6 7 8 9 10 11 | <!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の内容は以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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を含まないようにする必要がある。