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値のデフォルトは CompositionType.AND。 

ただし、有効な制約が少なくとも1つあるかどうかを確認する場合は、この動作を変更できます。

これを実現するには、ConstraintCompositionCompositionType。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から入手できます。