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

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

Java EE(JSF)でDDDのようなことを考えてみる(2)

vermeer.hatenablog.jp

から、多少の肉付けをした版。

やったこと

  • Validation

  • ValidationExceptionの制御

  • メッセージ(出力まで)

  • ConversationScope制御

  • Redirectの強制

基本は過去の記事などで取り扱った要素を組み込んだ感じです。

DDD的なところ?
あんまりありませんね(笑)

どっちかというと、JSFCDIによる肉付けをしました。という感じです。

パッケージ構成

まだまだ試行錯誤中

やろうとしていることは

  • 設計思想に関する関心事

  • 汎用実装に関する関心事

  • プロダクトに関する関心事

を それぞれコンテキストとして分けて、最終的には それぞれを別のプロジェクトにする、というイメージです。

設計思想と汎用実装を オレオレフレームワークにして、今後のプロダクトは 使っていくという感じにしたいなぁと。

あえていえば、今回のDDD的なところ、になるのはパッケージ構成くらいでしょうか。

Validation

概ね、こんな感じで良いだろう、というところまで出来た気がします。

public class DateOfBirthForm implements DefaultForm<DateOfBirth>, Serializable {

    private static final long serialVersionUID = 1L;

    @NotBlank(groups = ValidationGroups.Form.class)
    private String value = "";

    public DateOfBirthForm() {
    }

    public DateOfBirthForm(String dateOfBirth) {
        this.value = dateOfBirth;
    }

    /**
     * @inheritDoc
     */
    @Override
    public String display() {
        return this.getValue().getValue();
    }

    /**
     * @inheritDoc
     */
    @Valid
    @Override
    public DateOfBirth getValue() {
        return new DateOfBirth(this.value);
    }

}

Formの主な関心事であるValueObjectのアクセッサ(getXXX)に@Validを付与して、検証対象としておく。

public class DateOfBirth {

    @DateTimeFormat
    private final String value;

    public DateOfBirth(String value) {
        this.value = value;
    }

    public Age currentAge() {
        return new Age(this.date());
    }

    public String getValue() {
        return this.value;
    }

    public LocalDate date() {
        return LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    }

}

ValueObjectのプロパティに対して、不変条件である@DateTimeFormat(自作)を注釈。
今回はしませんでしたが「過去の日付であること」とか「年齢が20歳以下」とか 要件によって条件を追加するイメージ。
もし「過去の日付であること」とする場合、標準アノテーションで検証をしようと思ったら プロパティの型がLocalDateじゃないといけないので、そこは悩ましいところ。
現状のアイディアとしては、戻り値の型がLocalDate検証用アクセッサを準備しておいて そこに検証アノテーションを付与するか、もともと「現在日付」については テストの容易性を考えて アプリケーション側で制御できるように 別途 インスタンスを設けるつもりもあったりするので、検証対象のクラスの型をStringとした自作検証アノテーションを設けるかもしれないです。
そのあたりは、実際に そういうことを実装するときに肉付けすれば良いかな。

@Documented
@Constraint(validatedBy = {ddd.domain.validation.annotation.DateTimeFormat.Validator.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
public @interface DateTimeFormat {

    String message() default "{ddd.domain.validation.annotation.DateTimeFormat.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    public @interface List {

        DateTimeFormat[] value();
    }

    public class Validator implements ConstraintValidator<DateTimeFormat, String> {

        @Override
        public void initialize(DateTimeFormat value) {
        }

        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            try {
                LocalDate.parse(value, DateTimeFormatter.ofPattern("uuuu-MM-dd").withResolverStyle(ResolverStyle.STRICT));
            } catch (DateTimeParseException ex) {
                return false;
            }
            return true;
        }
    }

}

自作の検証アノテーションについては、アノテーションと検証実装をペアとして1つのクラスで管理することにしました。
アノテーションはインターフェースみたいなものとして、別に実装をして 検証クラス(Validator)を独自に拡張できるように してみては?とも考えましたが、検証アノテーションでの検証クラスの指定の仕方が クラスパス直接指定であったため 分けても意味がないと考えて 1つにしました。
もし、検証クラスで検証ロジッククラスをDI出来るのであれば そういう方式で 拡張性が良さそうな仕組みになりそう、と思ったのですが。。
Hibernateなどインターフェースと実装を分けているので、出来るは出来ると思うのですが 私の力不足で ここは断念しました。

@Controller
public class UserRegistrationAction {

    private UserRegistrationPage registrationPage;

    private UserService userService;

    private Validator validator;

    public UserRegistrationAction() {
    }

    @Inject
    public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, Validator validator) {
        this.registrationPage = registrationForm;
        this.userService = userService;
        this.validator = validator;
    }

    public String fwPersist() {
        this.registrationPage.init();
        return "persistedit.xhtml";
    }

    public String confirm() {
        validator.validate(registrationPage.getValidationForm());
        return "persistconfirm.xhtml";
    }

    public String modify() {
        return "persistedit.xhtml";
    }

    public String register() {
        User requestUser = this.registrationPage.toUser();
        this.userService.register(requestUser);

        Optional<User> responseUser = this.userService.findByKey(requestUser);

        //とりあえず、登録要素が無いという有り得ない状況だったら 自画面にそのまま遷移させる(ありえないケース)
        if (responseUser.isPresent() == false) {
            return null;
        }

        this.registrationPage.update(responseUser.get());
        return "persistcomplete.xhtml";
    }

    @EndConversation
    public String fwTop() {
        return "index.xhtml";
    }

}

