Spring MVCの@ModelAttributeとThymeleaf 側の th:object について

Spring MVC でリクエストパラメータをBeanに割り当てる際に @ModelAttribute を使うのですが、GETリクエストの場合はパラメータがないときもあるから @ModelAttribute が不要だと思っていたのですが、そういうわけではないと理解したのでそれについて書いてみました。

サンプルプログラムを作りましたので、そのソースコードを見ながら説明します。

まずサンプルプログラムの動きからです。サンプルプログラムは入力した値をチェックして問題なければチェックOKの画面に遷移、チェックNGであれば元の画面のままエラーメッセージの表示をする、というものです。

初回アクセス時の画面です。

名前、メール、年齢、メモに値を入力をします。各項目はSpringのバリデーションでチェックをしています。

チェックボタンを押下します。入力値に問題がなければチェックOKの画面に遷移します。

SpringのバリデーションでチェックNGとなった場合です。元の画面に入力値とエラーメッセージを表示します。

サンプルプログラムは Spring Boot で作成しています。プロジェクトは Spring Initializr で作成しました。Spring Initializr のURLは https://start.spring.io/ です。

Group、Artifact、Name は以下としました。

Group : com.example
Artifact : demo11
Name : demo11

ビルドは Gradle です。Dependencies には Spring Web、Thymeleaf、Validation を選択します。プロジェクトをダウンロードします。

今回の Spring Boot のサンプルプログラムでは、コントローラー、データ格納用のBean、バリデーションのエラーメッセージ、HTMLファイルを作成します。HTMLファイルは入力フォームとチェックOKの表示をするものの2つを作成します。

コントローラー
FormController.java

データ格納用のBean
Person.java

エラーメッセージ
ValidationMessages.properties

HTMLファイル
form.html
checkok.html

プロジェクト内のファイルの構成は、このようになります。赤字が追加・修正をするファイルです。

データ格納用のBeanから説明します。

Beanのソースは Person.java です。テキストファイルの画像なので、あとでコピペ用のソースを載せておきます。


Personが保持する情報は 名前(name)、メール(mail)、年齢(age)、メモ(memo)です。Springのバリデーションを利用するため各フィールドにアノテーションを付けています。

@NotBlank
未入力を許可しない。半角スペースも不可。

@Email
Email形式であるかのチェック。

@Min(0)
@Max(200)
値の範囲が0〜200の範囲内かのチェック。

バリデーションでチェックNGとなった場合のエラーメッセージは ValidationMessages.properties に記載をします。今回は以下のように設定しています。


jakarta.validation.constraints.NotBlank.message = 何か値を入力してください。
jakarta.validation.constraints.Email.message = メールアドレスの書式が違います。
jakarta.validation.constraints.Min.message = {value}より大きくしてください。
jakarta.validation.constraints.Max.message = {value}より小さくしてください。

HTMLファイルの説明をします。

入力用のフォームの方からです。入力用のフォームは form.html です。

formタグに th:object を付与しています。また各項目のvalueの設定には *{ ... } で値を設定しています。Thymeleafで値を設定する際は ${ ... } を利用しますので異なる書き方です。これについては後述します。

入力項目のチェックNGは以下で判断しています。

#fields.hasErrors( '...' )

シングルクォートで囲まれた部分にはバリデーションチェックをするBeanの属性を指定します。チェックNGであれば hasErrors() がtrueとなります。Thymeleafのifの判定を見た場合、 th:if="${ ... }" としてはtrueと判断され th:errors の内容が表示されます。th:errors はエラーメッセージの表示です。

バリデーションでチェックOKだった場合に出力するHTMLファイルの方です。ファイルは checkok.html です。

コントローラーから受け取った結果を表示しています。 ${ ... } で値を設定します。

コントローラーの説明です。

コントローラーは FormController.java です。@RequestMapping でGETとPOSTの時に処理するメソッドを指定しています。GET時が form01()、POST時が form02() です。メソッド名を同じにしてもよいのですが説明のために変えています。

GET時の form01() のほうですが、@ModelAttribute でリクエストされたパラメータをBeanであるpersonに割り当てています。実際にはこのときのリクエストパラメータは空なのでpersonには何も値は入らないです。ModelAndView のmavに必要な値を設定して form.html をコントローラーが呼び出します。

このときにふと疑問に思ったのが一番最初に書いた @ModelAttribute は不要ではないのか?ということです。ですので、form01() の引数を以下にしても良いのでは?と思いました。


public ModelAndView form01( @ModelAttribute("fm") Person person, ModelAndView mav) { ... }

↓ ↓ ↓


public ModelAndView form01( ModelAndView mav) { ... }

ですが、実際にやってみるとうまくいきません。エラーになります。Spring Boot は起動しますが Whitelabel Error Page になります。

どこでエラーになっているかというと form.html の *{ ... } のところです。

