導入と properties の統合及び UTF-8 化
まず build.gradle
に以下を定義する:
dependencies {
compile('org.springframework.boot:spring-boot-starter-validation')
}
これですぐに使えるわけだが、その前にバリデータの設定をする。
後述するがバリデーションメッセージに関してはデフォルトで ValidationMessages.properties
に定義されているものを使用するのだが、これも後述するがバリデーションメッセージのプレースホルダに適用されるフィールド名は messages.properties
のものが使用されるので 2 箇所に書くことになってしまう。
できれば messages.properties
に両方書くようにしたい。
更にデフォルトで properties ファイルは UTF-8 エンコーディングになっていない。
これも UTF-8 に変更したい。
これを実現するには以下のような WebMvcConfigurer
を実装したコンフィギュレーションファイルを定義する:
@Configuration
class Configuration : WebMvcConfigurer {
/**
* バリデータを返す.
*
* @return バリデータ
*/
override fun getValidator(): Validator? {
val source = ReloadableResourceBundleMessageSource().also {
it.setBasename("classpath:messages") // ValidationMessages.properties でなく messages.properties を使用する
it.setDefaultEncoding("UTF-8") // エンコーディングとして UTF-8 を使用する
}
return LocalValidatorFactoryBean().also { it.setValidationMessageSource(source) }
}
}
ちなみに Spring 4 以前は WebMvcConfigurerAdapter
というインターフェースを使用していたようだが Spring 5 (Spring Boot 2.0) では非推奨となり WebMvcConfigurer
を使用するようになった。
尚 messages.properties
でなく messages.yaml
などというファイルを置くとよろしくやってくれるのかどうか試したのだが駄目だった。
残念。
バリデーション例
例えば以下のように messages.properties
に定義してあるものとする:
# フィールド名
date=日付
name=名前
tag=タグ
markdown=Markdown
# バリデーションメッセージ (デフォルトの差し替え)
javax.validation.constraints.NotBlank.message={0} を入力してください。
# カスタムバリデーションメッセージ
validation.max-length={0} は {1} 文字以下で入力してください。
validation.not-selected={0} を選択してください。
javax.validation.constraints.NotBlank.message
に関してはバリデーションで使用する @NotBlank
アノテーションでのエラー時に表示されるメッセージの差し替えである。
この場合 @NotBlank
アノテーションのパッケージ名を含めたクラス名が javax.validation.constraints.NotBlank
の為それに .message
を加えたものを定義しておくとデフォルトメッセージの差し替えができる。
@NotEmpty
や @Size
なども同様となる。
尚、こういう全体的に適用できるバリデーションメッセージだけでなく項目ごとに個別に指定したいバリデーションエラーメッセージがある。
例えば @Size
によるバリデーションエラーは「0 文字以上 64 文字以下」のような表示になってしまうが、多くの場合は「0 文字」は不要で最大桁数のみ通知すればいいはずだ。
こういう場合に任意のプロパティ名でカスタムバリデーションメッセージを定義しておく。
フィールド名に関してはフォームの POST 時に使用する Form
インスタンスのフィールド名と同じにしておく。
Form
class PostForm {
// 必須
@NotBlank
var date: String = ""
// 必須かつ 64 文字以内
@NotBlank @Size(min = 0, max = 64, message = "{validation.max-length}")
var name: String = ""
// 選択必須
@NotEmpty(message = "{validation.not-selected}")
var tag: Array<Int> = arrayOf()
// 必須
@NotBlank
var markdown: String = ""
}
上記の例のようにバリデーションエラー時に表示されるカスタムメッセージを messages.properties
を使用して指定したい場合は {validation.max-length}
のような記法で書く。
{}
を付けずに書くと任意の文字列を指定できるが、折角 messages.properties
が使えるのにハードコーディングすることもないだろう。
Controller
@Controller
class MyController {
/**
* 記事追加画面を表示する.
*
* @param model モデル
* @param form PostForm
* @return template 名
*/
@GetMapping("/posts/add")
fun addPost(model: Model, @ModelAttribute("form") form: PostForm): String = "posts/add"
/**
* 記事を追加する.
*
* @param model モデル
* @param form PostForm
* @param result BindingResult
* @return template 名
*/
@PostMapping("/posts/add")
fun savePost(model: Model, @Validated @ModelAttribute("form") form: PostForm, result: BindingResult): String {
if (result.hasErrors()) {
return "/posts/add"
}
// TODO 登録処理
return "redirect:/posts"
}
}
- 登録画面初期表示時の GET と POST を分ける (
Form
に対するバリデーション指定の為)
BindingResult
を Form
の直後の引数として定義する (※位置が違うと正しく機能しないので注意) とメソッド内で result.hasErrors()
でバリデーションエラーの有無を取得できる (BindingResult
がないとメソッドの中まで処理が進まずに弾かれてしまう)
- 登録画面初期表示時の GET のメソッドの方にも
Form
を含めたほうが良い (フォーム項目の初期データの表示時に Form
に直接値をセットすれば良い)
Form
と Model
は HTML 内の <form>
によって POST される項目か否かで使い分けると良さそうに見える
Thymeleaf
<form th:action="@{/posts/add}" method="POST" th:object="${form}">
<div>
<input type="text" id="date" name="date" th:value="*{date}" th:classappend="${#fields.hasErrors('*{date}') ? 'is-invalid' : ''}">
<div class="invalid-feedback" th:if="${#fields.hasErrors('*{date}')}" th:errors="*{date}"></div>
</div>
<div>
<input type="text" id="name" name="name" th:value="*{name}" th:classappend="${#fields.hasErrors('*{name}') ? 'is-invalid' : ''}">
<div class="invalid-feedback" th:if="${#fields.hasErrors('*{name}')}" th:errors="*{name}"></div>
</div>
<div>
<select class="custom-select" id="tag" name="tag" multiple="multiple" th:classappend="${#fields.hasErrors('*{tag}') ? 'is-invalid' : ''}">
<option th:each="t : ${tags}" th:value="${t.id}" th:text="${t.name}" th:selected="${#arrays.contains(form.tag, t.id)}"></option>
</select>
<div class="invalid-feedback" th:if="${#fields.hasErrors('*{tag}')}" th:errors="*{tag}"></div>
</div>
<div>
<textarea id="markdown" name="markdown" class="form-control" rows="16" th:classappend="${#fields.hasErrors('*{markdown}') ? 'is-invalid' : ''}">[[*{markdown}]]</textarea>
<div class="invalid-feedback" th:if="${#fields.hasErrors('*{markdown}')}" th:errors="*{markdown}"></div>
</div>
<div>
<input type="submit" value="保存"/>
</div>
</form>
<form>
の内容を全部書いたので若干見づらいが重要なのは ${#fields.hasErrors('(Form フィールド名)')}
でそのフィールドにエラーがあるかどうかが取得でき th:errors="(Form フィールド名)"
で対応するエラーメッセージが要素内のテキストノードに格納されるということだ。
コードにも例示したがエラーの有無で入力フォームの見た目を変えたい場合は th:classappend
を使用して class
属性を追加して CSS で見た目を変更すれば良い。
ここまで定義した内容で名前以外すべて空、名前は最大文字数をオーバーした状態で submit
すると以下のようにエラーメッセージが表示される:
- 日付 を入力してください。
- 名前 は 64 文字以下で入力してください。
- タグ を選択してください。
- Markdown を入力してください。