The Java EE 7 Tutorialの22 Bean Validation: Advanced Topicsの章をテキトーに訳した。
22 Bean Validation: Advanced Topics
この章では、カスタム制約(custom constraints), カスタムメッセージ(custom validator messages), Bean Validationを使用した制約グループ、の作成方法について説明します。
この章では以下のトピックを扱います。
- Creating Custom Constraints
- Customizing Validator Messages
- Grouping Constraints
- Using Method Constraints in Type Hierarchies
22.1 Creating Custom Constraints
Bean Validationは開発者がカスタム制約を作成するためのアノテーション・インターフェース・クラスを定義しています。
22.1.1 Using the Built-In Constraints to Make a New Constraint
Bean Validationには、新規の制約を作成したり再利用するために、制約を組み合わせられるビルトイン制約があります。開発者はいくつかのビルトイン制約で構成されたカスタム制約を定義し、単一アノテーションとしてコンポーネント属性に適用することで、制約の定義を簡略化できます。
@Pattern.List({ @Pattern(regexp = "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\." +"[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*" +"@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?") }) @Constraint(validatedBy = {}) @Documented @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface Email { String message() default "{invalid.email}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @interface List { Email[] value(); } }
このカスタム制約は属性に適用可能です。
... @Email protected String email; ...
22.1.2 Removing Ambiguity in Constraint Targets
カスタム制約は、制約ターゲットを指定するためのvalidationAppliesTo
を要求する戻り値とメソッド引数の両方に適用可能です。
@Constraint(validatedBy=MyConstraintValidator.class) @Target({ METHOD, FIELD, TYPE, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) public @interface MyConstraint { String message() default "{com.example.constraint.MyConstraint.message}"; Class<?>[] groups() default {}; ConstraintTarget validationAppliesTo() default ConstraintTarget.PARAMETERS; ... }
この制約はメソッド引数にデフォルトでvalidationAppliesTo
ターゲットを設定します。
@MyConstraint(validationAppliesTo=ConstraintTarget.RETURN_TYPE) public String doSomething(String param1, String param2) { ... }
上の例では、ターゲットはメソッドの戻り値に設定されています。
22.2 Customizing Validator Messages
Bean Validationにはビルトイン制約用にデフォルトメッセージのリソースバンドルが含まれています。このメッセージはカスタマイズと非英語圏ロケール用のローカライズが可能です。
22.2.1 The ValidationMessages Resource Bundle
ValidationMessages
リソースバンドルとロケールバリアントに含まれるデフォルトのメッセージはオーバーライドが可能です。ValidationMessages
リソースバンドルは通常プロパティファイルで、アプリケーションのデフォルトパッケージにValidationMessages.properties
として配置します。
22.2.1.1 Localizing Validation Messages
ValidationMessages.properties
のロケールバリアントは、ファイルのベースネームにロケールプレフィクスとアンダースコアを繋げることで、追加されます。たとえば、SpanishロケールバリアントのリソースバンドルはValidationMessages_es.properties
となります。
22.3 Grouping Constraints
制約は一つ以上のグループに追加が可能です。制約のサブセットを作成するために制約グループを使用し、特定オブジェクト検証用の制約を作成します。デフォルトでは、すべての制約はDefault
制約グループに含まれます。
制約グループはインターフェースで表現します。
public interface Employee {}
public interface Contractor {}
制約グループは他グループを継承可能です。
public interface Manager extends Employee {}
制約を要素に追加する場合、制約のgroups
要素にグループインターフェースのクラス名を指定することで制約の属するグループを宣言します。
@NotNull(groups=Employee.class) Phone workPhone;
複数グループを宣言するには、グループを({と})で囲み、グループのクラス名をカンマで区切ります。
@NotNull(groups={ Employee.class, Contractor.class }) Phone workPhone;
あるグループが他のグループを継承する場合、グループの検証はスーパーグループで宣言されているすべての制約が検証されます。たとえば、workPhone
フィールドの検証はManager
グループが検証されますが、これはEmployee
がManager
のスーパーインターフェースなためです。
22.3.1 Customizing Group Validation Order
デフォルトでは、制約グループは順不同で検証されます。とはいえ、他のグループよりもあるグループを先に検証すべき場合があります。たとえば、ある種のクラスでは、基礎データは拡張データよりも先に検証されるべきです。
グループに検証順序を設定するには、インターフェース定義にjavax.validation.GroupSequence
アノテーションを追加し、検証順序を設定します。
@GroupSequence({Default.class, ExpensiveValidationGroup.class}) public interface FullValidationGroup {}
FullValidationGroup
を検証する場合、まずDefault
グループが検証されます。そこでの検証がすべてパスしたあと、ExpensiveValidationGroup
グループが検証されます。ある制約がDefault
とExpensiveValidationGroup
両方の一部の場合、制約はDefault
グループの一部として検証され、その後のExpensiveValidationGroup
では検証されません。
22.4 Using Method Constraints in Type Hierarchies
継承階層のオブジェクトに検証制約を付与する場合、サブタイプ化による意図しないエラー防ぐために特別の配慮が必要です。
型指定をする場合、サブタイプはエラーにならずに置き換えが可能です。たとえば、Person
クラスとそのサブクラスEmployee
があるとすると、Person
インスタンスを使用する場所であっても、Employee
インスタンスを使うことが可能な筈です。もしEmployee
がPerson
のメソッドをオーバーライドしてメソッド引数に制約を付与している場合、Person
オブジェクトでは正しく動作するコードがEmployee
オブジェクトでは例外をスローする可能性があります。
以下のコードはクラス階層内におけるメソッド引数制約の誤った例を示しています。
public class Person { ... public void setEmail(String email) { ... } }
public class Employee extends Person { ... @Override public void setEmail(@Verified String email) { ... } }
@Verified
制約をEmployee.setEmail
に付与することで、Person.setEmail
で妥当だった引数はEmployee.setEmail
で非妥当になります。これは、サブタイプのメソッド(の引数)における事前条件の強化(strengthening the preconditions)と言います。サブタイプのメソッド呼び出しの事前条件の強化はするべきではありません。
同様に、メソッド呼び出しの戻り値をサブタイプで弱めるべきではありません。以下のコードはクラス階層におけるメソッド戻り値の制約の誤った例を示しています。
public class Person { ... @Verified public Email getEmail() { ... } }
public class Employee extends Person { ... @Override public Email getEmail() { ... } }
この例では、Employee.getEmail
メソッドは戻り値の@Verified
制約を削除しています。Person.getEmail
では検証にパスしない戻り値が、Employee.getEmail
ではパスします。これは、サブタイプ(の戻り値)における事後条件の緩和(weakening the postconditions)と言います。サブタイプのメソッド呼び出しで事後条件の緩和はするべきではありません。
もし型階層がサブタイプのメソッド呼び出しで事前条件の強化や事後条件の緩和をする場合、Bean Validationがjavax.validation.ConstraintDeclarationException
をスローします。
パラレルタイプ(parallel types)は、同一のメソッドシグネチャを複数のインターフェースが持ちそれを実装をするクラスです。このクラスは、事前条件の強化を回避するために、インターフェースに適用される制約に注意する必要があります。
public interface PaymentService { void processOrder(Order order, double amount); ... }
public interface CreditCardPaymentService { void processOrder(@NotNull Order order, @NotNull double amount); ... }
public class MyPaymentService implements PaymentService, CreditCardPaymentService { @Override public void processOrder(Order order, double amount) { ... } ... }
この場合、MyPaymentService
はCreditCardPaymentService
のprocessOrder
メソッドの制約を持ちますが、PaymentService.processOrder
を呼び出すクライアントコードはその制約の影響を受けません。これはサブタイプにおける事前条件の強化の一例であり、ConstraintDeclarationException
がスローされます。
22.4.1 Rules for Using Method Constraints in Type Hierarchies
以下に示すのは、型階層におけるメソッド検証制約の使用方法のルールです。
- サブタイプで、実装やオーバーライドするメソッドの引数に制約を追加してはならない。
- 複数のパラレルタイプを宣言するサブタイプで、実装やオーバーライドするメソッドの引数に制約を追加してはならない
- サブタイプで、実装やオーバーライドするメソッドの戻り値には制約を追加してもよい。
関連リンク
- The Java EE 7 Tutorialのテキトー翻訳まとめ - Qiita - Java EE 7 Tutorialのうち、自分がテキトー翻訳したものの一覧