LoginSignup
6
8

More than 3 years have passed since last update.

【Java】 独自アノテーションの作成

Last updated at Posted at 2020-06-14

経緯

当初のmodelに、多くのアノテーションを使って実装していたら、「見にくいから独自でアノテーション作って使えば見やすくなるよ」とレビューいただいたのでやってみた。

before

contollerクラス

@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(path = "/sample")
public class SampleController {

/**
   * サンプルデータ取得API
   *
   * @param id サンプルデータID
   * @return サンプルデータの詳細情報
   */
  @GetMapping(path = "{id}", produces = "application/json; charset=UTF-8")
  @ResponseStatus(HttpStatus.OK)
  public Response getSampleDataDetails(@Nonnull @Pattern(regexp = "[0-9]{8}") 
                                       @PathVariable final String id) {
    log.info(String.format("SampleController { id : %s }", id));
    return idUseCase.getSampleDataDetails(id);
  }
}

modelクラス

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ModelSample {

  @NotBlank
  @Size(max = 1024)
  private String name;

  @Digits(integer = 3, fraction = 2)
  private Double percentage;

}

after

contollerクラス

@Validated
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(path = "/sample")
public class SampleController {

  /**
   * サンプルデータ取得API
   *
   * @param id サンプルデータID
   * @return サンプルデータの詳細情報
   */
  @GetMapping(path = "{id}", produces = "application/json; charset=UTF-8")
  @ResponseStatus(HttpStatus.OK)
  public Response getSampleDataDetails(@Id @Valid @Nonnull
                                       @PathVariable final String id) {
    log.info(String.format("SampleController { id : %s }", id));
    return idUseCase.getSampleDataDetails(id);
  }
}

modelクラス

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Validated
public class ModelSample {

  @Name
  @Valid
  private String name;

  @Percentage
  @Valid
  private Double percentage;

}

アノテーションクラスの作成

before / after でアノテーションたちがスッキリしたのが実感できる。こいつを実現するために、まずはアノテーションクラスを作成する。

id 用アノテーションクラス

@Target(ElementType.PARAMETER)
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Pattern(regexp = "[0-9]{8}")
@Nonnull
public @interface Id {

  String message() default "id: don't match format";

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

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

}

解説

@Target

@Target の引数には、アノテーションを適用する場所を指定します。

適用する場所
ElementType.TYPE クラス、インターフェース、アノテーション、enum型
ElementType.FIELD フィールド
ElementType.CONSTRUCTOR コンストラクタ
ElementType.METHOD メソッド

@Retention

@Retention の引数には、アノテーションを保持する範囲を指定します。

保持する範囲
RetentionPolicy.RUNTIME 実行時に保持する。
RetentionPolicy.CLASS クラスファイルに保持する。(実行時には保持されない。)
RetentionPolicy.SOURCE ソースファイルに保持する。(クラスファイルには保持されない。)

*@Retentionをつけない場合、RetentionPolicy.CLASS がデフォルトになります。

@Constraint

@Constraint の引数、validateBy属性には、このアノテーションで実行するバリデーションを指定します。独自のバリデーション処理を実装したい場合に、処理を実装したバリデータクラスをここに指定します。今回は既存の制約を組み合わせて作成するため、こちらの引数は空にしています。

制約アノテーション

@Pattern@Nonnull などの制約アノテーションを独自で作成したアノテーションクラスにつけます。ここで付与したアノテーションは、独自アノテーションを使用したクラスのバリデーション実行時にチェックされます。

@interface

@interface を利用することで、アノテーションを自分で定義することができます。

message

message には、制約違反時のメッセージを指定します。

groups

groups は、状況に応じて制約チェックの実行の是非を判別させるための属性を指定します。groups属性を指定すると、制約を任意のグループにまとめることができ、バリデーションの実行時にそのグループを指定することで、グループごとに異なる制約のチェックを行うことができます。今回はグループにまとめる必要がないため、空で実装しています。

payload

payload は、制約違反に対して重要度などの任意のカテゴリを付与する属性を指定します。必要に応じて使用しますが、今回は特に任意のカテゴリはないので、空で実装しています。

List

List は、ネストしたアノテーションで、同じ制約を異なる条件で複数定義する場合に用いる。今回は同じ制約を異なる条件で複数定義しないため、使用しません。

*制約アノテーションは、message, groups, payload を設定しなければ実行時エラーとなります。

他のアノテーションクラスも作成

同様に、namepercentage のアノテーションも作成します。

name 用アノテーションクラス

@Target(ElementType.FIELD)
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Size(max = 1024)
@NotBlank
public @interface Name {
  String message() default "name: don't match format";

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

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

percentage 用アノテーションクラス

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@Digits(integer = 3, fraction = 2)
public @interface Percentage {
  String message() default "percentage: don't match format";

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

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

解説

@Size

対象のフィールドの文字列の長さが指定したサイズの範囲内にあることを示します。

@Digits

integerには整数部の最大桁数、fractionには小数部の最大桁数 を指定します。@Digits(integer = 3, fraction = 2)は、「最大桁数3のうち、小数点以下は2桁が最大である」とういうことを表しています。

アノテーションを使用クラスにつける

contollerクラス

@Validated
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(path = "/sample")
public class SampleController {

