Bean検証による制約構成
1. 概要
このチュートリアルでは、Bean検証の制約構成について説明します。
単一のカスタムアノテーションの下で複数の制約をグループ化すると、コードの重複を減らし、読みやすさを向上させることができます。 構成された制約を作成する方法と、必要に応じてそれらをカスタマイズする方法を説明します。
コード例では、JavaBean検証の基本と同じ依存関係があります。
2. 問題を理解する
まず、データモデルについて理解しましょう。 この記事の大部分の例では、 Account クラスを使用します:
public class Account {
@NotNull
@Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
@Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
private String username;
@NotNull
@Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
@Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
private String nickname;
@NotNull
@Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
@Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
private String password;
// getters and setters
}
@ NotNull、@ Pattern、、および@Length制約のグループが3つのフィールドのそれぞれに対して繰り返されていることがわかります。
さらに、これらのフィールドの1つが異なるレイヤーの複数のクラスに存在する場合、制約が一致する必要があります。これにより、コードの重複がさらに増加します。
たとえば、DTOオブジェクトと@Entityモデルにusernameフィールドがあると想像できます。
3. 構成された制約の作成
3つの制約を適切な名前のカスタムアノテーションの下にグループ化することで、コードの重複を回避できます。
@NotNull
@Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
@Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
public @interface ValidAlphanumeric {
String message() default "field should have a valid length and contain numeric character(s).";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
したがって、 @ValidAlphanumeric を使用して、アカウントフィールドを検証できるようになりました。
public class Account {
@ValidAlphanumeric
private String username;
@ValidAlphanumeric
private String password;
@ValidAlphanumeric
private String nickname;
// getters and setters
}
その結果、 @ValidAlphanumeric アノテーションをテストし、違反した制約と同じ数の違反を予期できます。
たとえば、 username を“ john”に設定した場合、は短すぎて数字が含まれていないため、2つの違反が予想されます。
@Test
public void whenUsernameIsInvalid_validationShouldReturnTwoViolations() {
Account account = new Account();
account.setPassword("valid_password123");
account.setNickname("valid_nickname123");
account.setUsername("john");
Set<ConstraintViolation<Account>> violations = validator.validate(account);
assertThat(violations).hasSize(2);
}
4. @ReportAsSingleViolationを使用する
一方、検証では、グループ全体に対して単一のConstraintViolationを返す必要がある場合があります。
これを実現するには、構成された制約に@ReportAsSingleViolationで注釈を付ける必要があります。
@NotNull
@Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
@Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
@ReportAsSingleViolation
public @interface ValidAlphanumericWithSingleViolation {
String message() default "field should have a valid length and contain numeric character(s).";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
その後、 password フィールドを使用して新しいアノテーションをテストし、単一の違反を予期できます。
@Test
public void whenPasswordIsInvalid_validationShouldReturnSingleViolation() {
Account account = new Account();
account.setUsername("valid_username123");
account.setNickname("valid_nickname123");
account.setPassword("john");
Set<ConstraintViolation<Account>> violations = validator.validate(account);
assertThat(violations).hasSize(1);
}
5. ブール制約の構成
これまでのところ、検証は、すべての構成制約が有効な場合にのみ合格しました。 これは、 ConstraintComposition値のデフォルトは
ただし、有効な制約が少なくとも1つあるかどうかを確認する場合は、この動作を変更できます。
これを実現するには、ConstraintCompositionをCompositionType。ORに切り替える必要があります。
@Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
@Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
@ConstraintComposition(CompositionType.OR)
public @interface ValidLengthOrNumericCharacter {
String message() default "field should have a valid length or contain numeric character(s).";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
たとえば、値が短すぎるが少なくとも1つの数字が含まれている場合、違反はありません。
モデルのニックネームフィールドを使用して、この新しいアノテーションをテストしてみましょう。
@Test
public void whenNicknameIsTooShortButContainsNumericCharacter_validationShouldPass() {
Account account = new Account();
account.setUsername("valid_username123");
account.setPassword("valid_password123");
account.setNickname("doe1");
Set<ConstraintViolation<Account>> violations = validator.validate(account);
assertThat(violations).isEmpty();
}
同様に、制約が失敗していることを確認したい場合は、CompositionType.ALL_FALSEを使用できます。
6. メソッド検証のための構成された制約の使用
さらに、合成制約をメソッド制約として使用できます。
メソッドの戻り値を検証するには、構成された制約に @SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)を追加するだけです。
@NotNull
@Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
@Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
@SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)
public @interface AlphanumericReturnValue {
String message() default "method return value should have a valid length and contain numeric character(s).";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
これを例証するために、カスタム制約で注釈が付けられたgetAnInvalidAlphanumericValueメソッドを使用します。
@Component
@Validated
public class AccountService {
@AlphanumericReturnValue
public String getAnInvalidAlphanumericValue() {
return "john";
}
}
ここで、このメソッドを呼び出して、ConstraintViolationExceptionがスローされることを期待しましょう。
@Test
public void whenMethodReturnValuesIsInvalid_validationShouldFail() {
assertThatThrownBy(() -> accountService.getAnInvalidAlphanumericValue())
.isInstanceOf(ConstraintViolationException.class)
.hasMessageContaining("must contain at least one numeric character")
.hasMessageContaining("must have between 6 and 32 characters");
}
7. 結論
この記事では、合成制約を使用してコードの重複を回避する方法を見てきました。
その後、検証にブール論理を使用し、単一の制約違反を返し、メソッドの戻り値に適用するように、構成された制約をカスタマイズする方法を学びました。
いつものように、ソースコードはGitHubでから入手できます。