Bean Validationで独自のアノーテションを作成し、検証を行う


JavaにはBean Validationというものがあります。フィールドに@NotNullなどと付けておくアレです。JSR303と呼ばれています。今日はこの話です。

Bean Validationですが、クラスのフィールドに@Hogeとか付けておいて、後はフレームワークに勝手に検証させるなり、自分でvalidするなりして利用するかと思います。

これ、実際に使ってみると確かに便利で良いのですが、自分のビジネスロジックのニーズにマッチするアノテーションが無い!ということが割と頻繁に発生します。そこで、独自のアノテーションを作ってしまおう!というのが今回のメインの話です。

参考情報

JSR303とは何か、Bean Validationが何かを知りたい人は以下が参考になるかと思います。

JSR 303 Bean Validationで遊んでみるよ! - Yamkazu’s Blog

上記のページにもありますが、独自アノテーションを作るなら以下が参考になります。ISBNとか良い題材ですね。

5.5. 入力チェック - terasoluna.org

独自アノテーションの作り方

さて、それではいよいよ独自アノテーションを作って行こうと思います。今回はByteSizeというアノテーションを作って検証してみようと思います。

既にSizeというアノテーションが存在しますが、これは文字列に付けた場合length、つまり文字数で判定するので、文字のバイト数は関係が無いという状況です。今回はバイト数をチェックするためのアノテーションを作って行きます。

まずはアノテーション。

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Constraint(validatedBy = {ByteSizeValidator.class})
@Target({FIELD})
@Retention(RUNTIME)
public @interface ByteSize {
  String message() default "{validation.ByteSize.message}";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  /**
   * 文字サイズを計測する文字エンコーディング
   *
   * @return
   */
  String encoding() default "UTF-8";

  /**
   * 許容するバイトサイズの最大値
   *
   * @return
   */
  int size();

  @Target({FIELD})
  @Retention(RUNTIME)
  @Documented
  public @interface List {
    ByteSize[] value();
  }
}

アノテーションに対する実際の検証処理はこちらです。

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.io.UnsupportedEncodingException;

public class ByteSizeValidator implements ConstraintValidator<ByteSize, String> {

  /**
   * 文字エンコーディング
   */
  private String encoding;

  /**
   * 許容する最大バイト数
   */
  private int size;

  @Override
  public void initialize(ByteSize byteSize) {
    encoding = byteSize.encoding();
    size = byteSize.size();
  }

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    if (value == null) {
      return true;
    }

    return isByteSizeValid(value);
  }

  /**
   * 引数で指定した文字列のバイトサイズを検証します。
   *
   * @param value 検証する文字列
   * @return trueの場合、指定した許容サイズ以下
   */
  private boolean isByteSizeValid(String value) {

    try {
      byte[] bytes = value.getBytes(encoding);

      if (bytes.length > size) {
        //大きい
        return false;
      }
    } catch (UnsupportedEncodingException e) {
      return false;
    }

    return true;
  }
}

こんな感じで作っておくと分かりやすいコードになると思います。

使い方はフレームワークに検証させるか、自分でValidator操作して検証するかします。

解説

ByteSizeインタフェース(アノテーション)ですが、何やらやたら沢山アノテーションが付いています。

@Documented アノテーション用のアノテーションです。これを付けると、アノテーションが付いていることがJavadocに載ります。例えば今回作った@ByteSizeアノテーションをHogeクラスのnameフィールドに付けてHogeクラスをJavadoc出力したとします。するとnameには@ByteSizeがついてることがJavadocに載ります。そういうことをするよ、というアノテーションが@Documentedです。

@Constraint これはそのままですね。ConstraintValidatorの実装クラスオブジェクトを指定しています。

@Target アノテーションをどこに付けるか、です。今回の@ByteSizeアノテーションはフィールドにだけ付けられれば良いのでFIELDだけ指定しています。

@Retention 保持。アノテーションの情報をどこまで持たせておくか、です。RUNTIMEを指定しているので実行時まで保持しておきます。これについては少々古いですが、このサイトの説明が丁寧かと思います。

以上です。

今後もバリデーション力を高めて行きたいです。