かんがるーさんの日記

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

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 に書かれている新機能を試してみます。

今回も試してみるだけでコミットはしません。

参照したサイト・書籍

  1. Thymeleaf 3 ten-minute migration guide
    http://www.thymeleaf.org/doc/articles/thymeleaf3migration.html

  2. Tutorial: Using Thymeleaf
    http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

目次

  1. SpELコンパイラを有効にする
  2. Fragment Expressions
  3. The No-Operation token
  4. Decoupled Template Logic
  5. 最後に

手順

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>

エラー画面が表示されました。。。

f:id:ksby:20170511000328p:plain

ログを見ると 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>

今度は画面が表示されて、

f:id:ksby:20170511002844p:plain

以下の 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 に書かれています。

共通部分をまとめることができ、かつ部分的な変更もしやすくなるので、かなり便利な機能だと思います。

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 テーブルをクリアして検索対象図書館登録画面を表示すると、「※図書館が選択されていません」のメッセージが表示されます。

f:id:ksby:20170511013520p:plain

図書館を選択すると、選択された図書館が表示されます。

f:id:ksby:20170511013726p:plain

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
初版発行。