かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その5 )( メールのテンプレートに使用していた Velocity を FreeMarker に変更する )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その4 )( build.gradle 修正後の Rebuild で出た Warning を解消する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • java: org.springframework.ui.velocityのorg.springframework.ui.velocity.VelocityEngineUtilsは非推奨になりました の対応として、これまでメールのテンプレートに使用していた Velocity を FreeMarker に変更します。
    • 今後のサンプル作成で Thymeleaf 3 は必ずさわると思うので、今回は FreeMarker を使用する場合の対応方法を調べたいと思います。

参照したサイト・書籍

  1. FreeMarker Java Template Engine
    http://freemarker.org/

  2. Spring BootアプリのテストをSpockで書く
    http://int128.hatenablog.com/entry/2016/12/13/003600

  3. Spring Boot Security + Thymeleaf : IProcessorDialect class missing
    http://stackoverflow.com/questions/37270322/spring-boot-security-thymeleaf-iprocessordialect-class-missing

  4. How 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-template

  5. The Move from Velocity to FreeMarker with Spring Boot
    http://nixmash.com/java/the-move-from-velocity-to-freemarker-with-spring-boot/

目次

  1. build.gradle を変更する
  2. spring-boot-starter-freemarker では何が auto-configuration されるのか?
  3. application.properties を変更する
  4. VelocityUtils → FreeMarkerUtils へ変更する
  5. FreeMarkerUtilsTest クラスを作成して動作確認する
  6. メールのテンプレートファイルを変更する
  7. ksbysample.webapp.lending.helper.mail パッケージの下の MailxxxHelper クラスを変更する
  8. 動作確認はまだできないので Rebuild Project の確認だけ行う
  9. 次回は。。。

手順

build.gradle を変更する

  1. build.gradle を リンク先のその1の内容 に変更します。

  2. Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

    FreeMaker のホームページ を見ると最新版は Latest stable release: 2.3.25-incubating と書かれていますが、spring-boot-starter-freemarker を追加してダウンロードされたバージョンも同じでした。

    f:id:ksby:20170211102648p:plain

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 instanceGet 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 まで通った後に忘れていなければ検証したいと思います。

    f:id:ksby:20170211111354p:plain

    ちなみに DevTools を入れると FreeMarker のキャッシュが無効になることは org.springframework.boot.devtools.env の下の DevToolsPropertyDefaultsPostProcessor クラスに properties.put("spring.freemarker.cache", "false"); と記述されていることで確認しています。

application.properties を変更する

  1. src/main/resources の下の application.properties を リンク先の内容 に変更します。

VelocityUtils → FreeMarkerUtils へ変更する

  1. Project Tool Window で ksbysample.webapp.lending.util.velocity を選択し、Shift+F6 を押して「Rename」ダイアログを表示した後、"velocity" → “freemarker” へ変更して「OK」ボタンをクリックします。

    f:id:ksby:20170211115102p:plain

  2. Project Tool Window で ksbysample.webapp.lending.util.freemarker の下の VelocityUtils.java を選択し、Shift+F6 を押して「Rename」ダイアログを表示した後、"VelocityUtils" → “FreeMarkerUtils” へ変更して「OK」ボタンをクリックします。

    f:id:ksby:20170211115443p:plain

    今回は使用先のフィールドの変数名も変更するかを確認する「Rename Variables」ダイアログが表示されますので「Select all」ボタンを押して選択した後「OK」ボタンをクリックします。

    f:id:ksby:20170211181819p:plain

  3. 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 をバージョン番号を指定して導入します。

  1. build.gradle を リンク先のその2の内容 に変更します。

  2. Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

次にテストクラスを作ります。

  1. FreeMarkerUtils.java のソース上で Ctrl+Shift+T を押してコンテキストメニューを表示した後「Create New Test…」を選択します。「Create Test」ダイアログが表示されたら以下の画像の状態にした後、「OK」ボタンをクリックします。

    f:id:ksby:20170211125157p:plain

    「Choose Destination Directory」ダイアログが表示されたら「…\src\test\groovy...」の方を選択して「OK」ボタンをクリックします。

  2. src/test/groovy/ksbysample/webapp/lending/util/freemarker の下に FreeMarkerUtilsTest.groovy が作成されますので、リンク先のその1の内容 に変更します。

  3. src/test/resources の下に templates/mail ディレクトリを作成します。

  4. src/test/resources/templates/mail の下に FreeMarkerUtilsTest-001.ftl を作成し、リンク先の内容 を記述します。

  5. テストを実行してみます。が、以下のソースで 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; へ変更します。

  6. 再度テストを実行してみます。が、今度は “java.lang.NoClassDefFoundError: org/thymeleaf/dialect/IExpressionObjectDialect” のエラーが出ました。このエラーを解消しないとテストが通らないようです。原因を調べます。

    f:id:ksby:20170211183545p:plain

  7. 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 で指定しないようにします。

  8. build.gradle を リンク先のその3の内容 に変更します。

  9. Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

  10. 再度テストを実行すると今度は成功しました。

    f:id:ksby:20170211191343p:plain

    でもなんかテストに時間がかかるような。。。 最初、このテストに 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 クラスに付加したいと思います。

  11. src/test/java/ksbysample/common/test/rule/mockmvc の下の SecurityMockMvcResource.javaリンク先の内容 に変更します。

  12. テストを実行すると成功し、実行時間も4~5秒程度速くなりました。実施したことの割に速くなり過ぎでは?とも思いましたが、今は気にしないことにします。

    f:id:ksby:20170211193737p:plain

  13. もう少しテストを追加します。src/test/groovy/ksbysample/webapp/lending/util/freemarker の下の FreeMarkerUtilsTest.groovy を リンク先のその3の内容 に変更します。

  14. src/test/resources/templates/mail の下に以下のファイルを作成し、リンク先の内容 を記述します。

    • FreeMarkerUtilsTest-002.ftl
    • FreeMarkerUtilsTest-002-result.txt
    • FreeMarkerUtilsTest-003.ftl
    • FreeMarkerUtilsTest-003-result.txt
    • FreeMarkerUtilsTest-003-result2.txt
  15. テストを実行して全て成功することを確認します。

f:id:ksby:20170212010447p:plain

メールのテンプレートファイルを変更する

  1. src/main/resources/templates/mail の下のファイルの拡張子を全て .vm.ftl に変更します。

  2. 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つも出ないことだけ確認します。

f:id:ksby:20170212014837p:plain

次回は。。。

「Run ‘All Tests’ with Coverage」と build タスクを実行してみましたが、まだエラーが出ていますので次回は「Run ‘All Tests’ with Coverage」実行時のエラーを解消します。

f:id:ksby:20170212024752p:plain f:id:ksby:20170212024239p:plain

また今回 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-coreorg.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
初版発行。