Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( 番外編 )( Thymeleaf 3 へのバージョンアップを試してみる )
概要
記事一覧はこちらです。
Spring Boot 1.4 Release Notes に Thymeleaf 3 の記述がありましたので、Thymeleaf 3 へのバージョンアップを試してみます。
今回は試してみるだけでコミットはしません。
参照したサイト・書籍
Spring Boot Reference Guide - 74.9 Use Thymeleaf 3
http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-use-thymeleaf-3Thymeleaf 3 ten-minute migration guide
http://www.thymeleaf.org/doc/articles/thymeleaf3migration.htmlSpring Boot 1.4でThymeleaf 3.0系を使うための設定方法
http://qiita.com/kazuki43zoo/items/da64a68b9805e512cdc9Spring Boot 1.4+Thymeleaf 3.0でSpELコンパイラを有効にしてパフォーマンスを向上させよう!!
http://qiita.com/kazuki43zoo/items/f367845d50589281ed46#_reference-bb5b2b46ee255b058fed
目次
- Thymeleaf の定義がある BOM ファイルを探してみる
- build.gradle を変更する
- clean タスク → Rebuild Project → build タスクを実行してみる
- bootRun で起動してみる
- 動作確認
- Thymeleaf 2 でエラーになったことを Thymeleaf 3 ではエラーにならないか試してみる
- 次回は。。。
手順
Thymeleaf の定義がある BOM を探してみる
最初に Spring IO Platform の Athens-SR5 の BOM を見てみます。https://repo1.maven.org/maven2/io/spring/platform/platform-bom/Athens-SR5/platform-bom-Athens-SR5.pom を見ると、その中には “Thymeleaf” に関する定義はありませんでした。
次に Athens-SR5 の BOM の先頭に記述されている spring-boot-starter-parent の BOM を見ます。https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-parent/1.4.6.RELEASE/spring-boot-starter-parent-1.4.6.RELEASE.pom を見ると、この中にも “Thymeleaf” に関する定義はありませんでした。
次に spring-boot-starter-parent の BOM の先頭に記述されている spring-boot-dependencies の BOM を見ます。https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/1.4.6.RELEASE/spring-boot-dependencies-1.4.6.RELEASE.pom を見ると、以下の “Thymeleaf” に関する定義が記述されていました。
<properties> .......... <thymeleaf.version>2.1.5.RELEASE</thymeleaf.version> <thymeleaf-extras-springsecurity4.version>2.1.3.RELEASE</thymeleaf-extras-springsecurity4.version> <thymeleaf-extras-conditionalcomments.version>2.1.2.RELEASE</thymeleaf-extras-conditionalcomments.version> <thymeleaf-layout-dialect.version>1.4.0</thymeleaf-layout-dialect.version> <thymeleaf-extras-data-attribute.version>1.3</thymeleaf-extras-data-attribute.version> <thymeleaf-extras-java8time.version>2.1.0.RELEASE</thymeleaf-extras-java8time.version> .......... </properties> .......... <dependencyManagement> <dependencies> .......... <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <version>1.4.6.RELEASE</version> </dependency> .......... <dependency> <groupId>com.github.mxab.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-data-attribute</artifactId> <version>${thymeleaf-extras-data-attribute.version}</version> </dependency> .......... <dependency> <groupId>nz.net.ultraq.thymeleaf</groupId> <artifactId>thymeleaf-layout-dialect</artifactId> <version>${thymeleaf-layout-dialect.version}</version> </dependency> .......... <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf</artifactId> <version>${thymeleaf.version}</version> </dependency> <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring4</artifactId> <version>${thymeleaf.version}</version> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-conditionalcomments</artifactId> <version>${thymeleaf-extras-conditionalcomments.version}</version> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-java8time</artifactId> <version>${thymeleaf-extras-java8time.version}</version> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> <version>${thymeleaf-extras-springsecurity4.version}</version> </dependency> .......... </dependencies> </dependencyManagement>
以下の値を設定すればよさそうです。
- thymeleaf.version
- thymeleaf-extras-springsecurity4.version
- thymeleaf-extras-conditionalcomments.version
- thymeleaf-layout-dialect.version
- thymeleaf-extras-data-attribute.version
- thymeleaf-extras-java8time.version
build.gradle を変更する
jcenter, mavenCentral で Thymeleaf の各ライブラリのバージョンを確認した後、build.gradle の dependencyManagement の記述を以下のように変更します。
dependencyManagement { imports { mavenBom("io.spring.platform:platform-bom:Athens-SR5") { bomProperty 'guava.version', '21.0' bomProperty 'thymeleaf.version', '3.0.6.RELEASE' bomProperty 'thymeleaf-extras-springsecurity4.version', '3.0.2.RELEASE' bomProperty 'thymeleaf-layout-dialect.version', '2.2.1' bomProperty 'thymeleaf-extras-java8time.version', '3.0.0.RELEASE' } } }
- 以下の記述を追加します。
bomProperty 'thymeleaf.version', '3.0.6.RELEASE'
bomProperty 'thymeleaf-extras-springsecurity4.version', '3.0.2.RELEASE'
bomProperty 'thymeleaf-layout-dialect.version', '2.2.1'
bomProperty 'thymeleaf-extras-java8time.version', '3.0.0.RELEASE'
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
Project Tool Window の External Libraries を見ると、指定したバージョンのライブラリがダウンロードされて依存関係にセットされていることが確認できます。
clean タスク → Rebuild Project → build タスクを実行してみる
clean タスク → Rebuild Project → build タスクを実行してみると、何のエラーも出ず “BUILD SUCCESSFUL” の文字が出力されました。
Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’ with Coverage」を選択してテストを実行しても、テストが全て成功することが確認できます。
bootRun で起動してみる
bootRun で Tomcat を起動してみるとエラーは出ずに “Started Application in …” のログが出力されました。
が、途中で 2017-05-10 01:36:50.476 WARN 9576 --- [ restartedMain] org.thymeleaf.templatemode.TemplateMode : [THYMELEAF][restartedMain] Template Mode 'HTML5' is deprecated. Using Template Mode 'HTML' instead.
というログも出力されていました。
Spring Boot Reference Guide の 74.9 Use Thymeleaf 3 に warning message を避けたければ spring.thymeleaf.mode
に HTML
を設定するよう記述されていましたので、この設定を反映します。
application.properties を以下のように変更します。
spring.freemarker.cache=true spring.freemarker.settings.number_format=computer spring.freemarker.charset=UTF-8 spring.freemarker.enabled=false spring.freemarker.prefer-file-system-access=false spring.thymeleaf.mode=HTML valueshelper.classpath.prefix=
spring.thymeleaf.mode=HTML
を追加します。
ただし IntelliJ IDEA で spring.thymeleaf.mode
に設定可能な値の候補一覧を表示させると HTML
は出てきません。
設定した後も HTML
は赤字で表示されます。
設定はされているはずなので、bootRun で Tomcat を起動してみると、今度は WARN ログは出ずに “Started Application in …” のログが出力されました。
動作確認
jar ファイルを作成して Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その26 )( jar ファイルを作成して動作確認する2 ) に書いた手順で動作確認します。
結果は以下の通りです。
- 動作確認は全て正常に動作しました。ksbysample-webapp-lending.log にもエラーは出力されていません。
- 画面は以前と全く変わらず表示されました。
- Thymeleaf 3 は 2 よりパフォーマンスアップしているという話でしたが、このアプリの画面だと全然分かりませんでした。。。
Thymeleaf 2 でエラーになったことを Thymeleaf 3 ではエラーにならないか試してみる
Thymeleaf を使用する上で手間だと思っていたのは以下の2点です。
- HTML5 では閉じタグは必須ではないが、閉じタグを付けないとエラーになる。例えば
<meta charset="UTF-8">
は末尾を/>
とスラッシュを付けて<meta charset="UTF-8"/>
としないとダメ。<meta charset="UTF-8">
の場合、org.xml.sax.SAXParseException: 要素タイプ"meta"は、対応する終了タグ"</meta>"で終了する必要があります。
のログが出力される。 - 属性は必ず
="..."
を付ける必要がある。例えば xxx 属性を追加する場合、<div class="content-wrapper" xxx>
はエラーになり、<div class="content-wrapper" xxx="">
のようにする必要がある。<div class="content-wrapper" xxx>
の場合、org.xml.sax.SAXParseException: 要素タイプ"div"に関連付けられている属性名"xxx"の後には、' = '文字が必要です。
のログが出力される。
src/main/resources/templates/login.html で試してみます。以下の点を変更します。
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>ログイン画面</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"> <!-- Bootstrap 3.3.4 --> <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css"> <!-- Font Awesome Icons --> <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <!-- Ionicons --> <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css"> <!-- Theme style --> <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css"> <!-- AdminLTE Skins. Choose a skin from the css/skins folder instead of downloading all of them to reduce the load. --> <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css"> .......... </head> <!-- ADD THE CLASS layout-top-nav TO REMOVE THE SIDEBAR. --> <body class="skin-blue layout-top-nav"> <div class="wrapper"> <!-- Full Width Column --> <div class="content-wrapper"> <div class="container"> <!-- Main content --> <section class="content">
<head>...</head>
内の meta, link タグの末尾の/
を削除します。<div class="content-wrapper">
に xxx 属性を追加して<div class="content-wrapper" xxx>
にします。
bootRun で Tomcat を起動した後、http://localhost:8080/ にアクセスすると、ログイン画面が表示されました。
また他に気付いたこととして <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
は <html>
でも画面は表示されるのですが(これは Thymeleaf 2 でも表示されます)、<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
にしないと IntelliJ IDEA で th:
属性の補完が効かなくなりますので、これはこのままにします。
次回は。。。
Thymeleaf 3 ten-minute migration guide にいろいろ面白そうな機能が書かれているので、試してみます。
履歴
2017/05/10
初版発行。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その26 )( jar ファイルを作成して動作確認する2 )
概要
記事一覧はこちらです。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その25 )( jar ファイルを作成して動作確認する ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- jar ファイルを作成して動作確認する。前回の1回だけでは終わらなかったので、その続きです。
参照したサイト・書籍
目次
- 動作確認3
- サービスから削除する
- feature/128-issue -> 1.4.x へ Pull Request、1.4.x へマージ、feature/128-issue ブランチを削除
- 次回は。。。
手順
動作確認3
3回目。最初から動作確認し直します。
動作確認前に DB のデータを以下の状態にします。
- user_info, user_role テーブルのデータは開発時のままにします。
- lending_app, lending_book, library_forsearch テーブルのデータはクリアします。
サービス画面を開きます。サービス一覧から「ksbysample-webapp-lending」を選択し、「サービスの開始」リンクをクリックしてサービスを開始します。
以下の手順で動作確認します ( 画面キャプチャは省略します )。
- ブラウザを起動して http://localhost:8080/ にアクセスしてログイン画面を表示します。tanaka.taro@sample.com / taro でログインします。
- 検索対象図書館登録画面が表示されます。"東京都" で検索した後、一覧表示されている図書館から「国立国会図書館東京本館」を選択します。
- ログアウトします。
- ログイン画面に戻るので suzuki.hanako@test.co.jp / hanako でログインします。
- 貸出希望書籍 CSV ファイルアップロード画面が表示されます。以下の内容が記述された CSV ファイルをアップロードします。
“ISBN”,“書名”
“978-4-7741-6366-6”,“GitHub実践入門”
“978-4-7741-5377-3”,“JUnit実践入門”
“978-4-7973-8014-9”,“Java最強リファレンス”
“978-4-7973-4778-4”,“アジャイルソフトウェア開発の奥義”
“978-4-87311-704-1”,“Javaによる関数型プログラミング” - 「貸出状況を確認しました」のメールが送信されるので、メールに記述されている URL にアクセスします。
- 貸出申請画面が表示されます。3冊程「申請する」を選択して申請します。
- ログアウトします。
- 「貸出申請がありました」のメールが送信されるので、メールに記述されている URL にアクセスします。ログイン画面が表示されるので、tanaka.taro@sample.com / taro でログインします。
- 貸出承認画面が表示されます。「承認」あるいは「却下」を選択して確定させます。
- ログアウトします。
- 「貸出申請が承認・却下されました」のメールが送信されるので、メールに記述されている URL にアクセスします。ログイン画面が表示されるので、suzuki.hanako@test.co.jp / hanako でログインします。
- 貸出申請結果確認画面が表示されるので内容を確認します。
動作確認は特に問題ありませんでした。
smtp4dev を終了します。
サービス画面で「ksbysample-webapp-lending」サービスを停止します。
サービスから削除する
サービスを削除します。管理者モードで起動しているコマンドプロンプトから以下のコマンドを実行します。
> nssm.exe remove ksbysample-webapp-lending
「Remove the service?」のダイアログが表示されますので、「はい」ボタンをクリックします。サービスが削除されると「Service “ksbysample-webapp-lending” removed successfully!」のダイアログが表示されますので「OK」ボタンをクリックします。
feature/128-issue -> 1.4.x へ Pull Request、1.4.x へマージ、feature/128-issue ブランチを削除
feature/128-issue -> 1.4.x へ Pull Request、1.4.x へマージ、feature/128-issue ブランチを削除します。
1.4.x -> master へもマージします。
次回は。。。
前回手こずったので、まだ何かあるかもしれないと思いましたが、他には何も問題は起きませんでした。
この後は番外編として Thymeleaf 3 へのバージョンアップを試してみた後、感想を書いて完了させる予定です。
ソースコード
履歴
2017/05/06
初版発行。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その25 )( jar ファイルを作成して動作確認する )
概要
記事一覧はこちらです。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その24 )( Spring Boot を 1.4.5 → 1.4.6 にバージョンアップする ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- jar ファイルを作成して動作確認する。
参照したサイト・書籍
Windowsのバッチファイル中で日付をファイル名に使用する
http://www.atmarkit.co.jp/ait/articles/0405/01/news002.htmlFailure to find creator property with Lombok and an unwrapping mixin involved
https://github.com/FasterXML/jackson-databind/issues/1239FreeMarker - Remove comma from milliseconds
http://stackoverflow.com/questions/21577407/freemarker-remove-comma-from-millisecondsFREEMARKER Manual - Built-ins for numbers - c (when used with numerical value)
http://freemarker.org/docs/ref_builtins_number.html#ref_builtin_cFREEMARKER Manual - setting
http://freemarker.org/docs/ref_directive_setting.htmlSpring Boot Reference Guide - Appendix E. The executable jar format
https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.htmlWhy did Spring Boot 1.4 change its jar layout to locate application classes under BOOT-INF?
http://stackoverflow.com/questions/40292816/why-did-spring-boot-1-4-change-its-jar-layout-to-locate-application-classes-unde
目次
- jar ファイルを作成、配置する
- サービスに登録する
- 動作確認
- CSVファイルアップロード後に「貸出状況を確認しました」のメールが送信されない原因を調査する
- メールの中の lendingAppId の数値がカンマ区切りされる理由とは?
- 動作確認2
- 「貸出状況を確認しました」のメールに記載された URL にアクセスするとエラーになる原因を調査する
- 次回は。。。
手順
jar ファイルを作成、配置する
clean タスク実行 → Rebuild Project 実行 → build タスク実行します。
C:\project-springboot\ksbysample-webapp-lending\build\libs の下に ksbysample-webapp-lending-1.4.6-RELEASE.jar が作成されますので、C:\webapps\ksbysample-webapp-lending\lib の下に配置します。
webapps/ksbysample-webapp-lending/bat/webapp_startup.bat を リンク先の内容 に変更した後、C:\webapps\ksbysample-webapp-lending\bat の下にコピーします。
サービスに登録する
コマンドプロンプトを「管理者として実行…」モードで起動した後、以下のコマンドを実行します。
> cd /d C:\webapps\ksbysample-webapp-lending\nssm
> nssm.exe install ksbysample-webapp-lending
「NSSM service installer」画面が表示されます。以下の画像の値を入力した後、「Install service」ボタンをクリックします。サービスの登録に成功すると「Service “ksbysample-webapp-lending” installed successfully!」のダイアログが表示されますので「OK」ボタンをクリックします。
- 「Path」に
C:\webapps\ksbysample-webapp-lending\bat\webapp_startup.bat
を入力します。 - 「Startup directory」に
C:\webapps\ksbysample-webapp-lending\bat
を入力します。
動作確認
動作確認前に DB のデータを以下の状態にします。
- user_info, user_role テーブルのデータは開発時のままにします。
- lending_app, lending_book, library_forsearch テーブルのデータはクリアします。
サービス画面を開きます。サービス一覧から「ksbysample-webapp-lending」を選択し、「サービスの開始」リンクをクリックしてサービスを開始します。
C:\webapps\ksbysample-webapp-lending\logs の下の ksbysample-webapp-lending.log をエディタで開き、最後に “Started Application in …” のログが出力されていることを確認します。
メールを受信するので smtp4dev を起動します。
以下の手順で動作確認します ( 画面キャプチャは省略します )。
- ブラウザを起動して http://localhost:8080/ にアクセスしてログイン画面を表示します。tanaka.taro@sample.com / taro でログインします。
- 検索対象図書館登録画面が表示されます。"東京都" で検索した後、一覧表示されている図書館から「国立国会図書館東京本館」を選択します。
- ログアウトします。
- ログイン画面に戻るので suzuki.hanako@test.co.jp / hanako でログインします。
- 貸出希望書籍 CSV ファイルアップロード画面が表示されます。以下の内容が記述された CSV ファイルをアップロードします。
“ISBN”,“書名”
“978-4-7741-6366-6”,“GitHub実践入門”
“978-4-7741-5377-3”,“JUnit実践入門”
“978-4-7973-8014-9”,“Java最強リファレンス”
“978-4-7973-4778-4”,“アジャイルソフトウェア開発の奥義”
“978-4-87311-704-1”,“Javaによる関数型プログラミング” - 「貸出状況を確認しました」のメールが送信される。。。はずでしたが、メールが送信されませんでした。ksbysample-webapp-lending.log を見ると、以下のログが出力されていました。原因を調査します。
org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException: Listener method 'public void ksbysample.webapp.lending.listener.rabbitmq.InquiringStatusOfBookQueueListener.receiveMessage(org.springframework.amqp.core.Message) throws javax.mail.MessagingException' threw exception
Caused by: java.lang.IllegalArgumentException: Not enough variable values available to expand 'systemid'
at ksbysample.webapp.lending.service.calilapi.CalilApiService.lambda$getForEntityWithRetry$0(CalilApiService.java:137)
一旦サービス画面で「ksbysample-webapp-lending」サービスを停止します。
CSVファイルアップロード後に「貸出状況を確認しました」のメールが送信されない原因を調査する
デバッガで処理を何度か追った結果、原因は以下の内容であることが分かりました。
- ksbysample.webapp.lending.service.calilapi.CalilApiService#check 内で RestTemplate#getForEntity をリトライするために Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その13 )( RestTemplate で WebAPI を呼び出している処理に spring-retry でリトライ処理を入れる ) において CalilApiService#getForEntityWithRetry メソッドを定義して、それに置き換えた。
- RestTemplate#getForEntity には
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)
とpublic <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables)
の2つが存在する。 - CalilApiService#check 内で呼び出していた RestTemplate#getForEntity は
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables)
だったが、CalilApiService#getForEntityWithRetry メソッドはpublic <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)
を呼び出す方しか定義していなかったので、処理が適切に行われずエラーが発生した。
src/main/java/ksbysample/webapp/lending/service/calilapi/CalilApiService.java に private <T> ResponseEntity<T> getForEntityWithRetry(RestTemplate restTemplate, String url, Class<T> responseType, Map<String, ?> uriVariables)
を追加します。リンク先の内容 に変更します。
変更後に IntelliJ IDEA から bootRun を起動して CSVファイルアップロードすると、今度は Failed to evaluate Jackson deserialization for type [[simple type, class ksbysample.webapp.lending.service.calilapi.response.CheckApiResponse]]: com.fasterxml.jackson.databind.JsonMappingException: Invalid definition for property "" (of type Lksbysample/webapp/lending/service/calilapi/response/Libkey;): Could not find creator property with name '' (known Creator properties: [name, value])
というログが出力されました。
Web で調べると GitHub の Issue で Failure to find creator property with Lombok and an unwrapping mixin involved を見つけました。この Issue によると、
- lombok の
@AllArgsConstructor
を付けたクラスをネストしていて、そのクラスを Jackson で使用していると、この問題が発生する。 - Project の root 直下に lombok.config というファイルを作成し、
lombok.anyConstructor.suppressConstructorProperties = true
を記述すれば解決するとのこと。
ということで root 直下に lombok.config というファイルを新規作成し、リンク先の内容 を記述します。
再度 IntelliJ IDEA から bootRun を起動して CSVファイルアップロードすると、今度は「貸出状況を確認しました」のメールが送信されました。が、メールを見ると http://localhost:8080/lendingapp?lendingAppId=1,739
と lendingAppId の数値がカンマ区切りされていました。
メールの中の lendingAppId の数値がカンマ区切りされる理由とは?
stackoverflow の FreeMarker - Remove comma from milliseconds という QA を見つけました。FreeMarker のテンプレートの中で ${number}
と書いていて、数値→文字列に変換される場合、デフォルトでは3桁毎に “,” で区切られるようです。またこれを回避するには ${number?c}
のように末尾に ?c
を付ければよいとのこと。
ただしデフォルトで “,” 区切りになるのは避けたいので、もう少し調べてみたところ、FREEMARKER Manual - setting に number_format
の設定を computer
にすればデフォルトで ?c
を付けた状態になると記述がありました。この設定を追加することにします。
src/main/resources/application.properties を リンク先のその1の内容 に変更します。
動作確認すると「貸出状況を確認しました」のメールで lendingAppId の数値がカンマ区切りされなくなりました。
clean タスク実行 → Rebuild Project 実行 → build タスク実行し、作成された ksbysample-webapp-lending-1.4.6-RELEASE.jar を C:\project-springboot\ksbysample-webapp-lending\build\libs の下にコピーします。
動作確認2
最初から動作確認し直します。
動作確認前に DB のデータを以下の状態にします。
- user_info, user_role テーブルのデータは開発時のままにします。
- lending_app, lending_book, library_forsearch テーブルのデータはクリアします。
サービス画面を開きます。サービス一覧から「ksbysample-webapp-lending」を選択し、「サービスの開始」リンクをクリックしてサービスを開始します。
以下の手順で動作確認します ( 画面キャプチャは省略します )。
- ブラウザを起動して http://localhost:8080/ にアクセスしてログイン画面を表示します。tanaka.taro@sample.com / taro でログインします。
- 検索対象図書館登録画面が表示されます。"東京都" で検索した後、一覧表示されている図書館から「国立国会図書館東京本館」を選択します。
- ログアウトします。
- ログイン画面に戻るので suzuki.hanako@test.co.jp / hanako でログインします。
- 貸出希望書籍 CSV ファイルアップロード画面が表示されます。以下の内容が記述された CSV ファイルをアップロードします。
“ISBN”,“書名”
“978-4-7741-6366-6”,“GitHub実践入門”
“978-4-7741-5377-3”,“JUnit実践入門”
“978-4-7973-8014-9”,“Java最強リファレンス”
“978-4-7973-4778-4”,“アジャイルソフトウェア開発の奥義”
“978-4-87311-704-1”,“Javaによる関数型プログラミング” - 「貸出状況を確認しました」のメールが送信されるので、メールに記述されている URL にアクセスします。。。とエラー画面が表示されました。ksbysample-webapp-lending.log を見ると、
java.lang.Exception: null
のログが出力されていました。原因を調査します。
「貸出状況を確認しました」のメールに記載された URL にアクセスするとエラーになる原因を調査する
いろいろ試した結果、以下のことが分かりました。
- jar ファイルを作成してサービスから起動すると発生するが、IntelliJ IDEA から bootRun で起動すると発生しない。
- webapp_startup.bat 内で
-Dspring.profiles.active=product
→-Dspring.profiles.active=develop
に変更して起動してからhttp://localhost:8080/lendingapp?lendingAppId=...
の URL にアクセスするとorg.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "@vh.getText('LendingAppStatusValues', lendingappForm.lendingApp.status)" (lendingapp/lendingapp:78)
というログが出力される。
ksbysample.webapp.lending.values.ValuesHelper クラスで問題が発生しているようです。
src/main/java/ksbysample/webapp/lending/values/ValuesHelper.java に System.out.println("★★★...
でログを出力するように変更して、valuesObjList に読み込まれているデータを出力してみます。
@Component("vh") public class ValuesHelper { private final Map<String, String> valuesObjList; private ValuesHelper() throws IOException { ClassLoader loader = Thread.currentThread().getContextClassLoader(); System.out.println("★★★ this.getClass().getPackage().getName() = " + this.getClass().getPackage().getName()); valuesObjList = ClassPath.from(loader).getTopLevelClassesRecursive(this.getClass().getPackage().getName()) .stream() .filter(classInfo -> { try { Class<?> clazz = Class.forName(classInfo.getName()); return !clazz.equals(Values.class) && Values.class.isAssignableFrom(clazz); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } }) .collect(Collectors.toMap(ClassPath.ClassInfo::getSimpleName, ClassPath.ClassInfo::getName)); valuesObjList.entrySet().stream() .forEach(e -> { System.out.println(String.format("★★★ key = %s, value = %s", e.getKey(), e.getValue())); }); }
IntelliJ IDEA から bootRun で起動した時には、以下のように出力されました。
★★★ this.getClass().getPackage().getName() = ksbysample.webapp.lending.values ★★★ key = LendingBookLendingAppFlgValues, value = ksbysample.webapp.lending.values.lendingbook.LendingBookLendingAppFlgValues ★★★ key = LendingBookApprovalResultValues, value = ksbysample.webapp.lending.values.lendingbook.LendingBookApprovalResultValues ★★★ key = LendingAppStatusValues, value = ksbysample.webapp.lending.values.lendingapp.LendingAppStatusValues
jar ファイルを作成してサービスから起動した時には、以下のように出力されました。
★★★ this.getClass().getPackage().getName() = ksbysample.webapp.lending.values
サービスから起動した時には valuesObjList に何も読み込まれていないようです。
Spring Boot 1.3.5 を使用していた頃に戻して同じことを試してみると、IntelliJ IDEA から bootRun で起動した時と同じように出力されました。
★★★ this.getClass().getPackage().getName() = ksbysample.webapp.lending.values ★★★ key = LendingBookLendingAppFlgValues, value = ksbysample.webapp.lending.values.lendingbook.LendingBookLendingAppFlgValues ★★★ key = LendingBookApprovalResultValues, value = ksbysample.webapp.lending.values.lendingbook.LendingBookApprovalResultValues ★★★ key = LendingAppStatusValues, value = ksbysample.webapp.lending.values.lendingapp.LendingAppStatusValues
ksbysample-webapp-lending-1.1.0-RELEASE.jar と ksbysample-webapp-lending-1.4.6-RELEASE.jar を解凍してディレクトリ構成を比較してみると、ksbysample-webapp-lending-1.1.0-RELEASE.jar の時は解凍したディレクトリの直下に ksbysample ディレクトリが出来ていたのに対し、ksbysample-webapp-lending-1.4.6-RELEASE.jar では BOOT-INF\classes の下に ksbysample ディレクトリが出来ていました。jar ファイル内のディレクトリ構成が変更されていました。
Spring Boot 1.4 Release Notes の Executable jar layout に確かにレイアウトが変更されたことの記述がありました。
for (final ClassPath.ClassInfo info : ClassPath.from(loader).getTopLevelClasses()) { ... }
のコードを追加して取得されるクラス一覧を出力してみると、BOOT-INF.classes.
の文字列が追加されています。
@Component("vh") public class ValuesHelper { private final Map<String, String> valuesObjList; private ValuesHelper() throws IOException { ClassLoader loader = Thread.currentThread().getContextClassLoader(); for (final ClassPath.ClassInfo info : ClassPath.from(loader).getTopLevelClasses()) { System.out.println("★★★ " + info.getName()); } ..........
★★★ BOOT-INF.classes.ksbysample.webapp.lending.values.lendingapp.LendingAppStatusValues
分かったことをまとめると、以下のようになります。
- jar ファイルから起動された場合、
com.google.common.reflect.ClassPath#getTopLevelClassesRecursive
でクラス一覧を取得するにはパッケージの前にBOOT-INF.classes.
を付ける必要がある。com.google.common.reflect.ClassPath#getTopLevelClassesRecursive
ではなく Spring で特定パッケージ配下のクラス一覧を取得する方法がありそうな気がするが、全く分かりませんでした。。。 - IntelliJ IDEA から bootRun で起動する場合には、
BOOT-INF.classes.
を付ける必要はなし。 Class.forName(...)
を呼び出す場合にはBOOT-INF.classes.
は常に不要である。
設定ファイルに設定項目を追加し jar ファイルから起動された時だけ BOOT-INF.classes.
が付くようにして、その設定項目を
ksbysample.webapp.lending.values.ValuesHelper
のコンストラクタの処理で使用するようにします。
application.properties を リンク先のその2の内容 に変更します。
application-product.properties を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/values/ValuesHelper.java を リンク先の内容 に変更します。
clean タスク実行 → Rebuild Project 実行 → build タスク実行し、作成された ksbysample-webapp-lending-1.4.6-RELEASE.jar を C:\project-springboot\ksbysample-webapp-lending\build\libs の下にコピーします。
次回は。。。
長くなったので、一旦ここで終了し次へ続きます。
ソースコード
webapp_startup.bat
@echo on setlocal set JAVA_HOME=C:\Java\jdk1.8.0_131 set PATH=%JAVA_HOME%\bin;%PATH% set WEBAPP_HOME=C:\webapps\ksbysample-webapp-lending set WEBAPP_JAR=ksbysample-webapp-lending-1.4.6-RELEASE.jar cd /d %WEBAPP_HOME% java -server ^ -Xms1024m -Xmx1024m ^ -XX:MaxMetaspaceSize=384m ^ -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled ^ -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=75 ^ -XX:+ScavengeBeforeFullGC -XX:+CMSScavengeBeforeRemark ^ -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps ^ -Xloggc:%WEBAPP_HOME%/logs/gc.log ^ -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M ^ -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=%WEBAPP_HOME%/logs/%date:~0,4%%date:~5,2%%date:~8,2%.hprof ^ -XX:ErrorFile=%WEBAPP_HOME%/logs/hs_err_pid_%p.log ^ -Dsun.net.inetaddr.ttl=100 ^ -Dcom.sun.management.jmxremote ^ -Dcom.sun.management.jmxremote.port=7900 ^ -Dcom.sun.management.jmxremote.ssl=false ^ -Dcom.sun.management.jmxremote.authenticate=false ^ -Dspring.profiles.active=product ^ -jar lib\%WEBAPP_JAR%
set JAVA_HOME=C:\Java\jdk1.8.0_121
→set JAVA_HOME=C:\Java\jdk1.8.0_131
に変更します。set WEBAPP_JAR=ksbysample-webapp-lending-1.4.5-RELEASE.jar
→set WEBAPP_JAR=ksbysample-webapp-lending-1.4.6-RELEASE.jar
に変更します。- Windows の場合、date コマンドがないため今の記述だとファイル名が日付にならないことに気付いたので、
-XX:HeapDumpPath=%WEBAPP_HOME%/logs/%date:~0,4%%date:~5,2%%date:~8,2%.hprof
に変更します。
CalilApiService.java
private <T> ResponseEntity<T> getForEntityWithRetry(RestTemplate restTemplate, String url , Class<T> responseType, Object... uriVariables) { ResponseEntity<T> response = this.simpleRetryTemplate.execute(context -> { if (context.getRetryCount() > 0) { logger.info("★★★ リトライ回数 = " + context.getRetryCount()); } ResponseEntity<T> innerResponse = restTemplate.getForEntity(url, responseType, uriVariables); return innerResponse; }); return response; } private <T> ResponseEntity<T> getForEntityWithRetry(RestTemplate restTemplate, String url , Class<T> responseType, Map<String, ?> uriVariables) { ResponseEntity<T> response = this.simpleRetryTemplate.execute(context -> { if (context.getRetryCount() > 0) { logger.info("★★★ リトライ回数 = " + context.getRetryCount()); } ResponseEntity<T> innerResponse = restTemplate.getForEntity(url, responseType, uriVariables); return innerResponse; }); return response; }
private <T> ResponseEntity<T> getForEntityWithRetry(RestTemplate restTemplate, String url, Class<T> responseType, Map<String, ?> uriVariables)
メソッドを追加します。
lombok.config
lombok.anyConstructor.suppressConstructorProperties = true
application.properties
■その1
.......... spring.freemarker.cache=true spring.freemarker.settings.number_format=computer spring.freemarker.charset=UTF-8 spring.freemarker.enabled=false spring.freemarker.prefer-file-system-access=false
spring.freemarker.settings.number_format=computer
を追加します。
■その2
.......... spring.freemarker.cache=true spring.freemarker.settings.number_format=computer spring.freemarker.charset=UTF-8 spring.freemarker.enabled=false spring.freemarker.prefer-file-system-access=false valueshelper.classpath.prefix=
valueshelper.classpath.prefix=
を追加します。
application-product.properties
.......... spring.redis.sentinel.master=mymaster spring.redis.sentinel.nodes=localhost:6381,localhost:6382,localhost:6383 valueshelper.classpath.prefix=BOOT-INF.classes.
valueshelper.classpath.prefix=BOOT-INF.classes.
を追加します。
ValuesHelper.java
@Component("vh") public class ValuesHelper { private final Map<String, String> valuesObjList; private ValuesHelper(@Value("${valueshelper.classpath.prefix:}") String classpathPrefix) throws IOException { ClassLoader loader = Thread.currentThread().getContextClassLoader(); valuesObjList = ClassPath.from(loader) .getTopLevelClassesRecursive(classpathPrefix + this.getClass().getPackage().getName()) .stream() .filter(classInfo -> { try { Class<?> clazz = Class.forName(classInfo.getName().replace(classpathPrefix, "")); return !clazz.equals(Values.class) && Values.class.isAssignableFrom(clazz); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } }) .collect(Collectors.toMap(classInfo -> classInfo.getSimpleName() , classInfo -> classInfo.getName().replace(classpathPrefix, ""))); }
- コンストラクタの引数に
@Value("${valueshelper.classpath.prefix:}") String classpathPrefix
を追加します。 .getTopLevelClassesRecursive(...)
の引数をthis.getClass().getPackage().getName()
→classpathPrefix + this.getClass().getPackage().getName()
に変更します。Class.forName(classInfo.getName())
→Class.forName(classInfo.getName().replace(classpathPrefix, ""))
に変更します。.collect(Collectors.toMap(ClassPath.ClassInfo::getSimpleName, ClassPath.ClassInfo::getName));
→.collect(Collectors.toMap(classInfo -> classInfo.getSimpleName(), classInfo -> classInfo.getName().replace(classpathPrefix, "")));
に変更します。
履歴
2017/05/06
初版発行。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その24 )( Spring Boot を 1.4.5 → 1.4.6 にバージョンアップする )
概要
記事一覧はこちらです。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その23 )( Spring Security 関連で修正した方がよい箇所を見直す ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- Spring Boot の 1.4.6 がリリースされているのでバージョンアップします。
- Error Prone 以外のライブラリも最新バージョンにバージョンアップします。
参照したサイト・書籍
目次
手順
build.gradle を変更する
build.gradle を リンク先の内容 に変更します。
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
Spring Boot が 1.4.6.RELEASE にバージョンアップされていることを確認します。
clean タスク → Rebuild Project → build タスクを実行する
clean タスク → Rebuild Project → build タスクを実行します。
無事 “BUILD SUCCESSFUL” のメッセージが表示されています。
Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」を実行すると、こちらも全てのテストが成功しました。
ソースコード
build.gradle
group 'ksbysample' version '1.4.6-RELEASE' buildscript { ext { springBootVersion = '1.4.6.RELEASE' } repositories { jcenter() maven { url "http://repo.spring.io/repo/" } maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") // for Error Prone ( http://errorprone.info/ ) classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.10") // for Grgit classpath("org.ajoberstar:grgit:1.9.2") // Gradle Download Task classpath("de.undercouch:gradle-download-task:3.2.0") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'org.springframework.boot' apply plugin: 'de.undercouch.download' apply plugin: 'groovy' apply plugin: 'net.ltgt.errorprone' apply plugin: 'checkstyle' apply plugin: 'findbugs' sourceCompatibility = 1.8 targetCompatibility = 1.8 task wrapper(type: Wrapper) { gradleVersion = '2.13' } [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing,-path'] compileJava.options.compilerArgs += ['-Xep:RemoveUnusedImports:WARN'] // for Doma 2 // JavaクラスとSQLファイルの出力先ディレクトリを同じにする processResources.destinationDir = compileJava.destinationDir // コンパイルより前にSQLファイルを出力先ディレクトリにコピーするために依存関係を逆転する compileJava.dependsOn processResources idea { module { inheritOutputDirs = false outputDir = file("$buildDir/classes/main/") } } configurations { // for Doma 2 domaGenRuntime } checkstyle { configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml") toolVersion = '7.7' sourceSets = [project.sourceSets.main] } findbugs { toolVersion = '3.0.1' sourceSets = [project.sourceSets.main] ignoreFailures = true effort = "max" excludeFilter = file("${rootProject.projectDir}/config/findbugs/findbugs-exclude.xml") } tasks.withType(FindBugs) { reports { xml.enabled = false html.enabled = true } } repositories { jcenter() } dependencyManagement { imports { mavenBom("io.spring.platform:platform-bom:Athens-SR5") { bomProperty 'guava.version', '21.0' } } } bootRepackage { mainClass = 'ksbysample.webapp.lending.Application' excludeDevtools = true } dependencies { def jdbcDriver = "org.postgresql:postgresql:42.0.0" def spockVersion = "1.1-groovy-2.4-rc-4" def domaVersion = "2.16.0" def lombokVersion = "1.16.16" def errorproneVersion = "2.0.15" // dependency-management-plugin によりバージョン番号が自動で設定されるもの // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照 compile("org.springframework.boot:spring-boot-starter-web") compile("org.springframework.boot:spring-boot-starter-thymeleaf") compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity4") compile("org.thymeleaf.extras:thymeleaf-extras-java8time") compile("org.springframework.boot:spring-boot-starter-data-jpa") compile("org.springframework.boot:spring-boot-starter-freemarker") compile("org.springframework.boot:spring-boot-starter-mail") compile("org.springframework.boot:spring-boot-starter-security") compile("org.springframework.boot:spring-boot-starter-data-redis") compile("org.springframework.boot:spring-boot-starter-amqp") compile("org.springframework.boot:spring-boot-devtools") compile("org.springframework.session:spring-session") compile("org.springframework.retry:spring-retry") compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") compile("com.google.guava:guava") compile("org.apache.commons:commons-lang3") compile("org.codehaus.janino:janino") testCompile("org.springframework.boot:spring-boot-starter-test") testCompile("org.springframework.security:spring-security-test") testCompile("org.yaml:snakeyaml") testCompile("org.mockito:mockito-core") // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの runtime("${jdbcDriver}") compile("com.integralblue:log4jdbc-spring-boot-starter:1.0.1") compile("org.simpleframework:simple-xml:2.7.1") compile("com.univocity:univocity-parsers:2.4.1") testCompile("org.dbunit:dbunit:2.5.3") testCompile("com.icegreen:greenmail:1.5.4") testCompile("org.assertj:assertj-core:3.6.2") testCompile("com.jayway.jsonpath:json-path:2.2.0") testCompile("org.spockframework:spock-core:${spockVersion}") { exclude module: "groovy-all" } testCompile("org.spockframework:spock-spring:${spockVersion}") { exclude module: "groovy-all" } testCompile("com.google.code.findbugs:jsr305:3.0.2") // for lombok compileOnly("org.projectlombok:lombok:${lombokVersion}") testCompileOnly("org.projectlombok:lombok:${lombokVersion}") // for Doma compile("org.seasar.doma:doma:${domaVersion}") domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}") domaGenRuntime("${jdbcDriver}") // for Error Prone ( http://errorprone.info/ ) errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}") compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}") }
version '1.4.5-RELEASE'
→version '1.4.6-RELEASE'
へ変更します。- buildscript の以下の点を変更します。
springBootVersion = '1.4.5.RELEASE'
→springBootVersion = '1.4.6.RELEASE'
に変更します。classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.9")
→classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.10")
に変更します。classpath("org.ajoberstar:grgit:1.9.0")
→classpath("org.ajoberstar:grgit:1.9.2")
に変更します。
- checkstyle の以下の点を変更します。
toolVersion = '7.6.1'
→toolVersion = '7.7'
に変更します。
- dependencyManagement の以下の点を変更します。
mavenBom("io.spring.platform:platform-bom:Athens-SR4")
→mavenBom("io.spring.platform:platform-bom:Athens-SR5")
に変更します。bomProperty 'commons-lang3.version', '3.5'
を削除します。
- dependencies の以下の点を変更します。
def jdbcDriver = "org.postgresql:postgresql:9.4.1212"
→def jdbcDriver = "org.postgresql:postgresql:42.0.0"
に変更します。compile("com.integralblue:log4jdbc-spring-boot-starter:1.0.0")
→compile("com.integralblue:log4jdbc-spring-boot-starter:1.0.1")
に変更します。compile("com.univocity:univocity-parsers:2.3.1")
→compile("com.univocity:univocity-parsers:2.4.1")
に変更します。testCompile("com.icegreen:greenmail:1.5.3")
→testCompile("com.icegreen:greenmail:1.5.4")
に変更します。
履歴
2017/05/03
初版発行。
IntelliJ IDEA を 2017.1 → 2017.1.2 へバージョンアップ
IntelliJ IDEA を 2017.1 → 2017.1.2 へバージョンアップする
IntelliJ IDEA の 2017.1.2 がリリースされたのでバージョンアップします。
- IntelliJ IDEA 2017.1.2 Release Notes
https://confluence.jetbrains.com/display/IDEADEV/IntelliJ+IDEA+2017.1.2+Release+Notes
※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。
IntelliJ IDEA のメインメニューから「Help」-「Check for Updates…」を選択します。
「Platform and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。
※前回 2017.1.1 へバージョンアップしたはずなのに、Current version に 2017.1 と表示されています。原因が分かりませんが 2017.1.1 へバージョンアップできていなかったようです。なんでだろう。。。
Plugin の update も表示されたので、チェックしたまま「Update and Restart」ボタンをクリックします。
Patch がダウンロードされて IntelliJ IDEA が再起動します。
メイン画面が表示されます。IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。
Gradle projects View のツリーを見ると「Tasks」の下に「other」しかない状態になっているので、左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
「other」以外に「build」等も表示された状態に戻ります。
前回 2017.1 → 2017.1.1 へバージョンアップしたと書いた時に「Indexing…」は表示されず、Gradle Tool Window のツリーの表示は other グループしかない初期の状態に戻りませんでした、と書きましたが、今回は表示されました。2017.1.1 へバージョンアップできていなかったようなので、やっぱり表示される&初期状態に戻る仕様のままのようです。
IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2017.1.2 へバージョンアップされていることを確認します。
clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して “BUILD SUCCESSFUL” のメッセージが表示されることを確認します。
Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’ with Coverage」を選択し、テストが全て成功することを確認します。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その23 )( Spring Security 関連で修正した方がよい箇所を見直す )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
- ログイン画面で何も入力せずに「ログイン」ボタンをクリックすると “500 Internal Server Error” になる問題を解消します。
- 他に Spring Security 関連で修正した方がよい箇所を見直すつもりでいましたが、上記の1点以外は修正しません。いろいろ Web の記事等を見ましたが、よく分かりませんでした。動作に支障はなさそうなので、このまま行くことにします。
参照したサイト・書籍
Spring Security 4.1 主な変更点
http://qiita.com/kazuki43zoo/items/e925f134e65d7595aa3cSpring Security 4.2 主な変更点
http://qiita.com/kazuki43zoo/items/ef6cc6d05d68a8cb7a0e
目次
手順
ログイン画面で何も入力せずに「ログイン」ボタンをクリックすると “500 Internal Server Error” になる原因を調査する
bootRun で Tomcat を起動してからログイン画面を表示し、何も入力せずに「ログイン」ボタンをクリックするとエラー画面が表示されることに気づきました。Spring Boot を 1.2 → 1.3 にバージョンアップした時からこの現象が出ていたようです。
以下のログが出力されていました。
org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "session['SPRING_SECURITY_LAST_EXCEPTION'].authentication.principal" (login:63)
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'authentication' cannot be found on object of type 'org.springframework.security.authentication.BadCredentialsException' - maybe not public?
ログインエラー時に入力された ID を入力されたままにしようとして session['SPRING_SECURITY_LAST_EXCEPTION'].authentication.principal
にアクセスしていたのですが、.authentication
にアクセスできなくなっていることが原因のようです。
Spring Boot の 1.2 を使用していた時に戻して、その時にはなぜエラーが出ないのかを確認してみたところ、
- ログインエラー時に
session['SPRING_SECURITY_LAST_EXCEPTION']
にセットされていたのはorg.springframework.security.authentication.BadCredentialsException
のインスタンスである。 BadCredentialsException
はorg.springframework.security.core.AuthenticationException
の継承クラスである。org.springframework.security.core.AuthenticationException
にはgetAuthentication
メソッドが実装されていた。ただし@Deprecated
アノテーションが付いていました。。。 Thymeleaf からアクセスしているだけだと気づきませんでした。
という訳で、この時にはまだ session['SPRING_SECURITY_LAST_EXCEPTION'].authentication
にアクセスできていたからでした。Spring Boot の 1.4 に戻してみると、org.springframework.security.core.AuthenticationException
から getAuthentication
メソッドが見事になくなっています。
ログインエラー時に ID を入力されたままにする
ログイン画面で何も入力せずに「ログイン」ボタンをクリックした時にエラーになる件は、単純に session['SPRING_SECURITY_LAST_EXCEPTION'].authentication.principal
を削除してアクセスしなければ回避できますが、それでは入力された ID がログインエラー時に消えてしまいます。
ログインエラー時でも、入力した ID が入力されたままになるようにしてみます。
Spring Security 4.1 主な変更点 の記事を見ると、Spring Security 4.1 で AuthenticationFailureHandler にて遷移先にフォワードする実装クラスが追加されたと記載されていました。
IntelliJ IDEA で AuthenticationFailureHandler インターフェースの実装クラスを調べて見ると以下の図のようになっていました。ForwardAuthenticationFailureHandler クラスが遷移先にフォワードする実装クラスのようです。これを使用してみます。
src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.java を リンク先の内容 に変更します。
src/main/resources/templates/login.html を リンク先の内容 に変更します。
ForwardAuthenticationFailureHandler クラスに変更するとログインエラー発生時にリダイレクトされなくなり、HTTPステータスコードが 302 ではなく 200 が返るようになります。src/test/java/ksbysample/webapp/lending/web/LoginControllerTest.java の以下のテストメソッドを変更します。
- 変更するテストメソッド
- パスワードの有効期限が切れているユーザならばログインはエラーになる()
- 存在しないユーザ名とパスワードを入力すればログインはエラーになる()
- 存在するユーザ名でもパスワードが正しくなければログインはエラーになる()
- アカウントの有効期限が切れているユーザならばログインはエラーになる()
- ログインを5回失敗すればアカウントはロックされる()
- enabledが0のユーザならばログインはエラーになる()
- 変更内容
.andExpect(status().isFound())
→.andExpect(status().isOk())
へ変更する。.andExpect(redirectedUrl("/"))
を削除する。.andExpect(request().sessionAttribute("SPRING_SECURITY_LAST_EXCEPTION", isA(DisabledException.class)));
→.andExpect(request().attribute("SPRING_SECURITY_LAST_EXCEPTION", isA(DisabledException.class)));
へ変更する。
- 変更するテストメソッド
bootRun で Tomcat を起動して動作を確認してみます。
ログイン画面を表示して何も入力せずに「ログイン」ボタンをクリックをすると、エラー画面は表示されずログイン画面上にエラーメッセージが表示されました。
ログイン画面を表示し直して ID に “test” とだけ入力して「ログイン」ボタンをクリックをすると、ログイン画面上にエラーメッセージが表示され、入力した “test” は入力された状態で表示されました。ここまでは期待通りの動作です。
また login.html で入力された ID を表示するために
${#httpServletRequest.getParameter('id')}
で出力するようにしましたが、これで XSS対策が問題ないのか試してみます。http://localhost:8080/?id=<script>alert('hello');</script>
でアクセスすると HTTPステータスコードの 400 が返ってきました。ログを見ると
java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986 at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:471)
というメッセージが出力されています。Spring Security ではなく org.apache.coyote.http11.Http11InputBuffer クラスが出力しています。org.apache.coyote.http11.Http11InputBuffer クラスが何のコンポーネントに含まれているのか調べてみると、org.apache.tomcat.embed:tomcat-embed-core:8.5.11 に入っているクラスでした。
RFC 7230, 3986 は以下のリンクに日本語訳があります。
HTTPステータスコードの 400 が返ってきたのは、
<
等が URI には直接使用できる文字ではないので、リクエストで送信されてくると Tomcat がエラーにするためのようです。起動していた Tomcat を停止します。
ソースコード
WebSecurityConfig.java
@Override protected void configure(HttpSecurity http) throws Exception { .......... http.formLogin() .loginPage("/") .loginProcessingUrl("/login") .defaultSuccessUrl(WebSecurityConfig.DEFAULT_SUCCESS_URL) .usernameParameter("id") .passwordParameter("password") .successHandler(new RoleAwareAuthenticationSuccessHandler()) .failureHandler(new ForwardAuthenticationFailureHandler("/")) .permitAll() ..........
.failureUrl("/")
を削除します。.failureHandler(new ForwardAuthenticationFailureHandler("/"))
を追加します。
login.html
<form role="form" action="#" th:action="@{/login}" method="post" id="login-form" autocomplete="off"> <div class="form-group" th:if="${#httpServletRequest.getAttribute('SPRING_SECURITY_LAST_EXCEPTION')} != null"> <p class="form-control-static text-danger" th:text="${#httpServletRequest.getAttribute('SPRING_SECURITY_LAST_EXCEPTION').message}"></p> </div> <div class="form-group"> <label for="id" class="sr-only">ID</label> <input type="text" name="id" id="id" class="form-control" placeholder="ID を入力して下さい" th:value="${#httpServletRequest.getParameter('id')} != null ? ${#httpServletRequest.getParameter('id')} : ''"/> </div> <div class="form-group"> <label for="password" class="sr-only">Password</label> <input type="password" name="password" id="password" class="form-control" placeholder="Password を入力して下さい"/> </div> <div class="form-group text-center"> <div class="checkbox"> <label><input type="checkbox" name="remember-me" id="remember-me" value="true"/>次回から自動的にログインする</label> </div> </div> <button id="btn-login" class="btn btn-primary btn-block">ログイン</button> </form>
- 遷移先にフォワードする実装クラス(ForwardAuthenticationFailureHandler)に変更した場合、ログインエラー時の例外はセッションではなくリクエストスコープにセットされますので、
session['SPRING_SECURITY_LAST_EXCEPTION']
→#httpServletRequest.getAttribute('SPRING_SECURITY_LAST_EXCEPTION')
に変更します。 - ID を表示する th:value の記述を
${session['SPRING_SECURITY_LAST_EXCEPTION']} != null ? ${session['SPRING_SECURITY_LAST_EXCEPTION'].authentication.principal} : ''
→${#httpServletRequest.getParameter('id')} != null ? ${#httpServletRequest.getParameter('id')} : ''
へ変更します。
履歴
2017/05/03
初版発行。
Git のコメントを "#" で始められるようにする
Git のコミットを GitHub の Issue に紐付けるために、コメントを “#128 …” のように “#” + Issue番号 から始めるようにしているのですが、Git for Windows の 2.11.0 の頃から git rebase -i
で rebase しようとすると Aborting commit due to empty commit message.
というメッセージが表示されて rebase できなくなっていました。
何か対応方法があるのか stackoverflow を見ていたところ、以下の QA が見つかりました。
Start a git commit message with a hashmark (#)
http://stackoverflow.com/questions/2788092/start-a-git-commit-message-with-a-hashmark
この QA を見ると、Git のデフォルトのコメント開始の文字は “#” になっているので、 git config core.commentChar ";"
で別の文字に変更すればよいとのこと。
git config core.commentChar ";"
でコメント開始の文字を “;” に変更してから git rebase -i
を実行してみると、起動したエディタの下に表示されるコメントの一番左側の文字が “#” から “;” に変わっています。
このまま rebase の操作を続けると、問題なく rebase することができました。
この後いろいろ試してみたのですが、git config core.commentChar auto
と実行するとコメント開始の文字を “#” にしたまま git rebase -i
が成功するようになりました。
git config core.commentChar auto
を実行してから git rebase -i
を実行すると、起動したエディタのコメントは “#” から始まっています。
このまま rebase の操作を続けると、問題なく rebase することができました。
初めから auto で設定しておいてもらえればよさそうに思えるのですが、何か問題があるのかな。。。
また git config core.commentChar auto
のコマンドを実行しても .gitconfig には core.commentChar
の設定は保存されないんですよね。どこに保存されるのかも疑問です。。。