システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る

BeanValidationのメッセージを遅延変換させる

はじめに

BeanValidationのメッセージを遅延変換する、ということの意図を簡単に説明したいと思います。

BeanValidationは検証(validate)した際にメッセージ変換も一緒に実行してくれます。

基本的に、これはありがたい機能なのですが、Domainで発行したメッセージをそのままPresentationで扱うのは適さない場合があります。

例えばクライアントロケールに応じたメッセージ出力をしたい場合です。

ロケールを考慮したメッセージ出力の制御については、こちらの記事が有効です。

masatoshitada.hatenadiary.jp

この方法は有効なのですが、少し条件があって 検証とメッセージ変換が同時に行っても良い場合、というところがあります。

これだとPresentation層で検証して、その結果をクライアントに返却する分には十分なのですが、Domain層やInfrastructure層で検証した場合、少々困ったことになります。

どういうことかというとDomain層でvalidateを行った場合、ロケールをどうにかして渡してあげる必要があります。ただ、これをしてしまうとPresentation層の情報をDomain層で参照しなくてはならずレイヤー違反を起こしてしまいます。

やりたいことは、Domain層のvalidation結果を、Presentation層で変換してクライアントに返却する、です。

ということで、検証とメッセージ変換を分けて実行する(遅延変換する)、という要求が生まれ、それに対応するための仕組みを考えてみよう、と思いました。

ざっくり

大げさなことをする必要はありません。

検証結果オブジェクトに保有している情報を元にメッセージを変換する、だけです。

検証時にもメッセージ変換をしているので冗長的な処理ではありますが、目的は上述の通りなので これは割り切ります。

ついでに参照するPropertyファイルもValidationMessagesとは異なるものを指定することにします

実装

変換するクラス

/**
 * BeanValidationの検証結果メッセージを変換するクラス.
 *
 * @author Yamashita,Takahiro
 */
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の説明は割愛します。

標準APIアノテーション

@Size(min = 11)

  • 検証時に変換されたメッセージ
    ValidationMessagesの標準のメッセージが使用されます。

  • 遅延変換したメッセージ
    MessagesのPropertyキーの一致するメッセージが使用されます。

独自アノテーション

独自の検証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です。

  • 検証時に変換されたメッセージ
    ValidationMessagesに追記したメッセージが使用されます。

  • 遅延変換したメッセージ
    MessagesのPropertyキーの一致するメッセージが使用されます。

アクセッサメソッドの戻り値検証

''@AssertTrue(message = "{custom.assert}")''

アクセッサメソッド*2の戻り値を検証します。

メッセージはPropertyキーを指定しています。

  • 検証時に変換されたメッセージ
    ValidationMessagesに追記したメッセージが使用されます。

  • 遅延変換したメッセージ
    Messagesの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ファイルについて、あれこれしたいところは残っていますが、とりあえず変換するところまで。

あと、この記事だけでは何が嬉しいのか分かりにくいと思います。

もう少し実際の利用ケースに応じた実装を肉付けしていけば遅延変換の嬉しさが伝わるかと思いますが、それはまた後日ということで。

*1:これは今後 私がやりたいことで活用する予定なので、その時に説明します

*2:getXXとかisXX