Thymeleafのプロパティアクセスのバグ取りはどう行う?ControllerやDAOのデバッグとの違いを新人エンジニア向けに解説
こんにちは。ゆうせいです。
今回は、Thymeleafのプロパティアクセスで起きるバグを、どのように調べればよいかを解説します。
たしかに、ControllerやDAOであれば、EclipseやIntelliJ IDEAのデバッガでブレークポイントを置けます。
変数の中身も見られます。
SQLの結果も追いやすいです。
一方、ThymeleafのHTMLテンプレートでは、Javaコードのように1行ずつステップ実行する感覚でデバッグしにくいですよね。
そのため、新人エンジニアは「Controllerまでは値が来ているのに、画面で表示できない」「th:fieldやth:textでエラーになる」「プロパティが見つからないと言われる」といった場面でつまずきやすいです。
結論から言うと、Thymeleafのプロパティアクセスのバグ取りは、次の順番で行うのが良いです。
| 順番 | 確認する場所 | 見ること |
|---|---|---|
| 1 | Controller | Modelに正しい名前で値を入れているか |
| 2 | DTO・Formクラス | フィールド名、getter、setterが正しいか |
| 3 | Thymeleaf | th:object、th:field、th:textの参照名が正しいか |
| 4 | 一時表示 | 画面に値を直接出して中身を確認する |
| 5 | ログ | ControllerでModelに入れる直前の値を出す |
| 6 | 例外メッセージ | どのプロパティが見つからないか読む |
Thymeleafのバグ取りでは、「HTMLを直接デバッガで追う」のではなく、「HTMLに渡す前のデータ」と「HTMLで参照している名前」を照合するのが基本です。
たとえるなら、宅配便の荷物が届かないときに、配達トラックだけを見るのではなく、宛名、住所、部屋番号、荷物の中身を順番に確認するようなものです。
Thymeleafのプロパティアクセスとは何か
まず、プロパティアクセスとは何かを整理しましょう。
Thymeleafでは、ControllerからModelに入れたオブジェクトのプロパティを、HTML側で参照できます。
たとえば、Controllerで次のように書いたとします。
model.addAttribute("car", carDto);
この場合、Thymeleaf側では次のように参照できます。
<p th:text="${car.name}"></p>
この${car.name}が、プロパティアクセスです。
意味は、「Modelの中にあるcarという名前のオブジェクトから、nameプロパティを取り出す」です。
Javaの感覚で言えば、次のような処理に近いです。
car.getName()Thymeleafの${car.name}は、直接フィールドnameを見ているというより、基本的にはJavaBeansのルールに従ってgetName()のようなgetterを探して値を取得します。
そのため、DTOやFormクラスにgetterがないと、Thymeleafから値を取り出せないことがあります。
よくあるエラー1:Model名とHTML側の名前が違う
最も多い原因の1つが、ControllerでModelに入れた名前と、HTML側で参照している名前が違うケースです。
Controller側です。
@GetMapping("/cars/detail")
public String detail(Model model) {
CarDto carDto = new CarDto();
carDto.setCarId(1);
carDto.setName("プリウス");
carDto.setPrice(2500000);
model.addAttribute("car", carDto);
return "cars/detail";
}このControllerでは、Modelにcarという名前で入れています。
正しいThymeleafです。
<p th:text="${car.name}"></p>
悪い例です。
Controllerではcarという名前なのに、HTMLではcarDtoを参照しています。
この場合、ThymeleafはcarDtoというModel属性を見つけられません。
| Controller | HTML | 結果 |
|---|---|---|
| model.addAttribute("car", carDto) | ${car.name} | 正しい |
| model.addAttribute("car", carDto) | ${carDto.name} | 名前が違うためNG |
新人エンジニアは、まずmodel.addAttributeの第1引数を見るクセをつけてください。
第1引数が、Thymeleafで使う名前です。
バグ取り方法1:ControllerでModelに入れる直前をログ出力する
Thymeleaf側で表示できないときは、まずControllerで値が入っているか確認します。
@GetMapping("/cars/detail")
public String detail(Model model) {
CarDto carDto = carsDao.findById(1);
System.out.println("carDto = " + carDto);
model.addAttribute("car", carDto);
return "cars/detail";
}CarDtoにtoString()を用意しておくと、確認が楽です。
public class CarDto {
private int carId;
private String name;
private int price;
public int getCarId() {
return carId;
}
public void setCarId(int carId) {
this.carId = carId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public String toString() {
return "CarDto{" +
"carId=" + carId +
", name='" + name + '\'' +
", price=" + price +
'}';
}
}コンソールに次のように出れば、Controllerまでは値が来ています。
carDto = CarDto{carId=1, name='プリウス', price=2500000}
この状態で画面に出ないなら、疑うべきはThymeleaf側の参照名、getter、th:object、th:fieldなどです。
逆に、ここでnullなら、ThymeleafではなくDAOやService側を疑います。
| Controllerログ | 疑う場所 |
|---|---|
| 値が入っている | Thymeleafの参照名、getter、HTML |
| nullになっている | DAO、Service、検索条件、DBデータ |
よくあるエラー2:getterがない
Thymeleafでプロパティを参照するには、getterが必要です。
悪い例です。
public class CarDto {
private String name;
public void setName(String name) {
this.name = name;
}
}このクラスにはgetName()がありません。
Thymeleafで次のように書いても、nameを取得できないことがあります。
<p th:text="${car.name}"></p>良い例です。
public class CarDto {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}Thymeleafの${car.name}は、JavaのgetName()を呼ぶ感覚だと考えてください。
フィールドがあるだけでは不十分な場合があります。
学校のロッカーでたとえるなら、nameという荷物が中にあっても、ロッカーを開ける鍵であるgetterがないと取り出せないようなものです。
よくあるエラー3:フィールド名とgetter名がずれている
JavaBeansの命名ルールに合っていないと、Thymeleafからうまく参照できないことがあります。
悪い例です。
public class CarDto {
private String carName;
public String getName() {
return carName;
}
public void setName(String carName) {
this.carName = carName;
}
}この場合、フィールド名はcarNameですが、getterはgetName()です。
Thymeleafでは、${car.name}ならgetName()に対応します。
しかし、${car.carName}と書くと、getCarName()を探すため、うまくいかない可能性があります。
良い例です。
public class CarDto {
private String carName;
public String getCarName() {
return carName;
}
public void setCarName(String carName) {
this.carName = carName;
}
}この場合、Thymeleaf側は次のように書きます。
<p th:text="${car.carName}"></p>大事なのは、Thymeleafのプロパティ名とgetter名の対応です。
| Thymeleaf | 探されるgetter |
|---|---|
| ${car.name} | getName() |
| ${car.carName} | getCarName() |
| ${car.price} | getPrice() |
| ${car.deletedAt} | getDeletedAt() |
バグ取り方法2:Thymeleafで一時的に値を直接表示する
Thymeleafのバグ取りでは、HTML内に一時的なデバッグ表示を入れる方法が有効です。
たとえば、次のように書きます。
<div style="border: 1px solid red; padding: 10px;">
<p>デバッグ表示</p>
<p th:text="${car}"></p>
<p th:text="${car.name}"></p>
<p th:text="${car.price}"></p>
</div>これにより、画面上でcar全体やcar.name、car.priceが見えるか確認できます。
もし${car}は表示できるのに${car.name}でエラーになるなら、car自体はModelにあります。
問題はnameプロパティです。
| 表示結果 | 考えられる原因 |
|---|---|
| ${car}も表示できない | Modelにcarが入っていない、名前が違う |
| ${car}は表示できるが${car.name}がNG | getName()がない、プロパティ名が違う |
| ${car.name}は表示できるがth:fieldがNG | th:objectやフォームバインドの問題 |
この方法は、Javaのデバッガほどきれいではありません。
しかし、Thymeleafではかなり実践的です。
紙の地図に赤ペンで印をつけるように、まず画面にデータが来ているか見える化しましょう。
よくあるエラー4:リスト表示で変数名を間違える
一覧画面では、th:eachでリストを回します。
Controllerです。
@GetMapping("/cars")
public String list(Model model) {
List<CarDto> carList = carsDao.findAll();
model.addAttribute("carList", carList);
return "cars/list";
}HTMLです。
<table>
<tr th:each="car : ${carList}">
<td th:text="${car.carId}"></td>
<td th:text="${car.name}"></td>
<td th:text="${car.price}"></td>
</tr>
</table>ここでのポイントは、${carList}とcarの違いです。
| 名前 | 意味 |
|---|---|
| carList | ControllerからModelに入れたリスト全体 |
| car | th:eachで1件ずつ取り出した変数 |
悪い例です。
<tr th:each="car : ${cars}">
<td th:text="${car.name}"></td>
</tr>ControllerではcarListという名前で入れているのに、HTMLではcarsを参照しています。
この場合、リストが見つかりません。
もう1つの悪い例です。
<tr th:each="car : ${carList}">
<td th:text="${carList.name}"></td>
</tr>carListはリスト全体です。
1件の車ではありません。
1件ずつのnameを表示したいなら、th:eachで定義したcarを使います。
<td th:text="${car.name}"></td>
お弁当箱でたとえるなら、carListは弁当箱全体です。
carは、その中の1つのおかずです。
おかずの名前を知りたいのに、弁当箱全体に「あなたの名前は?」と聞いても困りますよね。
バグ取り方法4:リストの件数を表示する
リスト表示で何も出ないときは、まず件数を表示しましょう。
<p th:text="${#lists.size(carList)}"></p>
これで件数が表示されれば、リストはModelに入っています。
0なら、DAOや検索条件の問題かもしれません。
エラーになるなら、carListという名前が間違っている可能性があります。
| 表示結果 | 考えられる原因 |
|---|---|
| 3などの件数が表示される | リストはModelに入っている |
| 0が表示される | リストはあるが中身が空 |
| エラーになる | carListが存在しない、名前が違う |
件数確認はかなり便利です。
画面に出ない理由が、「データがない」のか「参照名が違う」のかを分けられます。
よくあるエラー5:ネストしたプロパティがnull
Thymeleafでは、ネストしたプロパティを参照することがあります。
<p th:text="${user.address.city}"></p>この場合、user、address、cityの順に参照します。
もしuserは存在していても、addressがnullなら、cityに進めません。
| 参照 | 必要なもの |
|---|---|
| ${user.address.city} | userがnullでない |
| ${user.address.city} | user.getAddress()がnullでない |
| ${user.address.city} | address.getCity()が存在する |
この場合は、Controller側でaddressが入っているか確認します。
System.out.println("user = " + user);
System.out.println("address = " + user.getAddress());また、画面側では一時的に分けて確認します。
<p th:text="${user}"></p>
<p th:text="${user.address}"></p>
<p th:text="${user.address.city}"></p>一気に深いプロパティを見ると、どこでnullなのかわかりにくいです。
階段を1段ずつ上がるように、user、address、cityの順に確認してください。
バグ取り方法5:例外メッセージを読む
Thymeleafのエラーでは、長い例外メッセージが出ることがあります。
新人エンジニアは、長いログを見ると圧倒されるかもしれません。
しかし、全部を読む必要はありません。
まず、次のキーワードを探してください。
Exception evaluating SpringEL expression
Property or field 'name' cannot be found
Neither BindingResult nor plain target object for bean nameたとえば、次のようなエラーです。
Property or field 'name' cannot be found on object of type 'com.example.demo.form.CarForm'
このエラーは、「CarFormの中にnameというプロパティが見つかりません」という意味です。
確認すべき点は次のとおりです。
| 確認項目 | 内容 |
|---|---|
| フィールド | private String name; があるか |
| getter | getName() があるか |
| setter | setName(String name) があるか |
| HTML | *{name} や ${carForm.name} のスペルが正しいか |
Thymeleafのバグ取り用チェックリスト
プロパティアクセスで困ったら、次の順番で確認しましょう。
| 番号 | チェック項目 | 確認例 |
|---|---|---|
| 1 | Controllerが正しいテンプレートを返しているか | return "cars/detail"; |
| 2 | Model名がHTMLと一致しているか | carなのかcarDtoなのか |
| 3 | DTOにgetterがあるか | getName()があるか |
| 4 | プロパティ名のスペルが正しいか | deletedAtとdeleteAtを間違えていないか |
| 5 | リスト名と1件変数名を混同していないか | carListとcarの違い |
| 6 | nullになっていないか | ネストしたプロパティを1段ずつ確認 |
| 7 | Controllerログで値を確認したか | System.out.printlnやlogger |
| 8 | 一時的にth:textで表示したか | ${car}、${car.name} |
このチェックリストを使うと、かなりのThymeleafエラーは切り分けできます。
ControllerやDAOのデバッガとThymeleafデバッグの違い
ControllerやDAOのバグ取りでは、デバッガが強力です。
ブレークポイントを置き、変数を見て、ステップ実行できます。
一方、Thymeleafはテンプレートエンジンです。
HTMLをサーバー側で処理し、最終的なHTMLを生成します。
そのため、Javaコードのようにテンプレートの中を1行ずつ気軽に追うというより、次のような見方になります。
| 場所 | デバッグ方法 |
|---|---|
| Controller | デバッガ、ログ、Model確認 |
| DAO | デバッガ、SQLログ、DB確認 |
| DTO・Form | getter、setter、toString確認 |
| Thymeleaf | 一時表示、例外メッセージ、HTML上の参照名確認 |
| ブラウザ | 表示結果、HTMLソース、開発者ツール確認 |
Thymeleafのデバッグでは、「Modelに何が入っているか」「テンプレートが何を探しているか」を突き合わせるのが基本です。
探偵のように、足跡を追うイメージです。
おすすめの実務的なバグ取り手順
実務では、次の順番で進めると効率が良いです。
手順1:Controllerの最後でModelに入れる値を確認する
System.out.println("carForm = " + carForm);
System.out.println("carList = " + carList);
model.addAttribute("carForm", carForm);
model.addAttribute("carList", carList);まず、Controllerが画面に渡す値を確認します。
ここで値がなければ、Thymeleafを見る前にController、Service、DAOを直します。
手順2:HTMLでModel名だけ表示する
<p th:text="${carForm}"></p>この表示ができるか確認します。
できなければ、Model名が間違っています。
手順3:プロパティを1つずつ表示する
<p th:text="${carForm.name}"></p>
<p th:text="${carForm.price}"></p>1つずつ確認します。
複数プロパティを一気に直そうとすると、原因がぼやけます。
手順4:一時デバッグ表示を消す
確認が終わったら、一時的に入れたデバッグ表示は必ず消しましょう。
<p th:text="${carForm}"></p>このような表示を残したまま本番に出すと、ユーザーに内部情報が見えてしまう可能性があります。
デバッグ用の赤ペンは、提出前に消す!
ログ出力はSystem.out.printlnよりloggerが望ましい
学習段階ではSystem.out.printlnでも構いません。
ただし、実務ではloggerを使うことが多いです。
package com.example.demo.controller;
import com.example.demo.form.CarForm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class CarController {
private static final Logger logger =
LoggerFactory.getLogger(CarController.class);
@GetMapping("/cars/new")
public String showForm(Model model) {
CarForm carForm = new CarForm();
carForm.setName("プリウス");
carForm.setPrice(2500000);
logger.debug("carForm = {}", carForm);
model.addAttribute("carForm", carForm);
return "cars/new";
}
}loggerを使うと、開発環境では詳細ログを出し、本番環境では抑えるといった制御がしやすくなります。
ただし、パスワードや個人情報をログに出してはいけません。
| ログに出してよい例 | ログに出してはいけない例 |
|---|---|
| DTOのIDや件数 | パスワード |
| 検索条件の一部 | クレジットカード番号 |
| リスト件数 | 個人番号、秘密トークン |
| 画面遷移先 | 認証情報 |
ブラウザの開発者ツールも使う
Thymeleafはサーバー側でHTMLを生成します。
ブラウザに届くころには、th:textやth:fieldは処理済みです。
そのため、ブラウザの開発者ツールで最終的なHTMLを見ることも役立ちます。
たとえば、次のThymeleafを書いたとします。
<input type="text" th:field="*{name}">ブラウザに届くHTMLでは、次のようになります。
<input type="text" id="name" name="name" value="プリウス">
開発者ツールで、name属性やvalue属性がどうなっているか確認できます。
| 確認するもの | 見るポイント |
|---|---|
| HTMLソース | valueが入っているか |
| name属性 | フォーム送信時のパラメータ名が正しいか |
| id属性 | labelやJavaScriptと対応しているか |
| 画面表示 | 期待した文字が出ているか |
Controller、Thymeleaf、ブラウザの3か所で見ると、原因を絞りやすくなります。
よくあるプロパティ名のミス一覧
| ミス | 例 | 正しい考え方 |
|---|---|---|
| 大文字小文字の違い | ${car.carid} | ${car.carId} |
| DTO名とModel名の混同 | ${carDto.name} | Model名がcarなら${car.name} |
| リスト名と1件名の混同 | ${carList.name} | th:each内では${car.name} |
| DBカラム名で書いてしまう | ${car.car_id} | Javaプロパティ名の${car.carId} |
| getter名と合っていない | ${car.carName} | getCarName()が必要 |
| Form名の不一致 | th:object="${form}" | ModelにcarFormなら${carForm} |
特に多いのが、DBカラム名とJavaプロパティ名の混同です。
DBではcar_idでも、JavaではcarIdにすることが多いです。
private int carId;
public int getCarId() {
return carId;
}この場合、Thymeleafでは次のように書きます。
<p th:text="${car.carId}"></p>DBのカラム名に引っ張られて、次のように書かないようにしましょう。
<p th:text="${car.car_id}"></p>Thymeleafでデバッグしやすい設計にする
Thymeleafのバグを減らすには、設計も大切です。
| 工夫 | 理由 |
|---|---|
| Model名をわかりやすくする | HTML側で迷いにくい |
| FormとDTOを分ける | 画面入力とDBデータの役割が明確になる |
| getter、setterを必ず用意する | プロパティアクセスしやすくなる |
| toStringを用意する | ログ確認しやすくなる |
| リスト名はListで終える | carListのように複数形だとわかりやすい |
| 1件変数は単数名にする | th:each="car : ${carList}"が読みやすい |
たとえば、次の命名はわかりやすいです。
model.addAttribute("carForm", carForm);
model.addAttribute("carList", carList);
model.addAttribute("car", carDto);逆に、次のような命名は混乱しやすいです。
model.addAttribute("data", carForm);
model.addAttribute("list", carList);
model.addAttribute("dto", carDto);dataやlistだけでは、HTML側で何のデータなのかわかりにくくなります。
名前は、未来の自分へのメモです。
雑に付けると、あとで自分が困ります。
新人エンジニア向けのデバッグ練習
Thymeleafのバグ取りに慣れるには、あえて小さな画面で練習するとよいです。
たとえば、次の3つを順番に作ります。
| 練習 | 内容 |
|---|---|
| 1件表示 | CarDtoを1件Modelに入れてth:textで表示する |
| リスト表示 | List<CarDto>をth:eachで表示する |
| フォーム表示 | CarFormをth:objectとth:fieldで表示する |
この3つができると、Thymeleafのプロパティアクセスの基本はかなり身につきます。
まずはDBを使わず、Controllerの中で固定値を作って確認するのもおすすめです。
@GetMapping("/debug/car")
public String debugCar(Model model) {
CarDto car = new CarDto();
car.setCarId(1);
car.setName("プリウス");
car.setPrice(2500000);
model.addAttribute("car", car);
return "debug/car";
}DBやDAOが絡むと、原因が増えます。
最初は固定データでThymeleafだけを確認すると、切り分けしやすいです。
まとめ
Thymeleafのプロパティアクセスのバグ取りは、ControllerやDAOのようにデバッガだけで追うのではなく、Model、DTO・Form、HTMLテンプレート、ブラウザ表示を順番に確認するのが基本です。
| 確認場所 | 見ること |
|---|---|
| Controller | model.addAttributeの名前と値 |
| DTO・Form | フィールド名、getter、setter、toString |
| Thymeleaf | ${}、*{}、th:object、th:field、th:each |
| 画面 | 一時的なth:text表示 |
| ログ | 値がControllerまで来ているか |
| ブラウザ | 生成後のHTML、name属性、value属性 |
一言でまとめるなら、Thymeleafのバグ取りは「Modelに入れた名前」と「HTMLで参照する名前」と「Javaのgetter名」を照合する作業です。
画面に出ないときは、まずControllerでログを出し、次にHTMLで${car}や${car.name}を一時表示し、最後にth:objectやth:fieldへ戻してください。
新人エンジニアは、特に次の3つを覚えてください。
| 覚えること | 理由 |
|---|---|
| model.addAttributeの第1引数がHTML側の名前になる | ${car}のcarに対応するため |
| プロパティアクセスにはgetterが必要 | ${car.name}はgetName()に対応するため |
| th:eachではリスト名と1件名を分ける | carListとcarを混同しないため |
今後の学習では、まずModel、DTO、Form、JavaBeansのgetter、th:text、th:object、th:field、th:eachを順番に確認してください。そのうえで、ログ出力、ブラウザ開発者ツール、例外メッセージの読み方を練習すると、Thymeleafのバグ取りがかなり楽になります。Thymeleafは「見えない魔法」ではなく、Controllerから渡されたデータをHTMLに埋め込む仕組みです。名前合わせを丁寧に行いましょう!
セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。
投稿者プロフィール

- 代表取締役
-
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。
学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。

