読者です 読者をやめる 読者になる 読者になる

Spring Boot で Thymeleaf テンプレートを使う

概要

Hello World をブラウザで表示できることを確認できたので、 HTML 文書をブラウザで表示できるようにします。 HTML を表示する際には静的な HTML としてではなく、 動的に生成した HTML を表示するようにするため、 テンプレートエンジンを使用します。 テンプレートエンジンには Thymeleaf (タイムリーフ) を使います。

確認環境

  • IntelliJ IDEA COMMUNITY 2016.2
  • Spring Boot 1.4.0

参考情報

Thymeleaf の使い方。日本語。

Thymeleaf と Spring の連携。英語。

手順

  1. build.gradle に org.springframework.boot:spring-boot-starter-thymeleaf を追加
  2. テンプレートを作成
  3. Controller を作成
  4. ブラウザで確認

Gradle の依存設定に Thymeleaf を追加する。下記は build.gradle ファイルの一部。

dependencies {
    ...
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    ...
}

テンプレートを作成する。形式は HTML であり拡張子は html として作成する。 今回は、user-request-list.html を src/main/resources/templates/ に以下の内容で作成した。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>機能一覧</title>
</head>
<body>

<table>
    <thead>
    <tr>
        <th>ID</th>
        <th>機能概要</th>
        <th>人気</th>
    </tr>
    </thead>
    <tbody>
    <tr>
        <td>FIX_1</td>
        <td>A機能の不具合を修正して欲しい</td>
        <td>50</td>
    </tr>
    <tr>
        <td>NEW_1</td>
        <td>B機能を追加して欲しい</td>
        <td>50</td>
    </tr>
    </tbody>
</table>

</body>
</html>

Controller を作成する。 @Controller アノテーションを付与したクラスと @RequestMapping を付与したメソッドを作成する。 今回は、UserRequestController.kt を src/main/kotlin/.../web に以下の内容で作成した。

package org.anyspirit.webapp.ask.request.web

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping

@Controller
class UserRequestController {

    @RequestMapping("/user-request-list")
    fun showList(): String = "user-request-list"
}

@RequestMapping の /user-request-list は URL の http://localhost:8080/user-request-list と対応する。 showList メソッドの戻り値の user-request-list はテンプレートの user-request-list.html と対応する。

ブラウザで http://localhost:8080/user-request-list を開くとエラーとなる。 ブラウザに表示されたエラーの内容は Exception parsing document: template="user-request-list", line 6 - column 3 となっており、 Terminal を確認すると下記のエラーが表示されている。

2016-08-29 17:30:13.343 ERROR 7100 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateInputException: Exception parsing document: template="user-request-list", line 6 - column 3] with root cause

org.xml.sax.SAXParseException: The element type "meta" must be terminated by the matching end-tag "</meta>".

meta タグを閉じなければならないとのこと。/ を追加して閉じタグがないことを明示したところ、エラーは表示されずに HTML が表示された。

    <meta charset="UTF-8" />

補足

meta タグを閉じなければならないことから、設定が XHTML になっていることを疑った。 適用されている auto-configuration を見るには debug スイッチを付与して起動するとある (Spring Boot Reference Guide)。 bootRun に --debug を付与して下記のように起動すると Gradle の DEBUG メッセージは表示されるが、 アプリケーションの DEBUG メッセージは表示されない。

$ ./gradlew --debug bootRun

アプリケーションの DEBUG メッセージを表示するには、build.gradle に下記を追加する方法がある。

bootRun {
    jvmArgs = ['-Ddebug']
}

欲しい情報はテンプレートの設定であり、これらの DEBUG メッセージには見当たらない。 テンプートの設定は Bean (Spring が生成を管理するオブジェクト) で行われると Tutorial: Thymeleaf + Spring に記載されている。 Bean の情報を取得してテンプレートのデフォルト設定を確認する。

Bean の情報を取得するために、main 関数に処理を加える。 SPRING INITIALIZER で作成したプロジェクトでは、ルートパッケージに XXXApplication.kt が作成され、main 関数が定義される。 下記のように記述することで Bean 名の一覧を取得できる。

