Spring Bootでのチェック処理は、よく使うものについては独自アノテーションで作成しておくことができる。今回は、独自アノテーションによるチェック処理を含むサンプルプログラムを作成してみたので、共有する。
前提条件
下記記事の実装が完了していること。
サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。

なお、上図の赤枠のうち、「check」フォルダ内が、前提条件のプログラムから新規追加したプログラムで、他は変更したプログラムとなる。
まず、「CheckDate」アノテーションの内容は以下の通りで、ここでは@CheckDateアノテーション属性で指定する項目を指定している。さらに、@Repeatableアノテーションを利用することで、「CheckDate」アノテーションによるチェック処理を、1クラスで複数回実施できるようにしている。
package com.example.demo.check;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
//RetentionPolicyはclassファイルに記録され実行時に参照できるモード(Runtime)とする
//JavaDoc指定対象(@Documented)とする
//バリデーションの実装クラスはCheckDateValidatorクラスとする
//本アノテーションによるチェック処理を、1クラスで複数回実施できるように設定
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy={CheckDateValidator.class})
@Repeatable(CheckDateAnnotation.class)
public @interface CheckDate {
//表示するエラーメッセージ(アノテーション属性で指定)
String message();
//特定のバリデーショングループがカスタマイズできるような設定
Class<?>[] groups() default {};
//チェック対象のオブジェクトになんらかのメタ情報を与えるためだけの宣言
Class<? extends Payload>[] payload() default {};
//チェック対象の日付_年(アノテーション属性で指定)
String dtYear();
//チェック対象の日付_月(アノテーション属性で指定)
String dtMonth();
//チェック対象の日付_日(アノテーション属性で指定)
String dtDay();
}
また、「CheckDate」アノテーションでのチェック処理を実装しているプログラムは以下の通り。
package com.example.demo.check;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
public class CheckDateValidator implements ConstraintValidator<CheckDate, Object> {
/** 日付のフォーマット */
private final static String dateFormat = "uuuuMMdd";
/** アノテーションで指定した年・月・日・メッセージの項目名 */
private String dtYear;
private String dtMonth;
private String dtDay;
private String message;
@Override
public void initialize(CheckDate annotation) {
//アノテーションで指定した年・月・日・メッセージの項目名を取得
this.dtYear = annotation.dtYear();
this.dtMonth = annotation.dtMonth();
this.dtDay = annotation.dtDay();
this.message = annotation.message();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
//BeanWrapperオブジェクトを生成
BeanWrapper beanWrapper = new BeanWrapperImpl(value);
//アノテーションで指定した年・月・日の項目値を取得
String year = (String)beanWrapper.getPropertyValue(dtYear);
String month = (String)beanWrapper.getPropertyValue(dtMonth);
String day = (String)beanWrapper.getPropertyValue(dtDay);
//年・月・日がすべて空白値の場合はtrueを返す
if(StringUtils.isEmpty(year) && StringUtils.isEmpty(month)
&& StringUtils.isEmpty(day)){
return true;
}
//年・月・日が存在する日付でなければ、エラーメッセージ・エラー項目を設定し
//falseを返す。そうでなければtrueを返す
String dateStr = year + addZero(month) + addZero(day);
if(!isCorrectDate(dateStr, dateFormat)){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(dtYear)
.addConstraintViolation();
return false;
}
return true;
}
/**
* DateTimeFormatterを利用して日付チェックを行う
* @param dateStr チェック対象文字列
* @param dateFormat 日付フォーマット
* @return 日付チェック結果
*/
private static boolean isCorrectDate(String dateStr, String dateFormat){
if(StringUtils.isEmpty(dateStr) || StringUtils.isEmpty(dateFormat)){
return false;
}
//日付と時刻を厳密に解決するスタイルで、DateTimeFormatterオブジェクトを作成
DateTimeFormatter df = DateTimeFormatter.ofPattern(dateFormat)
.withResolverStyle(ResolverStyle.STRICT);
try{
//チェック対象文字列をLocalDate型の日付に変換できれば、チェックOKとする
LocalDate.parse(dateStr, df);
return true;
}catch(Exception e){
return false;
}
}
/**
* 数値文字列が1桁の場合、頭に0を付けて返す
* @param intNum 数値文字列
* @return 変換後数値文字列
*/
private static String addZero(String intNum){
if(StringUtils.isEmpty(intNum)){
return intNum;
}
if(intNum.length() == 1){
return "0" + intNum;
}
return intNum;
}
}
上記プログラム内では、initializeメソッドでアノテーションで指定した項目名を取得している。また、isValidメソッドでは、アノテーション属性で指定した項目による日付チェック処理を行い、チェックエラーの場合にエラーメッセージを設定したり、赤反転するエラー項目の設定をしている。
さらに、「CheckDateAnnotation」アノテーションの内容は以下の通りで、「CheckDate」アノテーションによるチェック処理を、1クラスで複数回実施できるように設定している。
package com.example.demo.check;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
//CheckDateアノテーションチェック処理を
//1クラスで複数回実施できるように設定
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckDateAnnotation {
CheckDate[] value();
}
また、アノテーション属性で指定した日付(From, To)の大小関係をチェックするプログラムの内容は以下の通り。
package com.example.demo.check;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Documented;
//アノテーションの付与対象をクラス(ElementType.TYPE)にする
//RetentionPolicyはclassファイルに記録され実行時に参照できるモード(Runtime)とする
//JavaDoc指定対象(@Documented)とする
//バリデーションの実装クラスはCheckFromToDateValidatorクラスとする
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy={CheckFromToDateValidator.class})
public @interface CheckFromToDate {
//表示するエラーメッセージ(アノテーション属性で指定)
String message();
//特定のバリデーショングループがカスタマイズできるような設定
Class<?>[] groups() default {};
//チェック対象のオブジェクトになんらかのメタ情報を与えるためだけの宣言
Class<? extends Payload>[] payload() default {};
//チェック対象の日付_年_from(アノテーション属性で指定)
String dtYearFrom();
//チェック対象の日付_月_from(アノテーション属性で指定)
String dtMonthFrom();
//チェック対象の日付_日_from(アノテーション属性で指定)
String dtDayFrom();
//チェック対象の日付_年_to(アノテーション属性で指定)
String dtYearTo();
//チェック対象の日付_月_to(アノテーション属性で指定)
String dtMonthTo();
//チェック対象の日付_日_to(アノテーション属性で指定)
String dtDayTo();
}
package com.example.demo.check;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
public class CheckFromToDateValidator
implements ConstraintValidator<CheckFromToDate, Object> {
/** 日付のフォーマット */
private final static String dateFormat = "uuuuMMdd";
/** アノテーションで指定した年・月・日・メッセージの項目名 */
private String dtYearFrom;
private String dtMonthFrom;
private String dtDayFrom;
private String dtYearTo;
private String dtMonthTo;
private String dtDayTo;
private String message;
@Override
public void initialize(CheckFromToDate annotation) {
//アノテーションで指定したfrom,toの年・月・日・メッセージの項目名を取得
this.dtYearFrom = annotation.dtYearFrom();
this.dtMonthFrom = annotation.dtMonthFrom();
this.dtDayFrom = annotation.dtDayFrom();
this.dtYearTo = annotation.dtYearTo();
this.dtMonthTo = annotation.dtMonthTo();
this.dtDayTo = annotation.dtDayTo();
this.message = annotation.message();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
//BeanWrapperオブジェクトを生成
BeanWrapper beanWrapper = new BeanWrapperImpl(value);
//アノテーションで指定したfrom,toの年・月・日の項目値を取得
String yearFrom = (String)beanWrapper.getPropertyValue(dtYearFrom);
String monthFrom = (String)beanWrapper.getPropertyValue(dtMonthFrom);
String dayFrom = (String)beanWrapper.getPropertyValue(dtDayFrom);
String yearTo = (String)beanWrapper.getPropertyValue(dtYearTo);
String monthTo = (String)beanWrapper.getPropertyValue(dtMonthTo);
String dayTo = (String)beanWrapper.getPropertyValue(dtDayTo);
//from,toの年・月・日の年月日がすべて入力されている場合
if(!StringUtils.isEmpty(yearFrom) && !StringUtils.isEmpty(monthFrom)
&& !StringUtils.isEmpty(dayFrom) && !StringUtils.isEmpty(yearTo)
&& !StringUtils.isEmpty(monthTo) && !StringUtils.isEmpty(dayTo)){
//年月日_from, 年月日_toを生成
String fromDay = yearFrom + addZero(monthFrom) + addZero(dayFrom);
String toDay = yearTo + addZero(monthTo) + addZero(dayTo);
//年月日_from,年月日_toがいずれも存在する日付の場合
if(isCorrectDate(fromDay, dateFormat)
&& isCorrectDate(toDay, dateFormat)){
//生年月日_from>生年月日_toの場合は、エラーメッセージ・エラー項目
//を設定しfalseを返す。そうでなければtrueを返す
if(fromDay.compareTo(toDay) > 0){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(dtYearFrom)
.addConstraintViolation();
context.buildConstraintViolationWithTemplate("")
.addPropertyNode(dtYearTo)
.addConstraintViolation();
return false;
}
}
}
return true;
}
/**
* DateTimeFormatterを利用して日付チェックを行う
* @param dateStr チェック対象文字列
* @param dateFormat 日付フォーマット
* @return 日付チェック結果
*/
private static boolean isCorrectDate(String dateStr, String dateFormat){
if(StringUtils.isEmpty(dateStr) || StringUtils.isEmpty(dateFormat)){
return false;
}
//日付と時刻を厳密に解決するスタイルで、DateTimeFormatterオブジェクトを作成
DateTimeFormatter df = DateTimeFormatter.ofPattern(dateFormat)
.withResolverStyle(ResolverStyle.STRICT);
try{
//チェック対象文字列をLocalDate型の日付に変換できれば、チェックOKとする
LocalDate.parse(dateStr, df);
return true;
}catch(Exception e){
return false;
}
}
/**
* 数値文字列が1桁の場合、頭に0を付けて返す
* @param intNum 数値文字列
* @return 変換後数値文字列
*/
private static String addZero(String intNum){
if(StringUtils.isEmpty(intNum)){
return intNum;
}
if(intNum.length() == 1){
return "0" + intNum;
}
return intNum;
}
}
さらに、アノテーション属性で指定した日付が未来日かどうかをチェックするプログラムの内容は以下の通り。
package com.example.demo.check;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Documented;
//アノテーションの付与対象をクラス(ElementType.TYPE)にする
//RetentionPolicyはclassファイルに記録され実行時に参照できるモード(Runtime)とする
//JavaDoc指定対象(@Documented)とする
//バリデーションの実装クラスはFutureDateValidatorクラスとする
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy={FutureDateValidator.class})
public @interface FutureDate {
//表示するエラーメッセージ(アノテーション属性で指定)
String message();
//特定のバリデーショングループがカスタマイズできるような設定
Class<?>[] groups() default {};
//チェック対象のオブジェクトになんらかのメタ情報を与えるためだけの宣言
Class<? extends Payload>[] payload() default {};
//チェック対象の日付_年(アノテーション属性で指定)
String dtYear();
//チェック対象の日付_月(アノテーション属性で指定)
String dtMonth();
//チェック対象の日付_日(アノテーション属性で指定)
String dtDay();
}
package com.example.demo.check;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.time.LocalDate;
import java.time.chrono.JapaneseChronology;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
import java.util.Locale;
public class FutureDateValidator implements ConstraintValidator<FutureDate, Object> {
/** 日付のフォーマット */
private final static String dateFormat = "uuuuMMdd";
/** アノテーションで指定した年・月・日・メッセージの項目名 */
private String dtYear;
private String dtMonth;
private String dtDay;
private String message;
@Override
public void initialize(FutureDate annotation) {
//アノテーションで指定した年・月・日・メッセージの項目名を取得
this.dtYear = annotation.dtYear();
this.dtMonth = annotation.dtMonth();
this.dtDay = annotation.dtDay();
this.message = annotation.message();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
//BeanWrapperオブジェクトを生成
BeanWrapper beanWrapper = new BeanWrapperImpl(value);
//アノテーションで指定した年・月・日の項目値を取得
String year = (String)beanWrapper.getPropertyValue(dtYear);
String month = (String)beanWrapper.getPropertyValue(dtMonth);
String day = (String)beanWrapper.getPropertyValue(dtDay);
//年・月・日がすべて空白値の場合はtrueを返す
if(StringUtils.isEmpty(year) && StringUtils.isEmpty(month)
&& StringUtils.isEmpty(day)){
return true;
}
//年・月・日が未来日の場合は、エラーメッセージ・エラー項目を設定し
//falseを返す。そうでなければtrueを返す
String dateStr = year + addZero(month) + addZero(day);
if(isFutureDate(dateStr, dateFormat)){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(dtYear)
.addConstraintViolation();
return false;
}
return true;
}
/**
* チェック対象文字列の日付が未来日かどうかを返す
* @param dateStr チェック対象文字列
* @param dateFormat 日付フォーマット
* @return 日付が未来日かどうか
*/
private static boolean isFutureDate(String dateStr, String dateFormat){
if(StringUtils.isEmpty(dateStr) || StringUtils.isEmpty(dateFormat)){
return false;
}
//日付と時刻を厳密に解決するスタイルで、暦体系は和暦体系で、
//DateTimeFormatterオブジェクトを作成
DateTimeFormatter df = DateTimeFormatter.ofPattern(dateFormat, Locale.JAPAN)
.withChronology(JapaneseChronology.INSTANCE)
.withResolverStyle(ResolverStyle.STRICT);
try{
//日付の文字列が未来日の場合は、trueを返す
//それ以外の、日付が不正な場合や過去日の場合は、falseを返す
LocalDate dateStrDate = LocalDate.parse(dateStr, df);
LocalDate now = LocalDate.now();
if(dateStrDate.isAfter(now)){
return true;
}
return false;
}catch(Exception e){
return false;
}
}
/**
* 数値文字列が1桁の場合、頭に0を付けて返す
* @param intNum 数値文字列
* @return 変換後数値文字列
*/
private static String addZero(String intNum){
if(StringUtils.isEmpty(intNum)){
return intNum;
}
if(intNum.length() == 1){
return "0" + intNum;
}
return intNum;
}
}
また、「DemoForm.java」の内容は以下の通りで、クラスに@CheckDateアノテーション、@FutureDateアノテーションを指定している。
package com.example.demo;
import com.example.demo.check.CheckDate;
import com.example.demo.check.FutureDate;
import lombok.Data;
import org.thymeleaf.util.StringUtils;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
//日付チェック・未来日チェックを独自アノテーションで実施
@Data
@CheckDate(dtYear = "birthYear", dtMonth = "birthMonth", dtDay = "birthDay"
, message = "{validation.date-invalidate}")
@FutureDate(dtYear = "birthYear", dtMonth = "birthMonth", dtDay = "birthDay"
, message = "{validation.date-future}")
public class DemoForm implements Serializable {
/** ID */
private String id;
/** 名前 */
@NotEmpty
private String name;
/** 生年月日_年 */
private String birthYear;
/** 生年月日_月 */
private String birthMonth;
/** 生年月日_日 */
private String birthDay;
/** 性別 */
@NotEmpty
private String sex;
/** メモ */
private String memo;
/** 確認チェック */
@NotEmpty
private String checked;
/** 性別(文字列) */
private String sex_value;
/** 生年月日_月のMapオブジェクト */
public Map<String,String> getMonthItems(){
Map<String, String> monthMap = new LinkedHashMap<String, String>();
for(int i = 1; i <= 12; i++){
monthMap.put(String.valueOf(i), String.valueOf(i));
}
return monthMap;
}
/** 生年月日_日のMapオブジェクト */
public Map<String,String> getDayItems(){
Map<String, String> dayMap = new LinkedHashMap<String, String>();
for(int i = 1; i <= 31; i++){
dayMap.put(String.valueOf(i), String.valueOf(i));
}
return dayMap;
}
/** 性別のMapオブジェクト */
public Map<String,String> getSexItems(){
Map<String, String> sexMap = new LinkedHashMap<String, String>();
sexMap.put("1", "男");
sexMap.put("2", "女");
return sexMap;
}
/**
* 生年月日の年・月・日が入力されているかをチェックする
* @return チェック結果
*/
@AssertTrue(message = "{validation.date-empty}")
public boolean isBirthDayRequired(){
if(StringUtils.isEmpty(birthYear)
&& StringUtils.isEmpty(birthMonth)
&& StringUtils.isEmpty(birthDay)){
return false;
}
return true;
}
/**
* 性別が不正な値でないかチェックする
* @return チェック結果
*/
@AssertTrue(message = "{validation.sex-invalidate}")
public boolean isSexInvalid(){
return StringUtils.isEmpty(sex) || getSexItems().keySet().contains(sex);
}
}さらに、「SearchForm.java」の内容は以下の通りで、クラスに@CheckDateアノテーション、@CheckFromToDateアノテーションを指定している。@CheckDateアノテーションに@Repeatableアノテーションを利用していたため、このクラスに@CheckDateアノテーションを複数回指定できるようになっている。
package com.example.demo;
import com.example.demo.check.CheckDate;
import com.example.demo.check.CheckFromToDate;
import lombok.Data;
import java.util.LinkedHashMap;
import java.util.Map;
//生年月日_from,toの日付チェックを独自アノテーションで実施
@Data
@CheckDate(dtYear = "fromBirthYear", dtMonth = "fromBirthMonth"
, dtDay = "fromBirthDay", message = "{validation.date-invalidate-from}")
@CheckDate(dtYear = "toBirthYear", dtMonth = "toBirthMonth"
, dtDay = "toBirthDay", message = "{validation.date-invalidate-to}")
@CheckFromToDate(dtYearFrom = "fromBirthYear", dtMonthFrom = "fromBirthMonth"
, dtDayFrom = "fromBirthDay", dtYearTo = "toBirthYear"
, dtMonthTo = "toBirthMonth", dtDayTo = "toBirthDay"
, message = "{validation.date-invalidate-from-to}")
public class SearchForm {
/** 検索用名前 */
private String searchName;
/** 生年月日_年_from */
private String fromBirthYear;
/** 生年月日_月_from */
private String fromBirthMonth;
/** 生年月日_日_from */
private String fromBirthDay;
/** 生年月日_年_to */
private String toBirthYear;
/** 生年月日_月_to */
private String toBirthMonth;
/** 生年月日_日_to */
private String toBirthDay;
/** 検索用性別 */
private String searchSex;
/** 一覧画面の現在ページ数 */
private int currentPageNum;
/** 生年月日_月のMapオブジェクト */
public Map<String,String> getMonthItems(){
Map<String, String> monthMap = new LinkedHashMap<String, String>();
for(int i = 1; i <= 12; i++){
monthMap.put(String.valueOf(i), String.valueOf(i));
}
return monthMap;
}
/** 生年月日_日のMapオブジェクト */
public Map<String,String> getDayItems(){
Map<String, String> dayMap = new LinkedHashMap<String, String>();
for(int i = 1; i <= 31; i++){
dayMap.put(String.valueOf(i), String.valueOf(i));
}
return dayMap;
}
/** 性別のMapオブジェクト */
public Map<String,String> getSexItems(){
Map<String, String> sexMap = new LinkedHashMap<String, String>();
sexMap.put("1", "男");
sexMap.put("2", "女");
return sexMap;
}
}
また、検索画面のHTMLの内容は以下の通り。
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link th:href="@{/demo.css}" rel="stylesheet" type="text/css" />
<title>index page</title>
</head>
<body>
<p>検索条件を指定し、「検索」ボタンを押下してください。</p><br/>
<form method="post" th:action="@{/search}" th:object="${searchForm}">
<!-- 2行エラーがある場合は、エラーメッセージを改行して表示 -->
<span th:if="*{#fields.hasErrors('fromBirthYear')}"
th:errors="*{fromBirthYear}" class="errorMessage"></span>
<span th:if="*{#fields.hasErrors('fromBirthYear') && #fields.hasErrors('toBirthYear')}">
<br/>
</span>
<span th:if="*{#fields.hasErrors('toBirthYear')}"
th:errors="*{toBirthYear}" class="errorMessage"></span>
<table border="1" cellpadding="5">
<tr>
<th>名前</th>
<td><input type="text" th:value="*{searchName}" th:field="*{searchName}" /></td>
</tr>
<tr>
<th>生年月日</th>
<td><input type="text" th:value="*{fromBirthYear}" size="4"
maxlength="4" th:field="*{fromBirthYear}" th:errorclass="fieldError"/>年
<select th:field="*{fromBirthMonth}" th:errorclass="fieldError"
th:classappend="${#fields.hasErrors('fromBirthYear')} ? 'fieldError'">
<option value=""></option>
<option th:each="item : *{getMonthItems()}"
th:value="${item.key}" th:text="${item.value}"/>
</select>月
<select th:field="*{fromBirthDay}" th:errorclass="fieldError"
th:classappend="${#fields.hasErrors('fromBirthYear')} ? 'fieldError'">
<option value=""></option>
<option th:each="item : *{getDayItems()}"
th:value="${item.key}" th:text="${item.value}"/>
</select>日~
<input type="text" th:value="*{toBirthYear}" size="4"
maxlength="4" th:field="*{toBirthYear}" th:errorclass="fieldError"/>年
<select th:field="*{toBirthMonth}" th:errorclass="fieldError"
th:classappend="${#fields.hasErrors('toBirthYear')} ? 'fieldError'">
<option value=""></option>
<option th:each="item : *{getMonthItems()}"
th:value="${item.key}" th:text="${item.value}"/>
</select>月
<select th:field="*{toBirthDay}" th:errorclass="fieldError"
th:classappend="${#fields.hasErrors('toBirthYear')} ? 'fieldError'">
<option value=""></option>
<option th:each="item : *{getDayItems()}"
th:value="${item.key}" th:text="${item.value}"/>
</select>日
</td>
</tr>
<tr>
<th>性別</th>
<td>
<select th:field="*{searchSex}">
<option value=""></option>
<option th:each="item : *{getSexItems()}"
th:value="${item.key}" th:text="${item.value}"/>
</select>
</td>
</tr>
</table>
<br/><br/>
<input type="submit" value="検索" /><br/><br/>
<input type="button" value="閉じる" onclick="window.close();" />
</form>
</body>
</html>さらに、入力画面のHTMLの内容は以下の通り。
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link th:href="@{/demo.css}" rel="stylesheet" type="text/css" />
<title>入力画面</title>
</head>
<body>
<p>下記必要事項を記載の上、「確認」ボタンを押下してください。</p><br/>
<form method="post" th:action="@{/confirm}" th:object="${demoForm}">
<table border="0">
<tr>
<td align="left" valign="top">名前:</td>
<td>
<input type="text" th:value="*{name}"
th:field="*{name}" th:errorclass="fieldError" />
<span th:if="*{#fields.hasErrors('name')}"
th:errors="*{name}" class="errorMessage"></span>
</td>
</tr>
<tr>
<td align="left" valign="top">生年月日:</td>
<td>
<input type="text" th:value="*{birthYear}" size="4"
maxlength="4" th:field="*{birthYear}" th:errorclass="fieldError"
th:classappend="${#fields.hasErrors('birthDayRequired')} ? 'fieldError'" />年
<select th:field="*{birthMonth}" th:errorclass="fieldError"
th:classappend="${#fields.hasErrors('birthYear')
|| #fields.hasErrors('birthDayRequired')} ? 'fieldError'">
<option value="">---</option>
<option th:each="item : *{getMonthItems()}"
th:value="${item.key}" th:text="${item.value}"/>
</select>月
<select th:field="*{birthDay}" th:errorclass="fieldError"
th:classappend="${#fields.hasErrors('birthYear')
|| #fields.hasErrors('birthDayRequired')} ? 'fieldError'">
<option value="">---</option>
<option th:each="item : *{getDayItems()}"
th:value="${item.key}" th:text="${item.value}"/>
</select>日
<span th:if="*{#fields.hasErrors('birthDayRequired')}"
th:errors="*{birthDayRequired}" class="errorMessage"></span>
<span th:if="*{#fields.hasErrors('birthYear')}"
th:errors="*{birthYear}" class="errorMessage"></span>
</td>
</tr>
<tr>
<td align="left" valign="top">性別:</td>
<td>
<for th:each="item : *{getSexItems()}">
<input type="radio" name="sex" th:value="${item.key}" th:text="${item.value}"
th:field="*{sex}" th:errorclass="fieldError" />
</for>
<span th:if="*{#fields.hasErrors('sex')}"
th:errors="*{sex}" class="errorMessage"></span>
<span th:if="*{#fields.hasErrors('sexInvalid')}"
th:errors="*{sexInvalid}" class="errorMessage"></span>
</td>
</tr>
<tr>
<td align="left" valign="top">メモ:</td>
<td>
<textarea rows="6" cols="40" th:value="*{memo}" th:field="*{memo}"></textarea>
</td>
</tr>
<tr>
<td align="left" valign="top">入力確認:</td>
<td>
<input type="checkbox" name="checked" th:value="確認済"
th:field="*{checked}" th:errorclass="fieldError" />
<span th:if="*{#fields.hasErrors('checked')}"
th:errors="*{checked}" class="errorMessage"></span>
</td>
</tr>
</table>
<br/><br/>
<input type="submit" name="next" value="確認" />
<input type="submit" name="back" value="戻る" />
</form>
</body>
</html>その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-web-check-original/demo
サンプルプログラムの実行結果
検索画面のエラーメッセージ表示内容は以下の通り。
3) 生年月日(From), 生年月日(To)の日付がいずれも不正な場合

また、入力画面のエラーメッセージ表示内容は以下の通り。
要点まとめ
- Spring Bootでよく使うチェック処理は、独自アノテーションにまとめておくことができる。
- チェック処理を行う独自アノテーションは、アノテーションクラスとその実装クラスで構成され、アノテーションクラスでは属性を指定し、実装クラスでは具体的なチェック処理を定義する。












