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

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

JSFとCDIとBeanValidationで想定外だったこと

そもそも「いや、それJSFだと亜流ですので」って終わりそうな話だと思いますが、誰かの何かに役立つかもしれないので。

環境

  • Payara5

  • Java EE7

  • Java8

事象


Controller(もしくは ManagedBeanのAction相当)

@Named
@RequestScoped
public class UserRegistrationAction {

    private UserRegistrationPage registrationForm;

    private UserService userService;

    private ViewMessage viewMessage;

    public UserRegistrationAction() {
    }

    @Inject
    public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, ViewMessage viewMessage) {
        this.registrationForm = registrationForm;
        this.userService = userService;
        this.viewMessage = viewMessage;
    }

    public String fwPersist() {
        this.registrationForm.init();
        return "persistedit.xhtml?faces-redirect=true";
    }

    public String confirm() {

        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

        //validateしても、対象のクラスフィールドに @Validと記述しても無視される
        //理由は、コンテナで生成したインスタンスのフィールドが nullだから。
        //ただし、アクセッサを経由したら値は取得できることは デバッグモードで確認済み
        Set<ConstraintViolation<Object>> results = validator.validate(registrationForm, ValidationPriority.class);
        this.viewMessage.appendMessage(results);
        if (results.isEmpty() == false) {
            return "persistedit.xhtml?faces-redirect=true";
        }

        return "persistconfirm.xhtml?faces-redirect=true";
    }

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

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

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

        this.registrationForm.update(responseUser.get());
        return "persistcomplete.xhtml?faces-redirect=true";
    }
}

Form(もしくはManagedBeanのActionFrom相当)

@Named
@SessionScoped
public class UserRegistrationPage implements Serializable {

    private static final long serialVersionUID = 1L;

    private UserId userId;


    private EmailForm userEmail;

    @Valid
    private NameForm name;

    @Valid
    private DateOfBirthForm dateOfBirth;

    @Valid
    private PhoneNumberForm phoneNumber;

    private GenderForm gender;

    public UserRegistrationPage() {
    }

(以下略)
public class NameForm implements DefaultForm<UserName>, Serializable {

    private static final long serialVersionUID = 1L;

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

    public NameForm() {
    }

    public NameForm(String userName) {
        this.value = userName;
    }

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

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

フィールドに@Validアノテーションしていても、対象クラス配下の@NotBlankが 有効にならず、次の画面に遷移してしまいます。

コード全量
Bitbucket

対応後


Controller(もしくは ManagedBeanのAction相当)

@Named
@RequestScoped
public class UserRegistrationAction {

    private UserRegistrationPage registrationForm;

    private UserService userService;

    private ViewMessage viewMessage;

    public UserRegistrationAction() {
    }

    @Inject
    public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, ViewMessage viewMessage) {
        this.registrationForm = registrationForm;
        this.userService = userService;
        this.viewMessage = viewMessage;
    }

    public String fwPersist() {
        this.registrationForm.init();
        return "persistedit.xhtml?faces-redirect=true";
    }

    public String confirm() {

        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

        //CDI管理外のインスタンスを引数として検証を行えば @Valid が有効(普通のBeanValidationの挙動)
        Set<ConstraintViolation<Object>> results = validator.validate(registrationForm.getValidationPersistUser(), ValidationPriority.class);
        this.viewMessage.appendMessage(results);
        if (results.isEmpty() == false) {
            return "persistedit.xhtml?faces-redirect=true";
        }

        return "persistconfirm.xhtml?faces-redirect=true";
    }

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

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

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

        this.registrationForm.update(responseUser.get());
        return "persistcomplete.xhtml?faces-redirect=true";
    }
}

Form(もしくはManagedBeanのActionFrom相当)

@Named
@SessionScoped
public class UserRegistrationPage implements Serializable {

    //@Valid を外す(意味がないので)

    private static final long serialVersionUID = 1L;

    private UserId userId;

    private EmailForm userEmail;

    private NameForm name;

    private DateOfBirthForm dateOfBirth;

    private PhoneNumberForm phoneNumber;

    private GenderForm gender;

    public UserRegistrationPage() {
    }

    //(省略)


    public Object getValidationPersistUser() {
        ValidationPersistUser obj = new ValidationPersistUser();

        //private インナークラスなので、外部から更新されることは気にする必要は無いと考え
        //コンストラクタ実装など手間はかけずにフィールドに直接設定
        obj.userEmail = userEmail;
        obj.name = name;
        obj.dateOfBirth = dateOfBirth;
        obj.phoneNumber = phoneNumber;
        return obj;
    }

    // 追加Actionで必須検証が必要なフィールドだけを抜粋して検証を行う.
    // 本メソッドを見れば、検証対象が分かるので groups による指定よりも見通しは良い(と思う)
    private static class ValidationPersistUser {

        @Valid
        private EmailForm userEmail;

        @Valid
        private NameForm name;

        @Valid
        private DateOfBirthForm dateOfBirth;

        @Valid
        private PhoneNumberForm phoneNumber;

    }

(省略)

コード全量
Bitbucket

動機

vermeer.hatenablog.jp

追記メモ

改めて コメント、本当に ありがとうございます。

javax.inject.Providerは初見だったので、関連しそうなところの勉強用のリンク

toydi/README.md at master · tokuhirom/toydi · GitHub

Google Guice 使い方メモ

[dependency-injection] CDI注入ループ [circular-dependency] | CODE Q&A 問題解決 [日本語]

倖せの迷う森

さいごに

vermeer.hatenablog.jp

でも挙げた通り、これは私の実験場みたいなものなので、これがJSFの通常実装のパターンとは思わないでいただければと思います。