fun main(args: Array<String>) {
    val ctx = SpringApplication.run(AskRequestApplication::class.java, *args)

    ctx.beanDefinitionNames.sorted().map {
        println(it)
    }
}

bootRun で起動すると Terminal に Bean 名の一覧が表示される。 template に関する記述を探すと defaultTemplateResolver が見つかる。

defaultTemplateResolver の詳細を確認する。 main 関数に下記の処理を加えて Terminal の表示を確認すると、 SpringResourceTemplateResolver クラスのインスタンスが取得されている。

fun main(args: Array<String>) {
    val ctx = SpringApplication.run(AskRequestApplication::class.java, *args)
    
    ...

    val bean = ctx.getBean("defaultTemplateResolver")
    println(bean.toString())
}

クラスを特定できたので、インスタンスが保持している情報をメソッドで取り出す。 main 関数に下記の処理を加えて Terminal の表示を確認する。

fun main(args: Array<String>) {
    val ctx = SpringApplication.run(AskRequestApplication::class.java, *args)

    ...

    val bean = ctx.getBean("defaultTemplateResolver") as SpringResourceTemplateResolver
    bean.initialize()
    println(bean.prefix)
    println(bean.suffix)
    println(bean.templateMode)
    println(bean.isCacheable)
}

defaultTemplateResolver の設定は下記の通り。

  • prefix : classpath:/templates/
  • suffix : .html
  • templateMode : HTML5
  • isCacheable : false

templateMode は HTML5 であり XHTML ではない。 Tutorial: Using Thymeleaf (ja) によると、 HTML5 モードは整形式 XML と書かれている。 もし、整形式 XML を使いたくなければ LEGACYHTML5 モードを使用する。

LEGACYHTML5 モードに設定を変更したい場合には、templateResolver の Bean 設定を行う。 Bean の設定は、@Configuration を付与したクラスで行う。 Tutorial: Thymeleaf + Spring では XML で記載されているが Spring Boot の慣習に則りアノテーションを使う。 SPRING INITIALIZER で作成したプロジェクトでは、ルートパッケージに XXXApplication.kt が作成され、@SpringBootApplication が付与されている。 @SpringBootApplication は @Configuration, @EnableAutoConfiguration, @ComponentScan をまとめて付与する。 つまり、XXXApplication.kt で Bean の設定を行えば良いということ。

下記は templateResolver の Bean 設定。

@SpringBootApplication
open class AskRequestApplication {

    @Bean
    open fun templateResolver() = SpringResourceTemplateResolver().apply {
        prefix = "classpath:/templates/"
        suffix = ".html"
        templateMode = "LEGACYHTML5"
        isCacheable = false // TODO: make it true before release
    }
}

ブラウザで確認をすると下記のようなエラーが表示される。

Cannot perform conversion to XML from legacy HTML: The nekoHTML library is not in classpath. nekoHTML 1.9.15 or newer is required for processing templates in "LEGACYHTML5" mode http://nekohtml.sourceforge.net. Maven spec: "net.sourceforge.nekohtml::nekohtml::1.9.15". IMPORTANT: DO NOT use versions of nekoHTML older than 1.9.15.

LEGACYHTML5 を使うには nekoHTML が必要とのことなので build.gradle に下記を追加。

dependencies {
    ...
    compile('net.sourceforge.nekohtml:nekohtml:1.9.15') // for LEGACY HTML5 template
    ...
}

以上の設定を加えることで閉じタグを省略できるようになる。

Kotlin 補足

下記の Kotlin コードと Java コードは同じ。

    open fun templateResolver() = SpringResourceTemplateResolver().apply {
        prefix = "classpath:/templates/"
        suffix = ".html"
        templateMode = "LEGACYHTML5"
        isCacheable = false // TODO: make it true before release
    }
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setPrefix("classpath:/templates/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode("LEGACYHTML5");
        resolver.setIsCacheable(false); // TODO: make it true before release
        return resolver;
    }