1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiita100万記事感謝祭!記事投稿キャンペーン開催のお知らせ

[SpringBoot][Validator]クラス内の複数の値を利用したアノテーションを作る

Posted at

世の中には便利なものが溢れているね

Springでプロジェクト作りたいなら、もうコレでいいじゃん。。。

フロントはAngular・React・Vueから選べるし、
バックも認証周りもちゃんと初期設定(JWT認証とか)してくれるし、
JPAだのServiceなども生成してくれたりするし、
実行環境周りの設定(Docker)とかも色々やってくれるので、
なんか1からプロジェクト作って何かするの馬鹿みたいじゃん。ってか、工数の無駄だなって感じよ。
※複雑な処理は自分で実装しないとあれなので、そういうところだけちゃんとしないとダメだけど

今どきThymleafでHTML作って、jQuery・BootstrapでUI周りやって、APIとかじゃなくてSSRでしっかりリクエスト・レスポンスしてるようなシステムなんて、過去資産の使い回し or ライブラリ選定出来なかった or 人(年配の経験者)を集めたかったかじゃなきゃ選択しないよね~(超絶個人的偏見

フロントはNode系言語のSPA・バックはJWT認証・RestAPIでちゃんとしたIF仕様書を元に作業分担します(キリッ

みたいな案件やらせてもらえませんかねぇ?まぁいいや。

存在チェックを毎回Serviceクラスとかで実施するのめんどいよ~

FormやDTOのプロパティに設定するアノテーションだと、
必須だとか桁数だとかフォーマットのチェックは出来るけど、
その値がマスタに存在するかとか日付From/Toチェックとかは、
FormやDTO内に別途@AssertTrueだとかでメソッド作ったり、
入力チェック以降のServiceクラスでやったりしなきゃならないとかで、
結構地味にめんどくさかったりするよね(わかりみの極み

クラス内の変数を複数指定できるアノテーション作っちゃいなYO!!

アノテーションが指定できる場所は何も変数(プロパティ)だけじゃなくてクラス にも付けられるんだから、なんかもう自作しちゃえばよくね?

やりたいこと

  • アノテーションの引数に、クラス内の変数名を指定する
  • チェック用クラスでアノテーションに指定された変数の値をForm/DTOから取得
  • 取得した値を使ってチェックする(値がない場合はスキップ

意外と単純でしょ?

今回のサンプル

チェック対象のクラス内にどんな名前で定義されているかはわからないけど、
String型のhogeIdfugaIdを使った入力チェック+存在チェックをしたいよ~
ってお話。

クラス名とかはとりあえず適当につけているので、適度に改変して使ってもいいよ(責任は取らんけど

アノテーションクラス

ClassVal.java
package jp.co.asil.app_sample.validation.annotation;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jp.co.asil.app_sample.validation.ClassValValidator;

@Target({TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = ClassValValidator.class)
@Documented
public @interface ClassVal {
  String message() default "実装不備(存在しない変数名 or 型違い)"; 

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

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

}

チェック対象のクラス内に有るhogeIdfugaIdを特定するために、
クラス内の変数名を定義してもらうhogeId()fugaId()を用意。
同じ名前で定義されてた場合に余計な設定を書きたくなかったためデフォルトでhogeId/fugaIdとしている。

バリデーションクラス

ClassValValidator.java
package jp.co.asil.app_sample.validation;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jp.co.asil.app_sample.validation.annotation.ClassVal;

public class ClassValValidator implements ConstraintValidator<ClassVal, Object> {

  ClassVal annotation;

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

  @Override
  public boolean isValid(Object value, ConstraintValidatorContext context) {
    // 必須チェック(空の場合は以降のチェックはスキップ)
    if (value == null) {
      return true;
    }
    BeanWrapper beanWrapper = new BeanWrapperImpl(value);
    // 対象変数の存在チェック/型チェック
    if (!beanWrapper.isReadableProperty(annotation.hogeId())
        || !String.class.equals(beanWrapper.getPropertyType(annotation.hogeId()))) {
      changeErrorMessage(context, annotation.message(), annotation.hogeId());
      return false;
    }
    if (!beanWrapper.isReadableProperty(annotation.fugaId())
        || !String.class.equals(beanWrapper.getPropertyType(annotation.fugaId()))) {
      changeErrorMessage(context, annotation.message(), annotation.fugaId());
      return false;
    }
    String hogeId = (String) beanWrapper.getPropertyValue(annotation.hogeId());
    String fugaId = (String) beanWrapper.getPropertyValue(annotation.fugaId());
    // 必須チェック
    if (StringUtils.isAnyEmpty(hogeId, fugaId)) {
      return true;
    }
    // 存在チェック(以下に実装を行う)
    changeErrorMessage(context, "データが存在しません", annotation.hogeId());
    return false;
  }

  /**
   * エラーメッセージを変更する。
   * 
   * @param context
   * @param message
   */
  void changeErrorMessage(ConstraintValidatorContext context, String message, String node) {
    // デフォルトのConstraintViolationを無効にする。
    context.disableDefaultConstraintViolation();
    // テンプレートにより設定したいエラーメッセージでConstraintViolationを作成。
    context.buildConstraintViolationWithTemplate(message)
        .addPropertyNode(annotation.hogeId())
        .addConstraintViolation();
  }
}
  • 継承してるConstraintValidator<ClassVal, Object>のObjectは、どんな型のクラスがチェック対象になるか指定できないためObjectで受け取るような形にしているよ
    →処理時にはObjectがFormやDTOになってくるよ
  • springのBeanWrapperを使ってForm/Dtoを扱いやすくしているよ(リフレクションちゃんと書くとかめんどいしね!
  • 対象のObject内にアノテーションで定義した変数名が存在するか(isReadableProperty)と想定している型で定義されているかのチェックをしているよ(してない場合は「実装不備」っていう恥ずかしいメッセージが出るようにしているよ
  • 値がない場合はチェックしないでtrueを返してるよ(ココに必須チェックを入れてもいいよ
  • @AutowiredでRepositoryとかMapperをDIしてDBへのチェックとか、Enum存在チェックとか入れればチェック終わりだよ。

サンプル実装なので、存在チェックエラーが絶対に返るように実装しているけど、エラーが無ければtrueを返すようにしてね!

使用例

実際にFormクラスでどんな感じで指定するのかのサンプル

サンプル1

クラス内にhogeIdfugaIdが定義されている場合

SampleForm.java
@ClassVal
@Data
public class SampleForm {

  String hogeId;

  String fugaId;

  Integer piyoId;
}

アノテーションのデフォルト設定で賄えるため、クラスにしれっと付けるだけでOK!!

サンプル2

クラス内にhogeIdは無いけどfugaIdは定義されている場合

SampleForm.java
@ClassVal(hogeId = "hogehogeId")
@Data
public class SampleForm {

  String hogehogeId;

  String fugaId;

  Integer piyoId;
}

クラス内にhogeIdは無いけど、hogehogeIdが変わりなるよって場合
アノテーションの引数でhogeId = "hogehogeId"を設定することで、チェック時にはhogehogeIdの値を参照するようになるよ。

締め

アノテーションなんて、タダの目印ぐらいにしか使ってなかったかもしれないけど、
こういう使い方をしたらコード量の軽減とか開発効率の向上に少しでも役に立つってことがわかったかと思います。

コレはValidationに喰わせるための書き方がある程度決まった(@Constraintをアノテーションクラスに付けたり、チェッククラスにConstraintValidator継承してたりした)ものですが、AOPとか独自の関数とかでリフレクションを使って、自分が作ったアノテーションを取得することも可能なので、使い方によってはホントに色々と処理の実装を省略するようなことが出来るのかなって思うので、例えば「このアノテーションが付いたクラスが引数に来たら」とか「このアノテーションがついた変数やメソッドがクラス内にあったら」何かするような共通的な処理とかに使ってみても良いのかなって気はします。

独自アノテーションに限らずシステム固有の共通機能とかとしての難点としては、
使い方をきちんとチーム内などで共有しておかないと 「せっかくこんな便利なのが有るのに使われない・使い方が分からない」 という、しょうもない自体になってしまうので、アーキ担当とか実装責任者はそういった共有の技術とかも大事になってくるんじゃないかなって気がします。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?