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

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

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での実例も無かったので「おっ、これは良いかも(功名心)」と思ったり思わなかったりしたのですが、そんな下心で立ち向かうことは出来ず(苦笑)