  /**
   * サンプルデータ取得API
   *
   * @param id サンプルデータID
   * @return サンプルデータの詳細情報
   */
  @GetMapping(path = "{id}", produces = "application/json; charset=UTF-8")
  @ResponseStatus(HttpStatus.OK)
  public Response getSampleDataDetails(@Id @Valid @Nonnull
                                       @PathVariable final String id) {
    log.info(String.format("SampleController { id : %s }", id));
    return idUseCase.getSampleDataDetails(id);
  }
}

modelクラス

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Validated
public class ModelSample {

  @Name
  @Valid
  private String name;

  @Percentage
  @Valid
  private Double percentage;

}

注意点

  • 独自アノテーションをつけるクラスには必ず@Validatedをつけること。
  • 独自アノテーションと一緒に@Validをつけること。

おまけ:独自のバリデーション処理を作成

今回のように、既存の制約を組み合わせてアノテーションを作成するのではなく、自分でバリデーションの処理を追加したい場合もあるでしょう。そんな時は、バリデータクラスの実装を行います。今回作成したidバリデーションの処理を実装していきます。

id 用アノテーションクラス

@Target(ElementType.PARAMETER)
@Retention(RUNTIME)
@Constraint(validatedBy = {idValidator.class})
@Pattern(regexp = "[0-9]{8}")
@Nonnull
public @interface Id {

  String message() default "id: don't match format";

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

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

}

バリデーション処理実装 クラス

public class IdValidator implements ConstraintValidator<id, String> {

@Override 
   public void initialize(Id id) {} 

@Override 
   public boolean isValid(String value, ConstraintValidatorContext context) { 
     String pattern = "[0-9]{8}"; 
     if (!StringUtils.isEmpty(value)) {
       return value.matches(pattern);
     }
     return false; 
   } 
 } 

idクラス 解説

@Constraint

@Constraint にバリデーション処理のクラスを設定します。

IdValidatorクラス 解説

ConstraintValidator

バリデータはConstraintValidator<A,T> を実装する必要があります。Aは制約アノテーション、Tは入力値の型を示します。今回は、自作で制約アノテーションを作成したので、Aにはidクラスを、TにはStringで値を受け取る想定で実装しました。

Aはinitialize、TはisValidの引数の型となります。

initialize

initializeでは、JavaBeansのプロパティに設定された制約アノテーションの属性値を取得できます。今回は必要ないので空で作成しています。

isValid

isValidで検証を行い、戻り値がfalseなら、検証失敗としてConstraintVaiolationが生成されます。ここに処理を記載していきます。今回は、受け取った値が正規表現と一致しているか否かを判断し、trueまたはfalseを返しています。そもそもif文に入らなかった場合は、受け取った値がStringではないと判断できるので、falseを返します。

アノテーションを使用クラスにつける

使用方法は既存アノテーションを組み合わせて作成する場合と同じです。

@Validated
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(path = "/sample")
public class SampleController {

  /**
   * サンプルデータ取得API
   *
   * @param id サンプルデータID
   * @return サンプルデータの詳細情報
   */
  @GetMapping(path = "{id}", produces = "application/json; charset=UTF-8")
  @ResponseStatus(HttpStatus.OK)
  public Response getSampleDataDetails(@Id @Valid @Nonnull
                                       @PathVariable final String id) {
    log.info(String.format("SampleController { id : %s }", id));
    return idUseCase.getSampleDataDetails(id);
  }
}

くり返しになりますが、下記には十分注意してください。

  • 独自アノテーションをつけるクラスには必ず@Validatedをつけること。
  • 独自アノテーションと一緒に@Validをつけること。

まとめ

「独自アノテーション作っておいて!」と言われ、アノテーションだ自作できるということを知った。実際作ってみたら、modelクラスがだいぶスッキリして見やすくなった。それぞれどんなバリデーションがかかっているのか、わかりやすくなったことがメリットだなと思った。アノテーションの実装自体はそんなに難しくなかったけど、Contorollerや、アノテーションを使用するクラスに @Validated@Valid をつけないと動かないということに気がつくのにかなり時間がかかった。

参考資料

JavaEE7をはじめよう(23) - Bean Validationの基本
http://enterprisegeeks.hatenablog.com/entry/2016/02/15/072944

JavaEE7をはじめよう(24) - Bean Validationでカスタム制約を作る
http://enterprisegeeks.hatenablog.com/entry/2016/02/29/072811

TERASOLUNA Global Frameworkの機能詳細 - 5.5入力チェック
https://terasolunaorg.github.io/guideline/public_review/ArchitectureInDetail/Validation.html#validation-basic-validation

6
8
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
6
8