経緯
当初の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 を設定しなければ実行時エラーとなります。
他のアノテーションクラスも作成
同様に、name
と percentage
のアノテーションも作成します。
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