JSP+JSTLからThymeleafへ乗り換える

[アソビュー Advent Calendar 2019](https://qiita.com/advent-calendar/2019/asoview)の7日目です。

バックエンド開発チームの茶色いネズミ、アズマです。
今回はJavaのWebアプリケーションで古くから使われていたJSP+JSTLの機能を、Thymeleafに置き換えるときの対応表をご紹介します。

# JSP＋JSTLからThymeleafへ乗り換える

JavaのWebアプリケーションを作る手法としてかつてはデファクトだったJSPでしたが、現在はSpringBootがThymeleafを採択したことにより、既存アプリケーションのリプレースに伴いプレゼンテーションをJSPからThymeleafへ置き換える[^2]事例もあります。

今回は旧来のJavaWebアプリケーションで使われるJSP+タグライブラリのJSTLで使われている機能をThymeleafへ置き換える方法を、機能の対応表とともに紹介します。

[^2]: 元々がサーブレットでHTMLを文字列で出力していたのを、HTMLを1つのテンプレートと見立ててJavaコードを記述できるようにしたのがJSPです。ただし、JSPの動作速度は芳しくなく、タグ内にタグを重ねて記述するため可読性が悪くなり、元のHTMLに対してJSPコードを追加するので画面デザインやレイアウトの修正が入るとHTMLとJSPの差分修正を行うなど、メンテナンス性は決してよくありませんでした。

## 今回対象とするもの

JSTLタグ以外でも、JSPやEL式の使い方に関するものも紹介します。

|名前空間|機能の概要|
|----|----|
|c|基本操作。値の出力や一時格納に関するもの。|
|fmt|出力フォーマットの整形|
|fn|出力加工用の関数|

## 対象外のもの

|名前空間|機能の概要|除外理由|
|----|----|-----|
|x|xml操作(XPathやXSLT)|Thymeleaf内でXML操作や要素の操作はしない|
|sql|データベース操作|Thymeleafからデータベースの参照はしない[^1]|

[^1]: Thymeleaf-Springプラグインを利用して、Spring管理Bean、特に`@Service`、`@Repository`を付与したクラスからデータソースを参照して出力する方法はある。ただし適切なトランザクション管理とリソースの確保を行い、必要に応じてキャッシュ化した内容を返すこと。

## Core (名前空間：c)

coreタグは値の出力や条件分岐など、利用頻度の高いタグです。

|JSTL|Thymeleaf|機能概要|
|----|---------|----|
|`<c:out>` |th:value、th:textなど、HTMLで出力する属性名にあわせた同じ名前の属性を提供。 |テンプレートに渡した値をHTMLで出力 |
|`<c:set>` |th:with |テンプレートの一時変数に値を格納 |
|`<c:remove>` |（対応属性なし） |一時変数やサーブレット属性から値を削除 |
|`<c:if>` |th:if |条件分岐の条件式を記述。 |
|`<c:choose>` |th:switch |後続の`<c:when>`またはth:case要素で条件を羅列する。|
|`<c:when>` |th:case |`<c:choose>`やth:switchの子要素 |
|`<c:forEach>` |th:each |ループして繰り返し出力する。ループの要素、ルール変数など一時変数をここで定義 |
|`<c:forToken>` |#strings.arraySplitなど |文字列を区切り文字で分割 |
|`<c:url>` |${#uris.**} |URLを生成する。URLエンコードも兼ねる。 |
|`<c:import>` |th:insert, th:replace, th:include |外部リソースを取り込んで表示する |
|`<c:redirect>` |（対応機能なし） |リダイレクトする |
|`<c:catch>` |（対応機能なし） |出力中に発生した例外のハンドリング |
|`<c:param>` |（URL用の記法で提供） |他JSTLタグのリクエストパラメータを生成する |

ではそれぞれの機能と利用例を、対比させながら紹介します。

### th:text : タグのテキスト（＝要素値）に出力する

#### 要素値へ出力

```html
<div>メッセージ</div>
```

これを表示するために、名前：message、値：メッセージ で渡した場合を比較してみます。

| thymeleaf | JSP/JSTL |
|:-:|:-:|
| `<div th:text="${message}">テキスト</div>`  | `<div><c:out value="${message}"/></div>`  |

Thymeleafでは、divの要素値にかかれた テキスト の文字が、th:textの内容で出力されます。
JSPでは `<c:out>`経由で出力しなくても表示されますが、出力する値をサニタイズするためJSTLの`<c:out>`を必ず経由します。

#### 属性値へ出力

```html
<input type="text" value="メッセージ" />
```

これを表示するために、名前：message、値：メッセージ で渡した場合の書き方は、

| thymeleaf | JSP/JSTL |
|:-:|:-:|
| `<input type="text" value="テキスト" th:value="${message}"/>`  | `<input type="text" value="<c:out value="${message}"/>" />`  |

Thymeleafの特徴でもある **出力したい属性と同じ属性名を指定する** と値が書き換わります。
一方JSP/JSTLではタグの属性値の中へ、さらにタグで囲んて値を出力する記述になるのでやや読みにくいでしょうか。

#### 文字列結合

Thymeleaf内の属性内にて、文字列結合も可能です。文字列を直接記載する場合はシングルクォートで囲います。

```html
<div th:text="'これは、' + ${message} + 'です'">テキスト</div>
```

これの出力結果は、

```html
<div>これは、メッセージです</div>
```

### th:with ： 一時変数に値を格納

HTMLを出力しているときに、出力値を繰り返し使う場合や、値を再代入して出力内容を変更できます。

| thymeleaf | JSP/JSTL |
|:-:|:-:|
| `<div th:with="another='${original}' + です">テキスト</div>`  | `<div><c:set value="${original}です" var="another"/></div>`  |

Thymeleafのテンプレート内で定義した一時変数は、その一時変数を定義した要素ならびに子要素で参照できます。
Thymeleafへ 名前：message、値：メッセージ で渡した場合、

```html
<div th:with="anotherMessage='これは、' + ${message} + 'です'" th:text="${anotherMessage}">テキスト</div>
```

これの出力結果は、

```html
<div>これは、メッセージです</div>
```

#### 一時変数を複数定義する

一時変数を複数定義したい場合は、カンマつなぎで記載します。複数の定義を記載しても、文字列加工や計算はもちろん可能です。

```html
<div th:with="i=3, anotherMessage=${message} + 'です'">
	<span th:text="${anotherMessage}">テキスト</span>
	<span th:text="${i}">テキスト</span>
</div>
```

これの出力結果は、

```html
<div>
	<span>メッセージです</span>
	<span>3</span>
</div>
```

#### 同一の変数名を定義する

同一の変数名に格納した場合はどうなるでしょうか？

```html
<div th:with="message=${message} + 'です'">
	<span th:text="${message}">テキスト1</span>
</div>
<div th:text="${message}">テキスト2</div>
```

この場合は、**th:withの要素内だけ** messageの代入が実行されます。

```html
<div>
	<span>メッセージです</span>
</div>
<div>メッセージ</div>
```

Thymeleafのth:withで一時的に定義した値は **その要素と子要素のみで有効** です。対してJSP+JSTLでは格納した内容を保持して、テキスト2の出力まで同じ内容になります。ここが大きく異なる点でしょう。
そのためJSPでは<c:remove>で値を削除して後続処理を続ける仕組みです。

他にもThymeleafで一時変数を扱う場合は、th:if や th:each のように条件分岐やループ処理などのように、後続の属性と組み合わせで使います。

### th:if 条件を満たしたときに、子要素を出力する＆後続のthymeleaf属性の処理を行う

特定の条件を満たすときのみ子要素の内容を出力または子要素のタグを実行します。これはJSP/JSTLとThymeleafとで変わりませんが、Thymeleafは th:if を定義する要素が出力されます。また、同じ要素内のthymeleaf属性(th:〇〇)が実行されます。

| thymeleaf | JSP/JSTL |
|:-:|:-:|
| `<div th:if="${original == 'メッセージ'}>`  | `<div><c:if test="${original == 'メッセージ'}"/></div>`  |

属性値に条件式を書く方法や書き方はほぼ同じものを使えます。

#### 条件式の基本系

Thymeleafへ 名前：message、値：メッセージ で渡した場合、

```html
<div th:if="${message == 'メッセージ'}">
	<span th:text="${message}">テキスト</span>
</div>
```

この出力結果は

```html
<div>
	<span>メッセージ</span>
</div>
```

#### 条件式にJavaコードを使う

th:ifの条件式にはさまざまなJavaコードを記載できます。最も使うのはJavaクラスのメソッドを呼び出すことで、条件式に組み込むことや、表示専用の値を出力させることも可能です。

```html
<div th:if="${message.startsWith('メッセ')}">
	<span th:text="${message}">テキスト</span>
</div>
```

変数messageはStringで メッセージ でしたから、startsWith()がtrueになるので、この出力結果は

```html
<div>
	<span>メッセージ</span>
</div>
```

となります。

条件に合致しないときの出力は、例えば以下は 変数 messageの文字列の長さが256以上か判断します。

```html
<div th:if="${message.length() >= 256}">
	<span th:text="${message}">テキスト</span>
</div>
```

もしこのif文の条件を満たさないときは、その要素ならびに子要素が丸ごと出力されません。つまりこの<div>要素は出力されません。

### 複数条件で分岐する th:switch th:case

switch～caseは、Javaのswitch文と同じで、一方の条件にあわなければ、他方の条件を検証します。条件に1つでも合致しなかった場合の条件は `th:case="*"` で表します。

```html
<div th:switch="${message}">
	<span th:case="メッセージ" th:text="${message}"></span>
	<span th:case="*">該当するメッセージはありませんでした</span>
</div>
```

このThymeleafテンプレートへ 名前：message、値：メッセージ で渡した場合は、最初の th:case に合致するので、以下のように出力されます。

```html
<div>
	<span>メッセージ</span>
	
</div>
```

出力されなかった要素は空行になります。
messageの値がメッセージ以外だった場合は、

```html
<div>
	
	<span>該当するメッセージはありませんでした</span>
</div>
```

th:caseは文字列以外にも、数値や列挙値(enum)も可能です。

```html
<div th:switch="${price}">
	<span th:case="100" th:text="${price}">値段</span>
	<span th:case="200" th:text="${price}">値段</span>
	<span th:case="300" th:text="${price}">値段</span>
	<span th:case="*" th:text="該当する値段はありません"></span>
</div>
```

このテンプレートに対し、priceの値が200の場合は、

```html
<div>

	<span>200</spam>


</div>
```

が表示され、例えば priceの値が150の場合は、末尾の th:case のタグが実行されます。

```html
<div>



	<span>該当する値段はありません</spam>
</div>
```

#### enum値を使う場合

以下の列挙型を定義されている場合、

```java
public enum Direction {
	North, South, East , West ;

	public String getValue() {
		return name();
	}
}
```

Thymeleafでの検証方法は、以下のように記述できます。

```html
<div th:switch="${direction.getValue()}">
	<span th:case="North" th:text="北"></span>
	<span th:case="East" th:text="東"></span>
	<span th:case="South" th:text="南"></span>
	<span th:case="West" th:text="西"></span>
	<span th:case="*" th:text="該当なし"></span>
</div>
```

このテンプレートに対し、名前 direction 、値：Direction.East を指定した場合は、`<span th:case="East" th:text="東"></span>` の内容が出力されます。

### 繰り返し出力 th:each

`<table>`を使った表の出力や、`<select>`と`<option>`を使ったリストボックスなど、要素を繰り返し出力する方法には、JSTLとThymeleafでは記述法に違いがあります。

|JSTL|Thymeleaf|概要|
|:---|:---|:---|
|`<c:forEach>`|`th:each`|繰り返し出力を宣言|

JSTLを使ってリストボックスを出力するには、繰り返し出力をしたい要素に対して、`<c:forEach>`で囲みます。

```html
<select>
  <c:forEach var="result" items="${list}">
    <option value="<c:out value="${result.value}" />"><c:out value="${result.label}" /></option>
  </c:forEach>
</select>
```

Thymeleafは、繰り返し出力したい要素へ直接 `th:each` を記述します。タグの階層を変えることなく記述できます。

```html
<select>
  <option th:each="result : ${list}" th:value="${result.value}" th:inline="text">[[${result.label}]]</option>
</select>
```

## Format (名前空間：fmt)

出力フォーマットを定義する関数です。

|JSTL|Thymeleaf|タグ機能の概要|
|----|---------|----|
|`<fmt:formatNumber>` |${#numbers.formatInteger(..)} |数値、金額用フォーマット|
|`<fmt:formatDate>` |${#dates.format(..)} |日付フォーマット |
|`<fmt:parseNumber>` |※${#conversions.convert(object, targetClass)} |文字列を数値へ変換 |
|`<fmt:parseDate>` |※${#conversions.convert(object, targetClass)}  |文字列を日付へ変換 |
|`<fmt:setBundle>` |（なし） |リソースバンドルを設定 |
|`<fmt:bundle>` |（なし） |リソースバンドルの取得 |
|`<fmt:message>` |#{messageId} |リソースバンドルから指定したメッセージを出力 |
|`<fmt:requestEncoding>` |（なし） |リクエストの文字エンコーディングを変更する。 |
|`<fmt:setLocale>` |（なし） |ロケールを設定。この宣言以降のロケールが変更される。|
|`<fmt:setTimeZone>` |（なし） |タイムゾーンを設定。この宣言以降のタイムゾーンが切り替わる。 |
|`<fmt:timeZone>` |（なし） |タイムゾーンを返す。 |

fmtは値のフォーマットを定義する以外に、ロケールやタイムゾーンを同じJSPファイル内で切り替えることが可能です。
これによりテンプレート出力中に変数の型を変更したり、メッセージに出力する言語やプロパティファイルを切り替える機能が提供されていますが、これらは1つのファイル内で切り替えるよりも、ロケール別にJSPを切り替える方がメンテナンスしやすく、言語ごとに画面のレイアウトを変える方式を採用することが多いでしょう。

対して、Thymeleafでは文字列や金額、日付のフォーマット変換が多様に用意されています。ここでは一部を紹介します。

|Thymeleaf|概要|
|---|---|
|`${#dates.format(date, 'dd/MMM/yyyy HH:mm')}`|日付を指定したフォーマットで出力|
|`${#numbers.formatInteger(num,3)}`|最低表示桁数を指定して表示する|
|`${#numbers.formatPercent(num)}`|パーセント表記|
|`${#numbers.sequence(from,to)}`|連続する数値|

なおThymeleafでは、テンプレート出力中に変数の型を変換する convertions 関数を利用して別の型にすることはできますが、テンプレートでは型の値を出力することにとどめ、テンプレート内で不用意に変換する処理を書くのは可読性の低下を招きますので避けるべきです。

## Function (名前空間：fn)

文字列の操作を行う関数たちです。
Thymeleafでは変数がString型ならメソッドをそのまま記述すれば実行されますが、JSTLに用意されていた機能は`#string`関数にすべて用意されています。

|JSTL|Thymeleaf|タグ機能の概要|
|----|---------|----|
|`<fn:contains>` |`${#strings.contains(name,'ez')}` |指定した文字列が含まれている場合はtrue |
|`<fn:containsIgnoreCase>` |`${#strings.containsIgnoreCase(name,'ez')} ` |大文字小文字を無視して、指定した文字列が含まれている場合はtrue |
|`<fn:indexOf>` |`${#strings.indexOf(name,frag)} ` |指定した文字列の位置を返す |
|`<fn:startsWith>` |`${#strings.startsWith(name,'Don')}` |指定した文字列から開始した場合はtrue |
|`<fn:endsWith>` |`${#strings.endsWith(name,endingFragment)} ` |指定した文字列が末尾にある場合はtrue |
|`<fn:trim>` |`${#strings.trim(str)} ` |文字列の前後にある半角スペースを取り除く |
|`<fn:join>` |`${#strings.arrayJoin(namesArray,',')}` または `${#strings.listJoin(namesList,',')}` |文字列を結合する |
|`<fn:replace>` |`${#strings.replace(name,'las','ler')} ` |文字列を置き換える |
|`<fn:split>` |`${#strings.arraySplit(namesStr,',')}` または `${#strings.listSplit(namesStr,',')}` |指定した文字で分割する |
|`<fn:length>` |`${#strings.length(str)}` |文字列の長さを返す |
|`<fn:substring>` |`${#strings.substring(name,3,5)} ` |指定した文字列で切り取る |
|`<fn:substringAfter>` |`${#strings.substringAfter(name,prefix)}` |指定した位置以降で文字列を切り取る |
|`<fn:substringBefore>` |`${#strings.substringBefore(name,suffix)}` |指定した位置までで文字列を切り取る |
|`<fn:lowerCase>` |`${#strings.toLowerCase(name)}` |小文字にする |
|`<fn:upperCase>` |`${#strings.toUpperCase(name)}` |大文字にする |
|`<fn:escapeXml>` |`${#strings.escapeXml(str)} ` |HTMLエスケープを行う |
|(なし) |`${#strings.equals(first, second)}` |文字列が等しければtrue |
|(なし) |`${#strings.equalsIgnoreCase(first, second)}` |大文字小文字を無視して、文字列が等しければtrue  |
|(なし) |`${#strings.randomAlphanumeric(count)}` |指定した文字数で、英数のランダムな文字列を返す |


## JSPの暗黙オブジェクトとスコープ

JSPはサーブレットがもつ属性のスコープ[^3]＋JSP独自の領域を持っていますが、Thymeleafでもこの「スコープ」を指定して参照できます。

|JSPの暗黙オブジェクト|Thymeleafでの記述|
|---|---|
|request|#request|
|session|#session|
|application|#servletContext|
|page|(なし)|
|config|(なし)|

Thymeleafの設定に関する取得方法は別途あり、#execInfo で参照します。

[^3]: 変数を格納するときの寿命を定めたのがスコープ。リクエスト属性、セッション属性、アプリケーション属性（ないしはサーブレットコンテキスト属性）、JSPはさらにページコンテキスト属性の4つ。SpringMVCではフラッシュ属性が別途存在する。

## 算術式・検証式

Thymeleafでもほぼ同じものが利用できます。

|JSP・EL|Thymeleaf|処理の概要|
|---|---|---|
|`+` |`+` |和 |
|`-` |`-` |差 |
|`*` |`*` |積 |
|`/ div`|`/ div` |徐 |
|`% mod` |`% mod` |余り |
|`== eq` |`== eq` |等価 |
|`!= ne` |`!= ne` |等しくない |
|`< lt` |`< lt` |左辺 < 右辺 |
|`> gt` |`> gt` |左辺 > 右辺 |
|`<= le` |`<= le` |左辺 <= 右辺 |
|`>= ge` |`>= ge` |左辺 >= 右辺 |
|`& and` |`&` |論理積(AND) |
|`pipe or ｜` |｜ |論理和(OR) |
|`! not` |`! not` |否定(NOT) |
|`empty` |(なし) |空 |

## 例外ハンドリング

JSPではテンプレート出力中に何らかの実行時エラーがあった場合は、エラー専用ページに遷移する設定をJSPディレクティブに宣言でき、または（あまり推奨しませんが）スクリプトレット内で try～catch を行い、例外をキャッチして例外処理も実装できました。

Thymeleafでは例外ハンドリングを実装できません。エラー用のレスポンスが出力されるだけですので、例外ハンドリングはThymeleafテンプレートを呼び出すクラスで実装します。

## おわりに
以上、駆け足気味でしたが、JSP+JSTLの実装をThymeleafに置き換える一助になれば幸いです。