Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( 大目次 )
- その1 ( 概要 )
- その2 ( build.gradle の修正 )
- その3 ( Run ‘All Tests’ with Coverage 実行時に出るエラーを解消する )
- その4 ( 1.4系 → 1.5系で変更された点を修正する )
- その5 ( Thymeleaf を 2.1.5 → 3.0.6 へバージョンアップする )
- その6 ( Thymeleaf を 2.1.5 → 3.0.6 へバージョンアップする2 )
- その7 ( Gradle を 2.13 → 3.5 へバージョンアップし、FindBugs Gradle Plugin が出力する大量のログを抑制する )
- その8 ( logback-develop.xml, logback-unittest.xml, logback-product.xml の設定を logback-spring.xml と application.properties に移動してファイルを削除する )
- 番外編 ( static メソッドをモック化してテストするには? )
- その9 ( Spring Boot を 1.5.3 → 1.5.4 にバージョンアップする )
- 番外編 ( Groovy + JUnit4 でテストを書いてみる、Groovy SQL を使ってみる )
- その10 ( 起動時の spring.profiles.active のチェック処理を Set.contains を使用した方法に変更する )
- その11 ( build.gradle への PMD の導入 )
- その12 ( build.gradle への PMD の導入2 )
- その13 ( jar ファイルを作成して動作確認する )
- その14 ( request, response のログを出力する RequestAndResponseLogger クラスを修正する )
- その15 ( -XX:+ExitOnOutOfMemoryError と -XX:+CrashOnOutOfMemoryError オプションのどちらを指定すべきか? )
- 感想
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( 感想 )
記事一覧はこちらです。
1.3系 → 1.4 系へのバージョンアップでは動かなくなるということはあまりなくて、どちらかと言うと書き方がいろいろ変わるので、1.4 系の書き方に変更するのが主な対応になる、という感じでした。
Velocity が非推奨になったので FreeMarker に変えましたが、テキスト形式のテンプレートの場合には Thymeleaf 3 より FreeMarker の方が機能が揃っていて便利な気がします。ただし、デフォルトで数値は3桁毎に “,” 区切りになる等、まだ気付いていない点があるような気がしていて、使用時はちょっと注意が必要そうです。
CheckStyle, FindBugs を入れてみましたが、フォーマットやコードに問題があるところを指摘してくれるので便利でした。ただし Gradle を 3 系の最新版へバージョンアップすると FindBugs Plugin が警告メッセージを大量に出力するのが難点です。Gradle 3 へ上げようと思っていましたが、これのために止めました。FindBugs って今開発が止まっているんですよね。FindBugs を外して Gradle を 3 系へバージョンアップすべきか迷います。。。
ErrorProne は FindBugs では指摘してくれないコード上の問題点や、JDK 9 にした場合の問題点を報告してくれるので、こちらも今後は入れていきたいと思います。ただし lombok と相性が悪いんですよね。。。 バージョン 2.0.19 まで出ていますが、しばらく 2.0.15 で止めたままになる気がします。
以下の2つを今回入れましたが、こちらも便利でした。今後も入れていきます。
- Request mapper Plugin
- Log4jdbc Spring Boot Starter
次は 1.5.x へのバージョンアップをやる予定です。Thymeleaf も 3 に上げます。1.4.x → 1.5.x はそんなに大きな変更点はなさそうなので、短期間で終わるはず!(と言いつつ、たぶん寄り道するんだろうな。。。)
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その27 )( Thymeleaf parser-level comment blocks で @thymesVar のコメント文が HTML に出力されないようにする )
概要
記事一覧はこちらです。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その26 )( jar ファイルを作成して動作確認する2 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- Thymeleaf 3 で何が変わったのか確認したくて Tutorial: Using Thymeleaf を読んだのですが、Thymeleaf parser-level comment blocks というものがあることに今さら気づきました(Thymeleaf 3 の新機能ではありません)。
- @thymesVar のコメント文を HTML に出力しないようにするために使えそうなので試してみます。
参照したサイト・書籍
- Tutorial: Using Thymeleaf - 11.2. Thymeleaf parser-level comment blocks
http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#thymeleaf-parser-level-comment-blocks
目次
手順
@thymesVar のコメント文を HTML に出力しないようにする
IntelliJ IDEA Ulitimate Editon では <!-- @thymesVar id="beanValidationGroupForm" type="ksbysample.webapp.lending.web.springmvcmemo.BeanValidationGroupForm" -->
のようなコメント文を書いておくと、Thymeleaf テンプレート内で変数の補完が効くようになります。
コメント文を書いていないと、以下の画像の赤枠の部分で Ctrl+Enter を押しても何も表示されませんが、
<!-- @thymesVar id="beanValidationGroupForm" type="ksbysample.webapp.lending.web.springmvcmemo.BeanValidationGroupForm" -->
のコメントを付けると、Ctrl+Enter を押すと候補が表示されます。
ただし HTML を出力した時にこのコメントがそのまま残ります。
HTML を見た時に Thymeleaf が使われていることが分からないようにしたくて、出力されない方法をずっと探していたんですよね。。。
そこで見つけたのが Thymeleaf parser-level comment blocks です。Thymeleaf テンプレート内で記述するコメントの書き方を HTML のコメント文 <!-- ... -->
ではなく <!--/* ... */-->
にすれば、補完も効くし、HTML には出力されないようになります。
<!--/* @thymesVar id="beanValidationGroupForm" type="ksbysample.webapp.lending.web.springmvcmemo.BeanValidationGroupForm" */-->
に変えても候補は表示されます。
そしてコメントは HTML には出力されません。
以下のファイル内の <!-- @thymesVar ... -->
→ <!--/* @thymesVar ... */-->
に修正します。
- src/main/resources/templates/sessionsample/confirm.html
- src/main/resources/templates/sessionsample/first.html
- src/main/resources/templates/sessionsample/next.html
- src/main/resources/templates/springmvcmemo/beanValidationGroup.html
- src/main/resources/templates/textareamemo/display.html
- src/main/resources/templates/textareamemo/index.html
Alt+Enter で @thymesVar のコメント文は自動補完できました。。。
IntelliJ IDEA Ulitimate Editon では Thymeleaf テンプレート上で Alt+Enter を押すことで <!--/*@thymesVar ... */-->
のコメント文を自動補完できることに気づきました。
まず <!--/*@thymesVar ... */-->
のコメント文がない変数には赤波下線が表示されます。
赤波下線が表示されている変数にカーソルを移動した後、Alt+Enter を押してコンテキストメニューを表示した後「Declare external variable in comment annotation」を選択します。
<!--/*@thymesVar ... */-->
のコメント文が補完されます。type=""
の中は空でクラスの補完メニューが出ますので、入力します。
履歴
2017/05/13
初版発行。
2017/05/13
* <!--/*@thymesVar ... */-->
のコメント文の自動補完について追記しました。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( 番外編 )( Thymeleaf 3 へのバージョンアップを試してみる2 )
概要
記事一覧はこちらです。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( 番外編 )( Thymeleaf 3 へのバージョンアップを試してみる ) の続きです。Thymeleaf 3 ten-minute migration guide に書かれている新機能を試してみます。
今回も試してみるだけでコミットはしません。
参照したサイト・書籍
Thymeleaf 3 ten-minute migration guide
http://www.thymeleaf.org/doc/articles/thymeleaf3migration.htmlTutorial: Using Thymeleaf
http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
目次
手順
SpELコンパイラを有効にする
前回、「参照したサイト・書籍」にリンクを書いたのに設定するのを忘れていたので、Spring Boot 1.4+Thymeleaf 3.0でSpELコンパイラを有効にしてパフォーマンスを向上させよう!! の記事に従い SpELコンパイラを有効にします。
src/main/java/ksbysample/webapp/lending/config/WebMvcConfig.java を以下のように変更します。
@Configuration public class WebMvcConfig extends WebMvcConfigurerAdapter { .......... /** * Thymeleaf 3 のパフォーマンスを向上させるために SpEL コンパイラを有効にする * * @param templateEngine {@link SpringTemplateEngine} オブジェクト */ @Autowired public void configureThymeleafSpringTemplateEngine(SpringTemplateEngine templateEngine) { templateEngine.setEnableSpringELCompiler(true); } }
configureThymeleafSpringTemplateEngine
メソッドを追加します。
Fragment Expressions
Fragment Expressions は ~{ ... :: ... }
の形式でかなり自由に Thymeleaf テンプレートファイルに記載された HTML の一部を取得できるようになるような機能ですが、これを使ってヘッダーの共通化等がかなりやりやすくなっています。
例えば head タグ内の共通部分を定義するのに、これまでは共通部分を別の html ファイルに記述して th:replace
で読み込んでいました。
src/main/resources/templates/booklist/booklist.html では以下のように記述して、
<head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title>貸出希望書籍 CSV ファイルアップロード</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"/> <link th:replace="common/head-cssjs"/> <!-- Bootstrap File Input --> <link href="/css/fileinput.min.css" rel="stylesheet" type="text/css"/> <style type="text/css"> <!-- .callout ul li { margin-left: -30px; } --> </style> </head>
<link th:replace="common/head-cssjs"/>
で読み込んでいる src/main/resources/templates/common/head-cssjs.html は以下のように記述しています。
<!-- 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"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <style type="text/css"> <!-- .jp-gothic { font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; } .......... --> </style>
これで画面を表示すると以下の HTML が出力されます。
<head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title>貸出希望書籍 CSV ファイルアップロード</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"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <style type="text/css"> <!-- .jp-gothic { font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; } .......... --> </style> <!-- Bootstrap File Input --> <link href="/css/fileinput.min.css" rel="stylesheet" type="text/css"/> <style type="text/css"> <!-- .callout ul li { margin-left: -30px; } --> </style> </head>
これが Fragment Expressions の機能を利用すると head タグの共通部分を全て別ファイルに定義して、差分の箇所だけ各 html ファイルに記述できるようになります。
例えば src/main/resources/templates/common/head.html に head タグの共通部分を以下のように記述します。
<head th:fragment="common_header(title, links, style)"> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title th:replace="${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"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <!-- ここに各htmlで定義された link タグが追加される --> <th:block th:replace="${links}"/> <style type="text/css"> <!-- .jp-gothic { font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; } .......... --> </style> <!-- ここに各htmlで定義された style タグが追加される --> <th:block th:replace="${style}"/> </head>
src/main/resources/templates/booklist/booklist.html を以下のように修正します。変更したい title タグや、追加したい link, style タグだけ記述します。
<head th:replace="~{common/head :: common_header(~{::title}, ~{::link}, ~{::style})}"> <title>貸出希望書籍 CSV ファイルアップロード</title> <!-- Bootstrap File Input --> <link href="/css/fileinput.min.css" rel="stylesheet" type="text/css"/> <style type="text/css"> <!-- .callout ul li { margin-left: -30px; } --> </style> </head>
これで画面を表示すると以下の HTML が出力されます。src/main/resources/templates/booklist/booklist.html に記述した title, link, style タグの内容が反映されています。この例では link, style タグはそれぞれ1つしか記述していませんが、例えば link タグを2つ書けば、2つとも反映されます。
<head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title>貸出希望書籍 CSV ファイルアップロード</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"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <!-- ここに各htmlで定義された link タグが追加される --> <link href="/css/fileinput.min.css" rel="stylesheet" type="text/css"/> <style type="text/css"> <!-- .jp-gothic { font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; } .......... --> </style> <!-- ここに各htmlで定義された style タグが追加される --> <style type="text/css"> <!-- .callout ul li { margin-left: -30px; } --> </style> </head>
src/main/resources/templates/booklist/booklist.html の head タグの中に何も書かないと、
<head th:replace="~{common/head :: common_header(~{::title}, ~{::link}, ~{::style})}"> </head>
エラー画面が表示されました。。。
ログを見ると org.thymeleaf.exceptions.TemplateInputException: Error resolving fragment: "${title}": template or fragment could not be resolved (template: "common/head" - line 4, col 12)
というログが出力されていました。head タグの中に title タグを記述しなかったので、引数の ~{::title}
が空になりエラーになったようです。
Fragment Expressions と同じく新規に追加された The No-Operation token の機能を利用して src/main/resources/templates/common/head.html を以下のように修正します。?: _
を付けると、値がセットされていなければ th:replace
の処理が行われなくなります。
<head th:fragment="common_header(title, links, style)"> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title th:replace="${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"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <!-- ここに各htmlで定義された link タグが追加される --> <th:block th:replace="${links} ?: _"/> <style type="text/css"> <!-- .jp-gothic { font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; } .......... --> </style> <!-- ここに各htmlで定義された style タグが追加される --> <th:block th:replace="${style} ?: _"/> </head>
今度は画面が表示されて、
以下の HTML が出力されました。title タグは head.html の記述がそのまま出力されて、th:block
で書いていたところには何も出力されていません。
<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"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <!-- ここに各htmlで定義された link タグが追加される --> <style type="text/css"> <!-- .jp-gothic { font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; } .......... --> </style> <!-- ここに各htmlで定義された style タグが追加される --> </head>
また <th:block th:replace="${style} ?: _"/>
は The EMPTY fragment を利用して <th:block th:replace="${style} ?: ~{}"/>
と書いても結果は同じになります。
Fragment Expressions の詳細は以下のリンク先の Issue, Tutorial に書かれています。
- Fragment Expressions
https://github.com/thymeleaf/thymeleaf/issues/451 - Tutorial: Using Thymeleaf("fragment" で検索するといろいろヒットします)
http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
共通部分をまとめることができ、かつ部分的な変更もしやすくなるので、かなり便利な機能だと思います。
The No-Operation token
The No-Operation token は記述した条件にマッチしなければ th:*
の内容が反映されない機能です。
既に Fragment Expressions に The No-Operation token の例を記述しましたが、別の例を書いてみます。
画面右上の図書館の選択状態を表示するために、src/main/resources/templates/common/mainparts.html に以下のように記述していますが、
<!-- Navbar Right Menu --> <div class="navbar-custom-menu"> <p class="navbar-text" th:classappend="${#strings.startsWith(@libraryHelper.getSelectedLibrary(), '※')} ? 'noselected-library' : 'selected-library'" th:text="${@libraryHelper.getSelectedLibrary()}">※図書館が選択されていません</p> <ul class="nav navbar-nav"> <li><a href="/logout">ログアウト</a></li> </ul> </div> <!-- /.navbar-custom-menu -->
これを以下のように変更します。@libraryHelper.getSelectedLibrary()
が値を返さなければ、HTML の ※図書館が選択されていません
をそのまま表示します。
<!-- Navbar Right Menu --> <div class="navbar-custom-menu"> <p class="navbar-text" th:with="selectedLibrary=${@libraryHelper.getSelectedLibrary()}" th:classappend="${selectedLibrary} ? 'selected-library' : 'noselected-library'" th:text="${selectedLibrary} ?: _">※図書館が選択されていません</p> <ul class="nav navbar-nav"> <li><a href="/logout">ログアウト</a></li> </ul> </div> <!-- /.navbar-custom-menu -->
@libraryHelper.getSelectedLibrary()
の実装である src/main/java/ksbysample/webapp/lending/helper/library/LibraryHelper.java は現在以下のコードですが、
@Component public class LibraryHelper { .......... /** * @return ??? */ public String getSelectedLibrary() { String result; LibraryForsearch libraryForsearch = libraryForsearchDao.selectSelectedLibrary(); if (libraryForsearch == null) { result = "※図書館が選択されていません"; } else { result = "選択中:" + libraryForsearch.getFormal(); } return result; } }
getSelectedLibrary メソッドを以下のように変更します。図書館が未選択なら null を返します。
public String getSelectedLibrary() { String result = null; LibraryForsearch libraryForsearch = libraryForsearchDao.selectSelectedLibrary(); if (libraryForsearch != null) { result = "選択中:" + libraryForsearch.getFormal(); } return result; }
library_search テーブルをクリアして検索対象図書館登録画面を表示すると、「※図書館が選択されていません」のメッセージが表示されます。
図書館を選択すると、選択された図書館が表示されます。
Decoupled Template Logic
Decoupled Template Logic は HTML から Thymeleaf の記述を分離して、HTML ファイル自体には th:*
タグがないままに出来る機能です。
サンプルを作ろうと思いましたが、th:*
の記述を HTML に直接書いてもそんなに邪魔にならないと自分では思っているのと、th:*
を記述するところに id や class 等で指定しやすくしておかないと別ファイルから HTML 本体への位置指定が書きにくそうに思えたので、止めました。今の自分ではこの機能を使うことはないという印象です。
最後に
Thymeleaf 3 でいろいろ便利になっている印象を受けました。Thymeleaf 3 ten-minute migration guide だけでなく Tutorial も読んでみようと思います。
履歴
2017/05/11
初版発行。
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
初版発行。