Contorllerは こんな感じ

検証実行は

validator.validate(registrationPage.getValidationForm());

で やっていて、その対象情報は Formクラスから生成している、という感じです。
詳細は以下。

vermeer.hatenablog.jp

アドバイスもいただいたのですが、力不足でjavax.inject.Providerを使った方法では 出来ませんでした*1

ValidationExceptionの制御

上述の検証ロジックの

validator.validate();

の実装は

@Named
@Dependent
public class BeanValidatorProducer {

    @Produces
    public BeanValidator getBeanValidator() {
        return new BeanValidator();
    }
}
public class BeanValidator implements Validator {

    /**
     * {@inheritDoc }
     */
    @Override
    public void validate(Object validateTarget) {
        javax.validation.Validator validator = new GroupSequenceValidator(ValidationPriority.class);
        Set<ConstraintViolation<Object>> results = validator.validate(validateTarget);
        if (results.isEmpty() == false) {
            throw new BeanValidationException(results);
        }
    }
}

GroupSequenceValidatorは自作の拡張Validatior。

詳細は こちら

vermeerlab / beanvalidation / source / — Bitbucket

ですが、やっていることは

通常のValidatorは、validateする際に 摘要する validation group を指定しますが、 本クラスはインスタンスを生成する際に 適用する GroupSequenceを保持させておくことで validateの実装簡素化を図れます.

です(JavaDoc引用)。

で、検証例外が発生した時の挙動ですが

  • ExceptionHandler

  • Interceptor

の2段構えとしました。

通常で考えれば、Interceptorだけで良いと思います。
本当は、アノテーションを強要しないExceptionHandlerだけで解決したかったのですが、標準出力にエラーログが出力されてしまうので断念。
ログだけだったら、まぁ良いのですが(良くはないけれど)、ConversationIdが無くなったりとか まぁ、結局のところFacesContextsの参照とかしたいし、@Controllerの実装漏れを検知できて、ユーザーには「ちょっと変」くらいの印象を与えるだけで収まるセイフティーネットとしてExceptionHandlerを残して、Interceptorを基本とする方式にしました。

メッセージ

@Action
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
@Dependent
public class BeanValidationExceptionInterceptor {

    @Inject
    UrlContext urlContext;

    @Inject
    MessageHandler messageHandler;

    @AroundInvoke
    public Object invoke(InvocationContext ic) throws Exception {

        String currentViewId = urlContext.currentViewId();

        try {
            return ic.proceed();
        } catch (BeanValidationException ex) {
            Set<ConstraintViolation<Object>> results = ex.getValidatedResults();
            messageHandler.appendMessage(results);
            return currentViewId;
        }

    }
}

InterceptorでBeanValidationException からメッセージ出力をしています。

で、実際のメッセージ出力クラスは以下。

public class JsfMessageHandler implements MessageHandler {

    /**
     * {@inheritDoc }
     */
    @Override
    public void appendMessage(Set<ConstraintViolation<Object>> validatedResults) {

        FacesContext facesContext = FacesContext.getCurrentInstance();

        MessageInterpolatorFactory interpolatorFactory
                                   = MessageInterpolatorFactory.of("Messages", "FormMessages", "FormLabels");

        MessageInterpolator interpolator = interpolatorFactory.create();

        validatedResults.stream()
                .map(result -> {
                    return interpolator.toMessage(result);
                })
                .distinct()
                .forEach(message -> {

                    FacesMessage facemsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, message, null);
                    facesContext.addMessage(null, facemsg);
                });

        // リダイレクトしてもFacesMessageが消えないように設定
        facesContext.getExternalContext().getFlash().setKeepMessages(true);

    }

}

メッセージ出力自体の制御は上述と同じく、自作のメッセージ編集ライブラリを使っています。

版数は少し古いですが、やっていることは

vermeer.hatenablog.jp

の通りです。

これを使うことで、標準検証アノテーションのメッセージに対して、「Formのラベル(表示用表現)+ 検証結果」を組み合わせた内容編集を 程良き感じで実現しています。
日本語だけだったら 検証アノテーションmessageで直接指定しても良いんでしょうけど、オレオレとはいえフレームワーク的なものを考えているので 国際化対応は初めから考慮したいところ、ということで組み込みました。

ConversationScope制御

vermeer.hatenablog.jp

を 余計な標準出力とかを除外して ほぼ そのまま移植。

Redirectの強制

vermeer.hatenablog.jp

これも余計な標準出力とかを除外して ほぼ そのまま移植。

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

E2E test

vermeer_etc / jsf-ddd-e2etest / source / — Bitbucket

さいごに

これまで部分的に試行錯誤していたものを組み込んで1つの形にしたという感じです。
まぁまぁ形になった気がします。
まだまだ、やりたいことは山積みな訳ですが、Validationとその結果出力まで、というところで 一段落はついたので、そこまでを まとめてみました。
次は、絶対に避けれないセキュリティって思ったりするけど、先に出力メッセージに関する制御を やろうかな、とか正直 気分次第です(笑)。

*1:日本語情報が少なかったし、JSFでの実例も無かったので「おっ、これは良いかも(功名心)」と思ったり思わなかったりしたのですが、そんな下心で立ち向かうことは出来ず(苦笑)