Bean Validation 2.0を使ったメソッドの制約

Bean Validation 2.0によるメソッドの制約

1. 概要

この記事では、Bean Validation 2.0(JSR-380)を使用してメソッドの制約を定義および検証する方法について説明します。

the previous articleでは、組み込みのアノテーションを備えたJSR-380と、プロパティ検証の実装方法について説明しました。

ここでは、次のようなさまざまなタイプのメソッド制約に焦点を当てます。

  • 単一パラメーター制約

  • クロスパラメーター

  • 制約を返す

また、SpringValidatorを使用して制約を手動および自動で検証する方法についても説明します。

次の例では、Java Bean Validation Basicsとまったく同じ依存関係が必要です。

2. メソッド制約の宣言

開始するには、we’ll first discuss how to declare constraints on method parameters and return values of methods

前述のように、javax.validation.constraintsからの注釈を使用できますが、カスタム制約を指定することもできます(e。 g. カスタム制約またはクロスパラメーター制約の場合)。

2.1. 単一パラメータの制約

単一のパラメーターの制約を定義するのは簡単です。 We simply have to add annotations to each parameter as required

public void createReservation(@NotNull @Future LocalDate begin,
  @Min(1) int duration, @NotNull Customer customer) {

    // ...
}

同様に、コンストラクターにも同じアプローチを使用できます。

public class Customer {

