はじめに
BeanValidationのメッセージを遅延変換する、ということの意図を簡単に説明したいと思います。
BeanValidationは検証(validate)した際にメッセージ変換も一緒に実行してくれます。
基本的に、これはありがたい機能なのですが、Domainで発行したメッセージをそのままPresentationで扱うのは適さない場合があります。
例えばクライアントロケールに応じたメッセージ出力をしたい場合です。
ロケールを考慮したメッセージ出力の制御については、こちらの記事が有効です。
masatoshitada.hatenadiary.jp
この方法は有効なのですが、少し条件があって 検証とメッセージ変換が同時に行っても良い場合、というところがあります。
これだとPresentation層で検証して、その結果をクライアントに返却する分には十分なのですが、Domain層やInfrastructure層で検証した場合、少々困ったことになります。
どういうことかというとDomain層でvalidateを行った場合、ロケールをどうにかして渡してあげる必要があります。ただ、これをしてしまうとPresentation層の情報をDomain層で参照しなくてはならずレイヤー違反を起こしてしまいます。
やりたいことは、Domain層のvalidation結果を、Presentation層で変換してクライアントに返却する、です。
ということで、検証とメッセージ変換を分けて実行する(遅延変換する)、という要求が生まれ、それに対応するための仕組みを考えてみよう、と思いました。
ざっくり
大げさなことをする必要はありません。
検証結果オブジェクトに保有している情報を元にメッセージを変換する、だけです。
検証時にもメッセージ変換をしているので冗長的な処理ではありますが、目的は上述の通りなので これは割り切ります。
ついでに参照するPropertyファイルもValidationMessages
とは異なるものを指定することにします
実装
変換するクラス
BeanValidationの検証結果メッセージを変換するクラス.
@author
public class BeanValidationMessageInterpolator {
private final Locale locale;
private BeanValidationMessageInterpolator(Locale locale) {
this.locale = locale == null ? Locale.getDefault() : locale;
}
public static BeanValidationMessageInterpolator create() {
return new BeanValidationMessageInterpolator(null);
}
public static BeanValidationMessageInterpolator of(Locale locale) {
return new BeanValidationMessageInterpolator(locale);
}
public String toMessage(ConstraintViolation<?> constraintViolation) {
ResourceBundleLocator resourceBundleLocator = (Locale _locale) -> {
Control control = Control.getNoFallbackControl(Control.FORMAT_DEFAULT);
return ResourceBundle.getBundle("Messages", this.locale, control);
};
ResourceBundleMessageInterpolator interpolator = new ResourceBundleMessageInterpolator(resourceBundleLocator);
MessageInterpolatorContext context = new MessageInterpolatorContext(
constraintViolation.getConstraintDescriptor(),
constraintViolation.getInvalidValue(),
constraintViolation.getRootBeanClass(),
Collections.emptyMap(),
Collections.emptyMap()
);
String message = interpolator.interpolate(constraintViolation.getMessageTemplate(), context);
return message;
}
}
ロケールの指定が出来るようにしていますが、今回は使用していません。
使用方法は見ての通りということで割愛します。
ResourceBundleMessageInterpolator
がメッセージを変換するクラスです。
これに必要なパラメータを渡していきます。
ResourceBundleLocator
が参照するResourceBundleを管理するクラスです。
今回は手抜きをしてラムダで実装しましたが、本来は継承して別クラスにした方が良いと思います。
MessageInterpolatorContext
には検証結果の情報を渡します。
第4、第5パラメータは今回使用しないので、空Mapを渡しています*1。
最後にResourceBundleMessageInterpolator#interpolate
で変換したい値のプロパティまたは、値そのものを指定して変換します。
検証対象のクラス
@Value(staticConstructor = "of")
public class ValidationTarget {
@Size(min = 11)
private final String noMessage;
@CustomType
private final String customTypeValue;
@AssertTrue(message = "{custom.assert}")
protected boolean isValid() {
return false;
}
@Size(min = 100, message = "{min}以上の文字を指定してください。")
private final String withMessage;
}
Lombokの@Value
の説明は割愛します。
@Size(min = 11)
独自の検証Validator@CustomType
を作成。
ValidationMessages
に追記した {CustomValidator.message}
に指定したメッセージが使用されます。
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {CustomValidator.class})
public @interface CustomType {
String message() default "{CustomValidator.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@interface List {
CustomType[] value();
}
}
public class CustomValidator implements ConstraintValidator<CustomType, String> {
@Override
public void initialize(CustomType constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value == null;
}
}
メッセージを取得するのPropertyキーはCustomValidator.message
です。
アクセッサメソッドの戻り値検証
''@AssertTrue(message = "{custom.assert}")''
アクセッサメソッド*2の戻り値を検証します。
メッセージはPropertyキーを指定しています。
@Size(min = 100, message = "{min}以上の文字を指定してください。")
表示するメッセージを直接アノテーションで指定しています。
テストクラスと実行結果
public class ValidationTest {
@Test
public void lazyMessageConvert() {
ValidationTarget target = ValidationTarget.of("noMessage", "customTypeValue", "withMessage");
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<ValidationTarget>> results = validator.validate(target);
for (ConstraintViolation<ValidationTarget> result : results) {
BeanValidationMessageInterpolator interpolator = BeanValidationMessageInterpolator.create();
String convertedMessage = interpolator.toMessage(result);
System.out.println(">>>>>>>>>");
System.out.println("設定した値 = " + result.getInvalidValue());
System.out.println("検証時に変換されたメッセージ = " + result.getMessage());
System.out.println("遅延変換したメッセージ = " + convertedMessage);
}
}
}
>>>>>>>>>
設定した値 = false
検証時に変換されたメッセージ = direct set message template with annotation
遅延変換したメッセージ = アノテーションに直接設定したメッセージテンプレート
>>>>>>>>>
設定した値 = withMessage
検証時に変換されたメッセージ = 100以上の文字を指定してください。
遅延変換したメッセージ = 100以上の文字を指定してください。
>>>>>>>>>
設定した値 = noMessage
検証時に変換されたメッセージ = size must be between 11 and 2147483647
遅延変換したメッセージ = 文字数は 11 から 2147483647 の間にしてください。
>>>>>>>>>
設定した値 = customTypeValue
検証時に変換されたメッセージ = custom validatior message(default template message)
遅延変換したメッセージ = 独自検証のメッセージ(validatorクラスで指定したデフォルトメッセージ)
Code
Bitbucket
さいごに
ResourceBundleの扱い方や参照するPropertyファイルについて、あれこれしたいところは残っていますが、とりあえず変換するところまで。
あと、この記事だけでは何が嬉しいのか分かりにくいと思います。
もう少し実際の利用ケースに応じた実装を肉付けしていけば遅延変換の嬉しさが伝わるかと思いますが、それはまた後日ということで。