*{ ... } ですが、これは th:object がある前提で動くもののようです。nameを例にとると *{name} ${fm.name} と同じ意味になります。PersonのBeanが form.html に渡ってきていないためエラーになっています。

そうだとするとですが、@ModelAttribute("fm") Person person を form01() の引数に付与すると、なぜうまくいくのか?です。form01() ではリクエストパメータを @ModelAttribute で受け取りpersonに割り当ててはいるものの、ModelAndView のmavには何も設定していないからです。

ここのからくりですが、実はSpringが自動でやってくれているもののようです。以下の記載をしなくてもSpringが裏でやってくれているとのことです。

mav.addObject("fm", person);

そのため form.html にはオブジェクトが渡ってきており *{ ... } で値を設定する際も問題にならないようです。

ちなみに form.html には th:errors="*{ ... }" の記載もあります。推測が入りますが、エラーメッセージも( BindingResult も)裏でSpringが form.html に渡しているのだと思います。

POST時のform02() のほうです。

POST時はリクエストパラメータがありますので @ModelAttribute でpersonに割り当てます。バリデーションのチェックNGがあれば BindingResult のresultで判断をします。

form02() でも同じことですが、personのaddObjectの記載をしなくてもSpirngが裏でmavにaddObjectをしてくれているため form.html でも checkok.html でもpersonの属性を取り出すことができます。

Spring Boot の起動ですが gradlew を使用します。引数に bootRun を付与することで実行ができます。以下は Spring Boot を起動させたときのものです。

$ ./gradlew bootRun

ウェブブラウザから http://localhost:8080/validate にアクセスをするとサンプルプログラムが動きます。

使用したソースを以下に記載しておきます。

データ格納用のBean


package com.example.demo11;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Max;

public class Person {

	@NotBlank
	private String name;

	@Email
	private String mail;

	@Min(0)
	@Max(200)
	private int age;

	private String memo;

	public String getName() {
		return name;
	}

	public String getMail() {
		return mail;
	}

	public int getAge() {
		return age;
	}

	public String getMemo() {
		return memo;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setMail(String mail) {
		this.mail = mail;
	}

	public void setAge(int age) {
		this.age = age;
	}

	public void setMemo(String memo) {
		this.memo = memo;
	}

}

コントローラー


package com.example.demo11;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;

@Controller
public class FormController {

	@RequestMapping("/validate")
	public ModelAndView form01( @ModelAttribute("fm") Person person, ModelAndView mav) {

		mav.addObject("title", "Demo 11 page.");
		mav.addObject("msg", "Validated Check.");
		mav.setViewName("form");
		return mav;
		
	}

	@RequestMapping( value = "/validate", method = RequestMethod.POST )
	public ModelAndView form02( @ModelAttribute("fm")  @Validated Person person, 
					BindingResult result, ModelAndView mav ) {

		mav.addObject("title", "Demo 11 page.");

		if ( !result.hasErrors() ) {
			mav.addObject("msg", "Validated Check OK!");
			mav.setViewName("checkok");
		} else {
			mav.addObject("msg", "Validated Check NG..");
			mav.setViewName("form");
		}

		return mav;
	}

}

HTMLファイル


<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Demo 11</title>
</head>
<body bgcolor="#f5f5f5" text="#2f4f4f">
<h2 th:text="${title}"></h2>
<p th:text="${msg}"></p>

<form method="POST" action="/validate" th:object="${fm}" >
<div>
<label>名前 </label>
<input type="text" name="name" size="20" th:value="*{name}" />
<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span>
</div>
<div>
<label>メール</label>
<input type="text" name="mail" size="20" th:value="*{mail}" />
<span th:if="${#fields.hasErrors('mail')}" th:errors="*{mail}"></span>
</div>
<div>
<label>年齢 </label>
<input type="number" name="age" size="5" th:value="*{age}" />
<span th:if="${#fields.hasErrors('age')}" th:errors="*{age}"></span>
</div>
<div>
<label>メモ </label>
<input type="text" name="memo" size="40" th:value="*{memo}" />
</div><br>
<input type="submit" name="check" value="チェック" />
</form>

</body>
</html>

<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Demo 11</title>
</head>
<body bgcolor="#f5f5f5" text="#2f4f4f">
<h2 th:text="${title}"></h2>
<p th:text="${msg}"></p>

<div><label>名前 :</label><span th:text="${fm.name}"></span></div>
<div><label>メール:</label><span th:text="${fm.mail}"></span></div>
<div><label>年齢 :</label><span th:text="${fm.age}"></span></div>
<div><label>メモ :</label><span th:text="${fm.memo}"></span></div>

[<a href="http://localhost:8080/validate">戻る</a>]
</body>
</html>

ここに記載した Spring Boot のサンプルですが、以下の書籍を参考にさせてもらいました。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です