以前、標準制約以外のチェックをしたいとき、チェック処理を集約したバリデータを作成する記事を書きました。(フォームバリデーションのカスタマイズ【Spring Boot】)
しかし、この方法だと特定のFormオブジェクトにしか適用できないので再現性が低くなってしまいます。
そこで、独自のアノテーションを作成し複数のFormオブジェクトで使えるようにすることで、再現性を改善できたのではないかと思い、記事にまとめました。
以下、複数項目を使用したチェックと単項目チェックの2つの実装についてまとめています。
1.複数項目を使用したチェック
■概要
・宿泊プラン、チェックイン日付、チェックアウト日付を指定し空室が存在するかチェックする@VacancyRoomExistsを作成する。
・@VacancyRoomExistsをFormクラス単位に付与することで、複数項目を使用したチェックができる。
・@VacancyRoomExistsを宿泊プラン確認フォームと予約フォームの2つで使用することで、両方のフォームで同じチェックを行うことができる。
アノテーションを作成
package com.example.validator;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Documented
@Constraint(validatedBy = {VacancyRoomExistsValidator.class})//バリデータクラスを指定
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE})//クラスレベルで制約する
@Retention(RetentionPolicy.RUNTIME)
public @interface VacancyRoomExists {
String message() default "選択されたお部屋をご用意することができません。お手数ですが、再度検索をお願いいたします。";//エラーメッセージ
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String planId();//チェックに使う項目
String startDate();//チェックに使う項目
String endDate();//チェックに使う項目
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public static @interface List {
VacancyRoomExists[] value();
}
}
バリデータクラスを作成
ConstraintValidator
を継承して作成する。引数は <作成したアノテーションクラス,バリデーションでチェックしたい対象クラス> にする。
複数のFormクラスで使いたいので、第二引数はObjectにしている。
package com.example.validator;
import java.time.LocalDate;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.domain.Plan;
import com.example.service.SearchPlanService;
public class VacancyRoomExistsValidator implements ConstraintValidator<VacancyRoomExists, Object>{
private String id;//チェックに使う項目
private String startDate;//チェックに使う項目
private String endDate;//チェックに使う項目
@Autowired
SearchPlanService searchPlanService;//空室が存在するかDBに接続してチェックするためServiceクラスをインジェクション
@Override
public void initialize(VacancyRoomExists annotation) {
this.planId=annotation.planId();
this.startDate=annotation.startDate();
this.endDate=annotation.endDate();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
BeanWrapper beanWrapper = new BeanWrapperImpl(value);
Integer planIdVal = (Integer) beanWrapper.getPropertyValue(planId);
LocalDate startDateVal = (LocalDate) beanWrapper.getPropertyValue(startDate);
LocalDate endDateVal = (LocalDate) beanWrapper.getPropertyValue(endDate);
//nullチェックはフォームクラスのNotNullでチェックする
if(planIdVal==null||startDateVal==null||endDateVal==null) {
return true;
}else {
// 指定された日程に空室があるかチェックする
Plan plan = searchPlanService.searchVacancyRoomPlan(planIdVal,startDateVal,endDateVal);
if(plan != null) {
return true;
} else {
return false;
}
}
}
}
Formクラスにアノテーション付与
クラス単位に作成したアノテーション付与。
標準のアノテーションやほかに作成した独自アノテーションも付与できる。
■宿泊プラン確認フォーム
package com.example.form;
import java.time.LocalDate;
import javax.validation.constraints.NotNull;
import org.springframework.format.annotation.DateTimeFormat;
import lombok.Data;
import com.example.validator.DateIntegrety;
import com.example.validator.VacancyRoomExists;
@Data
@VacancyRoomExists(planId="planId", startDate="checkinDate", endDate="checkoutDate")//クラス単位でアノテーションを付ける。アノテーションの項目とFormの項目を結びつける。
@DateIntegrety(startDate="checkinDate", endDate="checkoutDate")//ほかの独自アノテーション
public class ConfirmPlanForm {
private Integer planId;
private String planName;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate checkinDate;
@DateTimeFormat(pattern = "yyyy-MM-dd")
@NotNull(message="チェックアウト日を入力してください")
private LocalDate checkoutDate;
@NotNull(message="宿泊人数を入力してください")
private Integer guestNumber;
}
■予約フォーム
package com.example.form;
import java.time.LocalDate;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import org.springframework.format.annotation.DateTimeFormat;
import com.example.validator.VacancyRoomExists;
import lombok.Data;
@Data
@VacancyRoomExists(planId="planId", startDate="checkinDate", endDate="checkoutDate")//クラス単位でアノテーション付与
public class ReservationForm {
private Integer reservationUser;
@NotBlank(message="氏名を入力してください")
private String name;
@NotBlank(message="ふりがなを入力してください")
@Pattern(regexp="^[ぁ-んー]*$", message="ふりがなはひらがなで入力してください")
private String nameKana;
@NotBlank(message="連絡先を入力してください")
@Pattern(regexp="^[0-9]{9,12}$", message="電話番号は半角数字で入力してください")
private String telephoneNumber;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate checkinDate;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate checkoutDate;
@NotBlank(message="チェックイン時刻を入力してください")
private String checkinTime;
private Integer guestNumber;
private Integer planId;
private Integer totalPrice;
private Integer createUser;
private Integer updateUser;
}
エラーメッセージの表示
th:errors="${Formクラス名}"
でエラーメッセージを表示する。
■宿泊プラン確認画面
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.4.2/css/all.css" integrity="sha384-/rXc/GQVaYpyDdyxK+ecHPVYJSN9bmVFBvjA/9eOB+pb3F2w2N6fc5qB9Ew5yIns" crossorigin="anonymous">
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" th:href="@{/css/common.css}" />
<title>プランの確認</title>
</head>
<body>
<!-- ヘッダー -->
<div th:insert="common :: fragment-header"></div>
<main>
<form th:action="@{/plan/confirm/first}" method="post" th:object="${confirmPlanForm}">
<div class="mgn">
<div th:errors="${confirmPlanForm}" class="errorMsg"></div><!-- 空室チェックのエラーメッセージ -->
<div th:errors="*{checkoutDate}" class="errorMsg"></div>
<div th:errors="*{guestNumber}" class="errorMsg"></div>
</div>
<!-- 省略 -->
</form>
</main>
</body>
</html>
■予約画面
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.4.2/css/all.css" integrity="sha384-/rXc/GQVaYpyDdyxK+ecHPVYJSN9bmVFBvjA/9eOB+pb3F2w2N6fc5qB9Ew5yIns" crossorigin="anonymous">
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" th:href="@{/css/common.css}" />
<title>予約</title>
</head>
<body>
<!-- ヘッダー -->
<div th:insert="common :: fragment-header"></div>
<main>
<form th:action="@{/reservation/form/confirm}" method="post" th:object="${reservationForm}">
<div class="mgn">
<div th:errors="${reservationForm}" class="errorMsg"></div><!-- 空室チェックのエラーメッセージ -->
<div th:errors="*{name}" class="errorMsg"></div>
<div th:errors="*{nameKana}" class="errorMsg"></div>
<div th:errors="*{telephoneNumber}" class="errorMsg"></div>
<div th:errors="*{checkinTime}" class="errorMsg"></div>
</div>
<!-- 省略 -->
</form>
</main>
</body>
</html>
画面イメージ
2.単項目のチェック
■概要
・入力したユーザーIDがユーザーマスタに存在するかチェックする@UserIdExistsを作成する。
・@UserIdExistsをフィールド単位に付与することで、単項目のチェックができる。
アノテーションを作成
@Target(ElementType.FIELD)
と記載することでフィールド単位の制御が可能になる。
package com.example.validator;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Documented
@Constraint(validatedBy = {UserIdExistsValidator.class})
@Target(ElementType.FIELD)//フィールドレベルで制約する
@Retention(RetentionPolicy.RUNTIME)
public @interface UserIdExists {
String message() default "そのユーザーIDはすでに使用されています";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public static @interface List {
UserIdExists[] value();
}
}
バリデータクラスを作成
ConstraintValidator
の引数は <作成したアノテーションクラス,バリデーションでチェックしたい対象フィールドの型> を設定する。
package com.example.validator;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.domain.User;
import com.example.service.UserService;
public class UserIdExistsValidator implements ConstraintValidator<UserIdExists, String>{
@Autowired
UserService userService;
@Override
public void initialize(UserIdExists annotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if(value == null || value.isEmpty()) {
return true;
}
User user = userService.searchByUserId(value);
if(user != null) {
return false;
} else {
return true;
}
}
}
Formクラスのフィールドにアノテーション付与
フィールドに作成したアノテーションを付ける。
package com.example.form;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import com.example.validator.UserIdExists;
import lombok.Data;
@Data
public class CreateAccountForm {
@NotBlank(message="ユーザーIDを入力してください")
@Pattern(regexp="^([a-zA-Z0-9._-]{3,15})$", message="ユーザーIDは3~15文字以内で入力してください。使用できる文字: a-zA-Z0-9_-.")
@UserIdExists//ユーザーIDにアノテーション付与
private String userId;
@NotBlank(message="氏名を入力してください")
private String userName;
@NotBlank(message="メールアドレスを入力してください")
@Email(message="メールアドレスの形式が不正です")
private String email;
@NotBlank(message="パスワードを入力してください")
@Pattern(regexp="^(?=.*?[0-9])(?=.*?[a-z])(?=.*?[A-Z])[0-9a-zA-Z]{8,16}$", message="パスワードは半角英数字を混在させて8~16文字以内で入力してください")
private String password;
@NotBlank(message="確認用パスワードを入力してください")
private String confirmPassword;
@NotBlank(message="権限を選択してください")
private String authority;
private Integer createUser;
private Integer updateUser;
@AssertTrue(message = "パスワードと確認用パスワードが一致しません")
public boolean isPasswordValid() {
if (password == null || password.isEmpty()) {
return true;
}
return password.equals(confirmPassword);
}
}
エラーメッセージの表示
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.4.2/css/all.css" integrity="sha384-/rXc/GQVaYpyDdyxK+ecHPVYJSN9bmVFBvjA/9eOB+pb3F2w2N6fc5qB9Ew5yIns" crossorigin="anonymous">
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" th:href="@{/css/common.css}" />
<title>ログイン</title>
</head>
<body>
<!-- ヘッダー -->
<div th:insert="common :: fragment-header"></div>
<main>
<form method="post" th:action="@{/account/create}" th:object="${createAccountForm}">
<div class="mgn">
<div th:errors="*{userId}" class="errorMsg"></div><!-- userIdの単項目チェックエラーが表示される -->
<div th:errors="*{userName}" class="errorMsg"></div>
<div th:errors="*{email}" class="errorMsg"></div>
<div th:errors="*{password}" class="errorMsg"></div>
<div th:errors="*{confirmPassword}" class="errorMsg"></div>
<div th:if="${#fields.hasErrors('passwordValid')}" th:errors="*{passwordValid}" class="errorMsg"></div>
<div th:errors="*{authority}" class="errorMsg"></div>
</div>
<!-- 省略 -->
</form>
</main>
</body>
</html>