Spring Boot チェック処理

Spring Bootのチェック処理を独自アノテーションで実行してみた

Spring Bootでのチェック処理は、よく使うものについては独自アノテーションで作成しておくことができる。今回は、独自アノテーションによるチェック処理を含むサンプルプログラムを作成してみたので、共有する。

前提条件

下記記事の実装が完了していること。

Spring Bootの相関チェックをform内で実行してみたSpring BootのWEB画面上での相関チェックは、form内で@AssertTrueアノテーションでも実行できることがわかったので...

サンプルプログラムの作成

作成したサンプルプログラムの構成は以下の通り。
サンプルプログラムの構成
なお、上図の赤枠のうち、「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

サンプルプログラムの実行結果

検索画面のエラーメッセージ表示内容は以下の通り。

1) 生年月日(From)の日付が不正な場合
検索画面のエラー1

2) 生年月日(To)の日付が不正な場合
検索画面のエラー2

3) 生年月日(From), 生年月日(To)の日付がいずれも不正な場合
検索画面のエラー3

4) 生年月日(From)>生年月日(To)の場合
検索画面のエラー4

また、入力画面のエラーメッセージ表示内容は以下の通り。

1) 生年月日が未入力の場合
入力画面のエラー1

2) 生年月日が年・月・日に未入力の項目がある場合
入力画面のエラー2

3) 生年月日が存在しない日付の場合
入力画面のエラー3

4) 生年月日が未来日の場合
入力画面のエラー4

要点まとめ

  • Spring Bootでよく使うチェック処理は、独自アノテーションにまとめておくことができる。
  • チェック処理を行う独自アノテーションは、アノテーションクラスとその実装クラスで構成され、アノテーションクラスでは属性を指定し、実装クラスでは具体的なチェック処理を定義する。