[SpringBoot][Validator]独自バリデーションつくってみるかぁ

# 何のために出社してるんだろう。。。
どうも、**Qiita書くためだけ**に出社してる、え～すけさんですよ。

ある程度技術があって、仕事も普通にこなせる人が**挑戦的**転職するのは全然いいと思うが、
技術もたいしてなくて、仕事もまともにこなせない人が「環境が変わったら何か出来る」と勘違いをして転職をするのはなかなか勧めらんねぇなぁ。
今ある環境ですらまともに出来ないやつが、よそに行って急にバリバリ仕事ができる人に変われるなんてそうそうありえないと思うので、人事の方々はどういった経緯で転職しようとしているのかしっかりと確認してフィルタリングしてね！

```java:中途採用フィルター
中途採用者リスト = 中途候補者リスト.stream()
                      .filter(中途候補者->中途候補者.技術力 > 53万 && 中途候補者.仕事達成率 > 80%)
                      .map(中途候補者->中途採用者)
                      .collect(Collectors.toList())
```

# ガチャガチャきゅ～っと フィギュ
SpringBootでFormやDTOのプロパティにアノテーション付けて入力チェックするじゃん。
最近SpringBoot案件ちょこっとやってて、似たようなことしかチェックしてないのに複数のアノテーション付けなきゃいけないとか、アノテーションだらけでソース見づらくなるしなんかめんどくせぇなぁという経験を得たので、とりあえず文字列特化型のアノテーションの雛形作ってみっかという試みです。

というか、「オリジナルのバリデーションってこんな感じで簡単に作れちゃうんだ！」というのを紹介するための駄文です。


# とりあえずここにLoginFormがあるじゃろ？
```java:LoginForm.java
@Data
public class LoginForm {
  @NotEmpty
  @Size(min= 5, max = 20, message = "{min}～{max}文字を入力してください")
  String username;
  
  @NotEmpty
  @Size(min= 8, max = 20, message = "{min}～{max}文字を入力してください")
  String password;
}
```

１項目に対して、必須チェックと文字数チェックの２つが設定されているやつ。
ちなみに２項目だから別にコード量的にもたいしたことないと思うけど、たとえば同じようなチェックをする項目がアホみたいにあったらどうする？
例えば隠し項目とかも含めて1画面内で大体20個ぐらいの登録項目がある場合

<details><summary>20個のプロパティの場合</summary>

```java:SampleForm.java
@Data
public class SampleForm {
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String antelope;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String ants;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String apes;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String baboons;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String badgers;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String bass;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String bats;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String bears;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String beavers;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String bees;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String boar;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String buffalo;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String camels;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String caterpillars;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String cats;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String cattle;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String cheetahs;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String chickens;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String clams;
  @NotEmpty
  @Size(min = 8, max = 20, message = "{min}～{max}文字を入力してください")
  public String cobras;
}
```

</details>

これ、必須と長さだけならまだ良いけど、文字種やフォーマットチェックだったりも入ってきたりしたら、もうカオスだよね。
※全部が全部に付けるわけじゃないので、別に大したことなくね？ってことは一旦置いておいて

# せめて文字列のチェックぐらいは1つのアノテーションでヤらないか？
とりあえず必須チェックと長さチェックぐらいはまとめて設定できるようにしようということで独自アノテーションとチェック処理クラスを作ってみた。

## まずアノテーションのクラス（＠interface）
```java:StringCheck.java
package jp.co.asil.sample_app.validation.annotation;

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 jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jp.co.asil.sample_app.validation.StringCheckValidator;

@Target({ ElementType.FIELD, ElementType.PARAMETER }) //アノテーションの使用できる箇所。(フィールドとパラメータのみ。必要であれば随時追加。)
@Retention(RetentionPolicy.RUNTIME) // 実行時に利用可能にする。
@Constraint(validatedBy = StringCheckValidator.class) // 検証を行うバリデーターを記載。
@Documented
public @interface StringCheck {
	String message() default "";  // バリデーションのデフォルトでエラーメッセージ。複合メッセージを想定しているため何も設定しない。

  boolean isRequired() default true; // 必須チェックを行うかどうか。デフォルトはtrue。
  String isRequiredMessage() default "{stringCheckValidator.required}";

  int minLength() default 0; // 最小文字数チェックを行うかどうか。デフォルトは0。
  int maxLength() default 0; // 最大文字数チェックを行うかどうか。デフォルトは0。
  String minLengthMessage() default "{stringCheckValidator.minLength}";
  String maxLengthMessage() default "{stringCheckValidator.maxLength}";
  String minMaxLengthMessage() default "{stringCheckValidator.minMaxLength}";

	Class<?>[] groups() default {};  // アノテーションのグループ化に使用する。適用する優先順位を付けたいときなど。ないとダメ。

	Class<? extends Payload>[] payload() default {};  // 同じアノテーションを割り当てたときの重要度の変更に用いる。詳しくはレファレンス参照。ないとダメ。
}
```