    public Customer(@Size(min = 5, max = 200) @NotNull String firstName,
      @Size(min = 5, max = 200) @NotNull String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    // properties, getters, and setters
}

2.2. クロスパラメータ制約の使用

場合によっては、複数の値を一度に検証する必要があります。たとえば、2つの数値が一方よりも大きい場合です。

これらのシナリオでは、2つ以上のパラメーターに依存する可能性があるカスタムのパラメーター間制約を定義できます。

Cross-parameter constraints can be considered as the method validation equivalent to class-level constraints。 両方を使用して、いくつかのプロパティに基づく検証を実装できます。

簡単な例を考えてみましょう。前のセクションのcreateReservation()メソッドのバリエーションは、タイプLocalDate:の開始日と終了日の2つのパラメーターを取ります。

したがって、beginが将来であり、endbeginの後にあることを確認する必要があります。 前の例とは異なり、単一のパラメータ制約を使用してこれを定義することはできません。

代わりに、クロスパラメーター制約が必要です。

単一パラメーター制約とは対照的に、cross-parameter constraints are declared on the method or constructor

@ConsistentDateParameters
public void createReservation(LocalDate begin,
  LocalDate end, Customer customer) {

    // ...
}

2.3. クロスパラメータ制約の作成

@ConsistentDateParameters制約を実装するには、2つのステップが必要です。

まず、define the constraint annotationを実行する必要があります。

@Constraint(validatedBy = ConsistentDateParameterValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ConsistentDateParameters {

    String message() default
      "End date must be after begin date and both must be in the future";

    Class[] groups() default {};

    Class[] payload() default {};
}

ここでは、これらの3つのプロパティは制約注釈に必須です。

  • message –は、エラーメッセージを作成するためのデフォルトのキーを返します。これにより、メッセージ補間を使用できるようになります。

  • groups –制約の検証グループを指定できます

  • payload – Bean Validation APIのクライアントが使用して、カスタムペイロードオブジェクトを制約に割り当てることができます

カスタム制約を定義する方法の詳細については、the official documentationを参照してください。

その後、バリデータクラスを定義できます。

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConsistentDateParameterValidator
  implements ConstraintValidator {

    @Override
    public boolean isValid(
      Object[] value,
      ConstraintValidatorContext context) {

        if (value[0] == null || value[1] == null) {
            return true;
        }

        if (!(value[0] instanceof LocalDate)
          || !(value[1] instanceof LocalDate)) {
            throw new IllegalArgumentException(
              "Illegal method signature, expected two parameters of type LocalDate.");
        }

        return ((LocalDate) value[0]).isAfter(LocalDate.now())
          && ((LocalDate) value[0]).isBefore((LocalDate) value[1]);
    }
}

ご覧のとおり、isValid()メソッドには実際の検証ロジックが含まれています。 まず、タイプLocalDate.の2つのパラメーターを取得することを確認します。その後、両方が将来であり、endbeginの後にあるかどうかを確認します。

また、ConsistentDateParameterValidatorクラスの@SupportedValidationTarget(ValidationTarget.PARAMETERS)アノテーションが必要であることに注意することが重要です。 これは、@ConsistentDateParameterがメソッドレベルで設定されているためですが、制約はメソッドパラメータに適用されます(次のセクションで説明するように、メソッドの戻り値には適用されません)。

注:Bean Validation仕様では、null-valuesを有効と見なすことを推奨しています。 nullが有効な値でない場合は、代わりに@NotNull-annotationを使用する必要があります。

2.4. 戻り値の制約

メソッドによって返されるオブジェクトを検証する必要がある場合があります。 このために、戻り値の制約を使用できます。

次の例では、組み込み制約を使用しています。

public class ReservationManagement {

    @NotNull
    @Size(min = 1)
    public List<@NotNull Customer> getAllCustomers() {
        return null;
    }
}

getAllCustomers()の場合、次の制約が適用されます。

  • まず、返されるリストはnullであってはならず、少なくとも1つのエントリが必要です。

  • さらに、リストにnullエントリを含めることはできません

2.5. 戻り値のカスタム制約

場合によっては、複雑なオブジェクトの検証も必要になる場合があります。

public class ReservationManagement {

    @ValidReservation
    public Reservation getReservationsById(int id) {
        return null;
    }
}

この例では、返されるReservationオブジェクトは、次に定義する@ValidReservationで定義された制約を満たす必要があります。

繰り返しますが、we first have to define the constraint annotation

@Constraint(validatedBy = ValidReservationValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ValidReservation {
    String message() default "End date must be after begin date "
      + "and both must be in the future, room number must be bigger than 0";

    Class[] groups() default {};

    Class[] payload() default {};
}

その後、バリデータクラスを定義します。

public class ValidReservationValidator
  implements ConstraintValidator {

    @Override
    public boolean isValid(
      Reservation reservation, ConstraintValidatorContext context) {

        if (reservation == null) {
            return true;
        }

        if (!(reservation instanceof Reservation)) {
            throw new IllegalArgumentException("Illegal method signature, "
            + "expected parameter of type Reservation.");
        }

        if (reservation.getBegin() == null
          || reservation.getEnd() == null
          || reservation.getCustomer() == null) {
            return false;
        }

        return (reservation.getBegin().isAfter(LocalDate.now())
          && reservation.getBegin().isBefore(reservation.getEnd())
          && reservation.getRoom() > 0);
    }
}

2.6. コンストラクターの戻り値

以前にValidReservationインターフェイス内でMETHODCONSTRUCTORtargetとして定義したので、we can also annotate the constructor of Reservation to validate constructed instances

public class Reservation {

    @ValidReservation
    public Reservation(
      LocalDate begin,
      LocalDate end,
      Customer customer,
      int room) {
        this.begin = begin;
        this.end = end;
        this.customer = customer;
        this.room = room;
    }

    // properties, getters, and setters
}

2.7. カスケード検証

最後に、Bean Validation APIを使用すると、いわゆるカスケード検証を使用して、単一のオブジェクトだけでなくオブジェクトグラフも検証できます。

Hence, we can use @Valid for a cascaded validation, if we want to validate complex objects。 これは、戻り値だけでなくメソッドのパラメーターにも有効です。

いくつかのプロパティ制約があるCustomerクラスがあると仮定しましょう。

public class Customer {

    @Size(min = 5, max = 200)
    private String firstName;

    @Size(min = 5, max = 200)
    private String lastName;

    // constructor, getters and setters
}

Reservationクラスには、Customerプロパティと、制約のあるその他のプロパティが含まれる場合があります。

public class Reservation {

    @Valid
    private Customer customer;

    @Positive
    private int room;

    // further properties, constructor, getters and setters
}

ここで、メソッドパラメータとしてReservationを参照すると、we can force the recursive validation of all properties

public void createNewCustomer(@Valid Reservation reservation) {
    // ...
}

ご覧のとおり、2つの場所で@Validを使用しています。

  • reservationパラメータ:createNewCustomer()が呼び出されると、Reservationオブジェクトの検証がトリガーされます

  • ここにはネストされたオブジェクトグラフがあるため、customer属性に@Validを追加する必要があります。これにより、このネストされたプロパティの検証がトリガーされます。

これは、タイプReservationのオブジェクトを返すメソッドでも機能します。

@Valid
public Reservation getReservationById(int id) {
    return null;
}

3. メソッド制約の検証

前のセクションで制約を宣言した後、これらの制約を実際に検証することができます。 そのために、複数のアプローチがあります。

3.1. Springによる自動検証

Spring Validationは、HibernateValidatorとの統合を提供します。

注:Spring ValidationはAOPに基づいており、Spring AOPをデフォルトの実装として使用します。 したがって、検証はメソッドに対してのみ機能し、コンストラクターに対しては機能しません。

Springで制約を自動的に検証するようにするには、次の2つのことを行う必要があります。

Firstly, we have to annotate the beans, which shall be validated, with @Validated

@Validated
public class ReservationManagement {

    public void createReservation(@NotNull @Future LocalDate begin,
      @Min(1) int duration, @NotNull Customer customer){

        // ...
    }

    @NotNull
    @Size(min = 1)
    public List<@NotNull Customer> getAllCustomers(){
        return null;
    }
}

次に、MethodValidationPostProcessorBeanを提供する必要があります。

@Configuration
@ComponentScan({ "org.example.javaxval.methodvalidation.model" })
public class MethodValidationConfig {

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

制約に違反した場合、コンテナはjavax.validation.ConstraintViolationExceptionをスローするようになりました。

Spring Bootを使用している場合、hibernate-validatorがクラスパスにある限り、コンテナーはMethodValidationPostProcessorBeanを登録します。

3.2. CDIによる自動検証(JSR-365)

バージョン1.1の時点で、Bean ValidationはCDI(Java EEのコンテキストと依存性注入)で動作します。

アプリケーションがJava EEコンテナで実行される場合、コンテナは呼び出し時にメソッドの制約を自動的に検証します。

3.3. プログラムによる検証

manual method validation in a standalone Java applicationの場合、javax.validation.executable.ExecutableValidatorインターフェースを使用できます。

次のコードを使用してインスタンスを取得できます。

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
ExecutableValidator executableValidator = factory.getValidator().forExecutables();

ExecutableValidatorは4つのメソッドを提供します:

  • メソッド検証用のvalidateParameters()およびvalidateReturnValue()

  • コンストラクター検証用のvalidateConstructorParameters()およびvalidateConstructorReturnValue()

最初のメソッドcreateReservation()のパラメーターを検証すると、次のようになります。

ReservationManagement object = new ReservationManagement();
Method method = ReservationManagement.class
  .getMethod("createReservation", LocalDate.class, int.class, Customer.class);
Object[] parameterValues = { LocalDate.now(), 0, null };
Set> violations
  = executableValidator.validateParameters(object, method, parameterValues);

注:公式ドキュメントでは、このインターフェースをアプリケーションコードから直接呼び出すことは推奨されていませんが、AOPやプロキシなどのメソッドインターセプトテクノロジーを介して使用することはお勧めしません。

ExecutableValidatorインターフェースの使用方法に興味がある場合は、official documentationをご覧ください。

4. 結論

このチュートリアルでは、Hibernate Validatorでメソッド制約を使用する方法を簡単に確認し、JSR-380の新機能についても説明しました。

最初に、さまざまなタイプの制約を宣言する方法について説明しました。

  • 単一パラメーターの制約

  • クロスパラメーター

  • 戻り値の制約

また、Spring Validatorを使用して制約を手動および自動で検証する方法も確認しました。

いつものように、例の完全なソースコードはover on GitHubで入手できます。