Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その5 )( メールのテンプレートに使用していた Velocity を FreeMarker に変更する )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
java: org.springframework.ui.velocityのorg.springframework.ui.velocity.VelocityEngineUtilsは非推奨になりました
の対応として、これまでメールのテンプレートに使用していた Velocity を FreeMarker に変更します。- 今後のサンプル作成で Thymeleaf 3 は必ずさわると思うので、今回は FreeMarker を使用する場合の対応方法を調べたいと思います。
参照したサイト・書籍
FreeMarker Java Template Engine
http://freemarker.org/Spring BootアプリのテストをSpockで書く
http://int128.hatenablog.com/entry/2016/12/13/003600Spring Boot Security + Thymeleaf : IProcessorDialect class missing
http://stackoverflow.com/questions/37270322/spring-boot-security-thymeleaf-iprocessordialect-class-missingHow to check if a variable exists in a FreeMarker template?
http://stackoverflow.com/questions/306732/how-to-check-if-a-variable-exists-in-a-freemarker-templateThe Move from Velocity to FreeMarker with Spring Boot
http://nixmash.com/java/the-move-from-velocity-to-freemarker-with-spring-boot/
目次
- build.gradle を変更する
- spring-boot-starter-freemarker では何が auto-configuration されるのか?
- application.properties を変更する
- VelocityUtils → FreeMarkerUtils へ変更する
- FreeMarkerUtilsTest クラスを作成して動作確認する
- メールのテンプレートファイルを変更する
- ksbysample.webapp.lending.helper.mail パッケージの下の MailxxxHelper クラスを変更する
- 動作確認はまだできないので Rebuild Project の確認だけ行う
- 次回は。。。
手順
build.gradle を変更する
build.gradle を リンク先のその1の内容 に変更します。
Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
FreeMaker のホームページ を見ると最新版は Latest stable release: 2.3.25-incubating と書かれていますが、spring-boot-starter-freemarker を追加してダウンロードされたバージョンも同じでした。
spring-boot-starter-freemarker では何が auto-configuration されるのか?
Spring Boot でメール送信する Web アプリケーションを作る ( その6 )( メール送信画面の作成 ) で spring-boot-starter-velocity を調べた時のように spring-boot-starter-freemarker の AutoConfiguration の動作を確認します。
org.springframework.boot.autoconfigure.freemarker の FreeMarkerAutoConfiguration クラスが FreeMarker の AutoConfiguration クラスで、ソースを見て分かることは、
- spring.freemarker.enabled = false を設定すれば、HTML のテンプレートファイル用の設定は反映されません ( freeMarkerViewResolver Bean が生成されません )。
- VelocityEngine に該当するのは freemarker.template.Configuration を返す freeMarkerConfiguration Bean のようです。FreeMarker の Manual の Create a configuration instance と Get the template を見た感じでは freemarker.template.Configuration から freemarker.template.Template を生成すればメールのテンプレートとして利用できそうです。
- テンプレートファイルの拡張子は
.ftl
です ( これは org.springframework.boot.autoconfigure.freemarker の下の FreeMarkerProperties クラスに記述があります )。
また application.properties に設定する項目を Spring Boot Reference Guide の Appendix A. Common application properties で確認すると、
- FreeMarker の設定は spring.freemarker.~ で設定します。
キャッシュの設定がデフォルトでは
spring.freemarker.cache=false
と書かれています。デフォルトは有効で DevTools を入れると無効になる、という訳ではないようです。IntelliJ IDEA の補完ではデフォルト値は表示されないのですが、どちらが正しいのかは build まで通った後に忘れていなければ検証したいと思います。ちなみに DevTools を入れると FreeMarker のキャッシュが無効になることは org.springframework.boot.devtools.env の下の DevToolsPropertyDefaultsPostProcessor クラスに
properties.put("spring.freemarker.cache", "false");
と記述されていることで確認しています。
application.properties を変更する
- src/main/resources の下の application.properties を リンク先の内容 に変更します。
VelocityUtils → FreeMarkerUtils へ変更する
Project Tool Window で ksbysample.webapp.lending.util.velocity を選択し、Shift+F6 を押して「Rename」ダイアログを表示した後、"velocity" → “freemarker” へ変更して「OK」ボタンをクリックします。
Project Tool Window で ksbysample.webapp.lending.util.freemarker の下の VelocityUtils.java を選択し、Shift+F6 を押して「Rename」ダイアログを表示した後、"VelocityUtils" → “FreeMarkerUtils” へ変更して「OK」ボタンをクリックします。
今回は使用先のフィールドの変数名も変更するかを確認する「Rename Variables」ダイアログが表示されますので「Select all」ボタンを押して選択した後「OK」ボタンをクリックします。
src/main/java/ksbysample/webapp/lending/util/freemarker の下の FreeMarkerUtils.java を リンク先の内容 に変更します。
FreeMarkerUtilsTest クラスを作成して動作確認する
テストは Spock で作ります。Spring IO Platform で指定される Spock のバージョン番号は 1.0-groovy-2.4
なのですが、このバージョンではまだ Spring Boot の 1.4 から導入されたテスト用の新アノテーションに対応していないとのことなので、対応されている 1.1 をバージョン番号を指定して導入します。
build.gradle を リンク先のその2の内容 に変更します。
Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
次にテストクラスを作ります。
FreeMarkerUtils.java のソース上で Ctrl+Shift+T を押してコンテキストメニューを表示した後「Create New Test…」を選択します。「Create Test」ダイアログが表示されたら以下の画像の状態にした後、「OK」ボタンをクリックします。
「Choose Destination Directory」ダイアログが表示されたら「…\src\test\groovy...」の方を選択して「OK」ボタンをクリックします。
src/test/groovy/ksbysample/webapp/lending/util/freemarker の下に FreeMarkerUtilsTest.groovy が作成されますので、リンク先のその1の内容 に変更します。
src/test/resources の下に templates/mail ディレクトリを作成します。
src/test/resources/templates/mail の下に FreeMarkerUtilsTest-001.ftl を作成し、リンク先の内容 を記述します。
テストを実行してみます。が、以下のソースで
java: パッケージorg.apache.commons.langは存在しません
というエラーメッセージが表示されました。- src/main/java/ksbysample/webapp/lending/web/lendingapp/LendingappController.java
- src/main/java/ksbysample/webapp/lending/web/ExceptionHandlerAdvice.java
どうも 1.0 の spock の依存関係に org.apache.commons.lang.StringUtils が入っていて、意識せずにそちらを使用していたようです。
import org.apache.commons.lang.StringUtils;
→import org.apache.commons.lang3.StringUtils;
へ変更します。再度テストを実行してみます。が、今度は “java.lang.NoClassDefFoundError: org/thymeleaf/dialect/IExpressionObjectDialect” のエラーが出ました。このエラーを解消しないとテストが通らないようです。原因を調べます。
Web で検索したところ、stackoverflow で Spring Boot Security + Thymeleaf : IProcessorDialect class missing という QA を見つけました。
build.gradle を見ると thymeleaf-extras-springsecurity4 は Spring IO Platform によりバージョン番号が自動で設定されるようにしていましたが、
compile("org.thymeleaf.extras:thymeleaf-extras-java8time:3.0.0.RELEASE")
という指定を別にしていました。これが原因ですね。Spring IO Platform の Appendix A. Dependency versions を見ると
org.thymeleaf.extras:thymeleaf-extras-java8time
が記述されていて Spring IO Platform の対象にできることが判明したので、バージョン番号を build.gradle で指定しないようにします。build.gradle を リンク先のその3の内容 に変更します。
Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
再度テストを実行すると今度は成功しました。
でもなんかテストに時間がかかるような。。。 最初、このテストに Web 環境は不要なので
@SpringBootTest(webEnvironment = MOCK)
ではなく@SpringBootTest(webEnvironment = NONE)
を指定したのですが、この記述だと ksbysample.common.test.rule.mockmvc の SecurityMockMvcResource クラスで@Autowired private WebApplicationContext context;
に DI が出来なくてエラーになったので、MOCK に変更しました。SecurityMockMvcResource クラスの Bean 生成を Web 環境の時だけに出来ないかな?と思って @Conditional 系アノテーションを調べたところ
@ConditionalOnWebApplication
というのがあったので、SecurityMockMvcResource クラスに付加したいと思います。src/test/java/ksbysample/common/test/rule/mockmvc の下の SecurityMockMvcResource.java を リンク先の内容 に変更します。
テストを実行すると成功し、実行時間も4~5秒程度速くなりました。実施したことの割に速くなり過ぎでは?とも思いましたが、今は気にしないことにします。
もう少しテストを追加します。src/test/groovy/ksbysample/webapp/lending/util/freemarker の下の FreeMarkerUtilsTest.groovy を リンク先のその3の内容 に変更します。
src/test/resources/templates/mail の下に以下のファイルを作成し、リンク先の内容 を記述します。
テストを実行して全て成功することを確認します。
メールのテンプレートファイルを変更する
src/main/resources/templates/mail の下のファイルの拡張子を全て
.vm
→.ftl
に変更します。src/main/resources/templates/mail の下の mail003-body.ftl を リンク先の内容 に変更します。
ksbysample.webapp.lending.helper.mail パッケージの下の MailxxxHelper クラスを変更する
必要があるかなと思っていたのですが、IntelliJ IDEA のリファクタリングの機能でいろいろ変更をした時に MailxxxHelper クラスにも必要な変更が反映されていて、この時点では何もすることがありませんでした。
以下のソースに自動で変更が反映されています。
- src/main/java/ksbysample/webapp/lending/helper/mail/Mail001Helper.java
- src/main/java/ksbysample/webapp/lending/helper/mail/Mail002Helper.java
- src/main/java/ksbysample/webapp/lending/helper/mail/Mail003Helper.java
変更内容は以下の点です。
private VelocityUtils velocityUtils;
→private FreeMarkerUtils freeMarkerUtils;
へ変更します。- TEMPLATE_LOCATION_TEXTMAIL 定数で指定しているテンプレートファイルの拡張子を
.vm
→.ftl
へ変更します。
動作確認はまだできないので Rebuild Project の確認だけ行う
動作確認は build でエラーが出なくなってから行いますので、この時点では clean タスク実行 → Rebuild Project を実行して Warning が1つも出ないことだけ確認します。
次回は。。。
「Run ‘All Tests’ with Coverage」と build タスクを実行してみましたが、まだエラーが出ていますので次回は「Run ‘All Tests’ with Coverage」実行時のエラーを解消します。
また今回 Velocity → FreeMarker へ切り替えてみて、以下の感想でした。
- 使い勝手はほとんど変わらず、むしろ FreeMarker の方が高機能です。
- FreeMarker は Web 上のマニュアルも綺麗でまとまっていて分かりやすい!
2010 年で開発が止まっている Velocity に対して Spring Boot がサポートを終了するのも仕方がないかな、と思いました。
と、ここまで書いてから The Move from Velocity to FreeMarker with Spring Boot の記事を見つけました。org.springframework.ui.freemarker の下に FreeMarkerTemplateUtils クラスなんてあるんですね。。。
ソースコード
build.gradle
■その1
dependencies { def jdbcDriver = "org.postgresql:postgresql:9.4.1212" // 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.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-velocity")
→compile("org.springframework.boot:spring-boot-starter-freemarker")
へ変更します。
■その2
dependencies { def jdbcDriver = "org.postgresql:postgresql:9.4.1212" def spockVersion = "1.1-groovy-2.4-rc-3" // dependency-management-plugin によりバージョン番号が自動で設定されるもの // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照 .......... // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの .......... testCompile("org.jmockit:jmockit:1.30") testCompile("org.spockframework:spock-core:${spockVersion}") { exclude module: "groovy-all" } testCompile("org.spockframework:spock-spring:${spockVersion}") { exclude module: "groovy-all" }
def spockVersion = "1.1-groovy-2.4-rc-3"
を追加します。org.spockframework:spock-core
、org.spockframework:spock-spring
をバージョン番号を指定するので記述位置を下へ変更します。testCompile("org.spockframework:spock-core")
→testCompile("org.spockframework:spock-core:${spockVersion}")
へ変更します。testCompile("org.spockframework:spock-spring")
→testCompile("org.spockframework:spock-spring:${spockVersion}")
へ変更します。
■その3
dependencies { def jdbcDriver = "org.postgresql:postgresql:9.4.1212" def spockVersion = "1.1-groovy-2.4-rc-3" // 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.thymeleaf.extras:thymeleaf-extras-java8time:3.0.0.RELEASE")
→compile("org.thymeleaf.extras:thymeleaf-extras-java8time")
へ変更し、記述位置を上へ変更します。
application.properties
hibernate.dialect=org.hibernate.dialect.PostgreSQL9Dialect doma.dialect=org.seasar.doma.jdbc.dialect.PostgresDialect spring.jpa.hibernate.ddl-auto=none spring.jpa.hibernate.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy spring.freemarker.cache=true spring.freemarker.charset=UTF-8 spring.freemarker.enabled=false spring.freemarker.prefer-file-system-access=false
- 以下の設定を削除します。
spring.velocity.enabled=false
spring.velocity.charset=UTF-8
- 以下の設定を追加します。
spring.freemarker.cache=true
spring.freemarker.charset=UTF-8
spring.freemarker.enabled=false
spring.freemarker.prefer-file-system-access=false
- デフォルトは true ですが、この設定を入れないとテストクラスからテストを実行した時に src/main/resources の下のテンプレートファイルを見に行ってくれませんでした。なぜデフォルト値が true なのか疑問です。。。
spring.jpa.hibernate.naming_strategy
の設定は非推奨か無くなっているようなので ( IntelliJ IDEA のエディタ上で取消線が表示されます )、次回以降に見直します。
FreeMarkerUtils.java
package ksbysample.webapp.lending.util.freemarker; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; import org.springframework.stereotype.Component; import java.io.IOException; import java.io.StringWriter; import java.util.Map; @Component public class FreeMarkerUtils { private final Configuration freeMarkerConfiguration; public FreeMarkerUtils(Configuration freeMarkerConfiguration) { this.freeMarkerConfiguration = freeMarkerConfiguration; } public String merge(String templateLocation, Map<String, Object> model) { Template template = getTemplate(templateLocation); return process(template, model); } private Template getTemplate(String templateLocation) { try { return this.freeMarkerConfiguration.getTemplate(templateLocation); } catch (IOException e) { throw new RuntimeException(e); } } private String process(Template template, Map<String, Object> model) { try { StringWriter sw = new StringWriter(); template.process(model, sw); return sw.toString(); } catch (TemplateException | IOException e) { throw new RuntimeException(e); } } }
FreeMarkerUtilsTest.groovy
■その1
package ksbysample.webapp.lending.util.freemarker import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import spock.lang.Specification import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.MOCK @SpringBootTest(webEnvironment = MOCK) class FreeMarkerUtilsTest extends Specification { @Autowired FreeMarkerUtils freeMarkerUtils def "テンプレートファイルから文字列を生成する_変数のみの場合"() { setup: def model = [username: "田中 太郎"] expect: freeMarkerUtils.merge("mail/FreeMarkerUtilsTest-001.ftl", model) == "田中 太郎" } }
■その2
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE @SpringBootTest(webEnvironment = NONE) class FreeMarkerUtilsTest extends Specification {
@SpringBootTest(webEnvironment = MOCK)
→@SpringBootTest(webEnvironment = NONE)
に変更します。
■その3
package ksbysample.webapp.lending.util.freemarker import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import spock.lang.Specification import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE @SpringBootTest(webEnvironment = NONE) class FreeMarkerUtilsTest extends Specification { @Autowired FreeMarkerUtils freeMarkerUtils class TestUser { String name Integer age String address } def "テンプレートファイルから文字列を生成する_変数のみの場合"() { setup: def model = [username: "田中 太郎"] expect: freeMarkerUtils.merge("mail/FreeMarkerUtilsTest-001.ftl", model) == "田中 太郎" } /** * 変数が null の場合にエラーにならないようにするには、テンプレートファイルの変数の最後に ! を付けること。 * ${xxx} ではなく ${xxx!} のように書く。 * null の時にはスペースで埋めたい時には ${xxx!?left_pad(3)} のように書ける * * ただし、この記述にすると model に必要なデータがない時に例外が throw されないので注意すること */ def "テンプレートファイルから文字列を生成する_変数がnullの場合"() { setup: def model = [username: null] expect: freeMarkerUtils.merge("mail/FreeMarkerUtilsTest-001.ftl", model) == "" } def "テンプレートファイルから文字列を生成する_変数+クラスの場合"() { setup: def model = [ username: "田中 太郎" , user : new TestUser(age: 25, address: "東京都千代田区") ] def result = new File("src/test/resources/templates/mail/FreeMarkerUtilsTest-002-result.txt").text expect: freeMarkerUtils.merge("mail/FreeMarkerUtilsTest-002.ftl", model) == result } def "テンプレートファイルから文字列を生成する_リストの場合"() { setup: def userList = [ new TestUser(name: "田中 太郎", age: 25, address: "東京都千代田区") , new TestUser(name: "鈴木 花子", age: 8, address: "神奈川県横浜市") , new TestUser(name: "高橋 孝", age: 100, address: "埼玉県大宮市") ] def model = [userList: userList] def result = new File("src/test/resources/templates/mail/FreeMarkerUtilsTest-003-result.txt").text expect: freeMarkerUtils.merge("mail/FreeMarkerUtilsTest-003.ftl", model) == result } def "テンプレートファイルから文字列を生成する_リストでnameの一部がnullの場合"() { setup: def userList = [ new TestUser(name: "田中 太郎", age: 25, address: "東京都千代田区") , new TestUser(name: null, age: 8, address: "神奈川県横浜市") , new TestUser(name: "高橋 孝", age: 100, address: "埼玉県大宮市") ] def model = [userList: userList] def result = new File("src/test/resources/templates/mail/FreeMarkerUtilsTest-003-result2.txt").text expect: freeMarkerUtils.merge("mail/FreeMarkerUtilsTest-003.ftl", model) == result } def "テンプレートファイルが存在しない場合はエラーになる"() { given: def model = [username: "田中 太郎"] when: freeMarkerUtils.merge("mail/FreeMarkerUtilsTest-notFound.ftl", model) then: RuntimeException e = thrown() e.getMessage() contains "Template not found" } }
FreeMarkerUtilsTest-001.ftl
${username!}
- 変数名の最後に ! を付けておくと、値が null の時にエラーになりません。
SecurityMockMvcResource.java
@Component @ConditionalOnWebApplication public class SecurityMockMvcResource extends ExternalResource {
@ConditionalOnWebApplication
を追加します。
FreeMarkerUtilsTest-002.ftl, FreeMarkerUtilsTest-002-result.txt, FreeMarkerUtilsTest-003.ftl, FreeMarkerUtilsTest-003-result.txt, FreeMarkerUtilsTest-003-result2.txt
■FreeMarkerUtilsTest-002.ftl
氏名: ${username!} 年齢: ${user.age!} 住所: ${user.address!}
■FreeMarkerUtilsTest-002-result.txt
氏名: 田中 太郎 年齢: 25 住所: 東京都千代田区
■FreeMarkerUtilsTest-003.ftl
氏名 年齢 住所 ---------------------------------------------------------- <#list userList as user> ${user.name!?right_pad(8, " ")} ${user.age!?left_pad(3)} ${user.address!} </#list>
■FreeMarkerUtilsTest-003-result.txt
氏名 年齢 住所 ---------------------------------------------------------- 田中 太郎 25 東京都千代田区 鈴木 花子 8 神奈川県横浜市 高橋 孝 100 埼玉県大宮市
■FreeMarkerUtilsTest-003-result2.txt
氏名 年齢 住所 ---------------------------------------------------------- 田中 太郎 25 東京都千代田区 8 神奈川県横浜市 高橋 孝 100 埼玉県大宮市
mail003-body.ftl
貸出申請が承認・却下されました。 ======================================================================== 承認/却下 書籍 ------------------------------------------------------------------------ <#list mail003BookDataList as bookData> ${bookData.approvalResultStr} ${bookData.bookName} </#list> ======================================================================== 詳細は以下のURLから確認してください。 http://localhost:8080/confirmresult?lendingAppId=${lendingAppId}
履歴
2017/02/12
初版発行。