注意：sample_appっていう適当なプロジェクトに作ったのでパッケージ名やimport部分は各自で書き直してね！

### ザックリ説明
- `@Target`でこのアノテーションってどこで使えるの？というのと定義
- `@Constraint`でこのアノテーションが付いてる時に、どのクラスでチェックするのかの定義
- 必須チェック用の`boolean isRequired()`と最小・最大文字長を設定するための`int minLength()`と`int maxLength()`
- あとはそれぞれに対応するメッセージ情報用変数を用意。
- `groups()`と`payload()`はおまじないのように書いておくだけでいいよ（無いとダメ

ちな、必須チェックはデフォルトtrueで設定した

## チェック処理をするクラス
```java:StringCheckValidator.java
package jp.co.asil.sample_app.validation;

import org.apache.commons.lang3.StringUtils;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jp.co.asil.sample_app.validation.annotation.StringCheck;

public class StringCheckValidator implements ConstraintValidator<StringCheck, String> {

  StringCheck annotation;

  @Override
  public void initialize(StringCheck constraintAnnotation) {
    this.annotation = constraintAnnotation;
  }

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    // 必須チェック(空の場合は以降のチェックはスキップ)
    if (StringUtils.isEmpty(value)) {
      if (annotation.isRequired()) {
        changeErrorMessage(context, annotation.isRequiredMessage());
      }
      return !annotation.isRequired();
    }

    //
    if (annotation.minLength() > 0 && annotation.maxLength() > 0) {
      // 最小文字数と最大文字数の両方が設定されている場合
      if (value.length() < annotation.minLength() || value.length() > annotation.maxLength()) {
        changeErrorMessage(context, annotation.minLengthMessage());
        return false;
      }
    } else if (annotation.minLength() > 0) {
      // 最小文字数のみ設定されている場合
      if (value.length() < annotation.minLength()) {
        changeErrorMessage(context, annotation.minLengthMessage());
        return false;
      }
    } else if (annotation.maxLength() > 0) {
      // 最大文字数のみ設定されている場合
      if (value.length() > annotation.maxLength()) {
        changeErrorMessage(context, annotation.maxLengthMessage());
        return false;
      }
    }

    return true;
  }

  /**
   * エラーメッセージを変更する。
   * @param context
   * @param message
   */
  void changeErrorMessage(ConstraintValidatorContext context, String message) {
    // デフォルトのConstraintViolationを無効にする。
    context.disableDefaultConstraintViolation();
    // テンプレートにより設定したいエラーメッセージでConstraintViolationを作成。
    context.buildConstraintViolationWithTemplate(message)
        .addConstraintViolation();
  }
}
```
### ザックリ説明
- `ConstraintValidator<アノテーションクラス, チェックするパラメータ>`を継承したクラスを作る
- overrideした`initialize`でフィールドに付いてるアノテーション情報を内部変数へ設定（後続処理で参照するため
- `isValid`で入力チェックを行う、エラーがあった場合はfalseを返すのが鉄則
- 値がない場合は文字数チェックをしても無駄なので、必須チェックだけしてreturn
- minとmaxが両方入っていた場合、片方しか入ってない場合でメッセージを分けたかったのでifで分岐
- エラーがあった場合`changeErrorMessage`にてデフォルトのメッセージ処理を無効にして、アノテーションで設定してあるメッセージを設定し直している。
コレをすることで1つのフィールドに複数のメッセージを一気に登録することが可能となる

## メッセージはこんな感じ
```properties:messages.properties
stringCheckValidator.required=必須入力です
stringCheckValidator.minLength={minLength}文字以上で入力してください
stringCheckValidator.maxLength={maxLength}文字以下で入力してください
stringCheckValidator.minMaxLength={minLength}～{maxLength}文字で入力してください
```

# 適応したLoginFormがここにあるじゃろ？
```java:LoginForm.java
@Data
public class LoginForm {
  @StringCheck(minLength = 5, maxLength = 20)
  String username;

  @StringCheck(minLength = 8, maxLength = 20)
  String password;
}
```

今は必須と文字長だけのチェックで、元のソースからするとたかだか1項目あたり1行の削減ですが、チェックできる内容を増やしたら便利になると思わないかい？

ちなみに`@Autowired`も使えるみたいなので、DBの存在チェックとか業務ロジックと合わせた相関チェックとかも簡単に作れるんじゃね、知らんけど