Featured image of post 【Play Framework2.7】自作のBeanValidatorのエラーメッセージをscalaテンプレートから取得出来ない場合の原因と対処方法【ConstraintValidator】

【Play Framework2.7】自作のBeanValidatorのエラーメッセージをscalaテンプレートから取得出来ない場合の原因と対処方法【ConstraintValidator】

1374文字

scalaテンプレートでバリデーションエラーを取ろうとするとエラーが発生する

とある案件にてPlayFramework2.7でWebシステムを開発している際に、バリデーションエラーを画面に表示しようと公式のように実装していたところ、以下のようなエラーが発生しました。

エラーログ
[error] a.a.ActorSystemImpl - Internal server error, sending 500 response
akka.http.impl.util.One2OneBidiFlow$OutputTruncationException: Inner flow was completed without producing result elements for 1 outstanding elements
	at akka.http.impl.util.One2OneBidiFlow$OutputTruncationException$.apply(One2OneBidiFlow.scala:22)
	at akka.http.impl.util.One2OneBidiFlow$OutputTruncationException$.apply(One2OneBidiFlow.scala:22)
	at akka.http.impl.util.One2OneBidiFlow$One2OneBidi$$anon$1$$anon$4.onUpstreamFinish(One2OneBidiFlow.scala:97)
	at akka.stream.impl.fusing.GraphInterpreter.processEvent(GraphInterpreter.scala:506)
	at akka.stream.impl.fusing.GraphInterpreter.execute(GraphInterpreter.scala:376)
	at akka.stream.impl.fusing.GraphInterpreterShell.runBatch(ActorGraphInterpreter.scala:606)
	at akka.stream.impl.fusing.GraphInterpreterShell$AsyncInput.execute(ActorGraphInterpreter.scala:485)
	at akka.stream.impl.fusing.GraphInterpreterShell.processEvent(ActorGraphInterpreter.scala:581)
	at akka.stream.impl.fusing.ActorGraphInterpreter.akka$stream$impl$fusing$ActorGraphInterpreter$$processEvent(ActorGraphInterpreter.scala:749)
	at akka.stream.impl.fusing.ActorGraphInterpreter$$anonfun$receive$1.applyOrElse(ActorGraphInterpreter.scala:764)

ログを見ただけではちょっとエラーがまだ特定出来ませんね。

まずはデバッグでソースを追いながら原因を特定して対応していこうと思います

手順

前提

今回は以下のような一般的な独自アノテーションによるバリデーションを行おうと思っています。

インタフェース

LoginCheck.class
package validations.auth;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Documented
@Constraint(validatedBy = {LoginCheckValidator.class})
@Target({FIELD, METHOD})
@Retention(RUNTIME)
public @interface LoginCheck {
  String message() default "error.custom.loginCheck";

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

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

実装クラス

LoginCheckValidator.class
package validations.auth;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import com.google.common.base.Strings;

import lombok.Builder;
import lombok.Data;

public class LoginCheckValidator
    implements ConstraintValidator<LoginCheck, LoginCheckValidator.Dto> {

  public void initialize(LoginCheck loginCheck) {}

  @Override
  public boolean isValid(LoginCheckValidator.Dto value, ConstraintValidatorContext context) {
    if (value.isNotFulfill()) {
      return true;
    }

    boolean isValid = {何やのチェックロジック};
    return isValid;
  }

  @Data
  @Builder
  public static class Dto {
    private String loginId;
    private String password;

    public boolean isNotFulfill() {
      return Strings.isNullOrEmpty(loginId) || Strings.isNullOrEmpty(password);
    }
  }
}

Formモデル

LoginFormModel.java
package models.admin.login.form;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import play.data.validation.Constraints;
import validations.auth.LoginCheck;
import validations.auth.LoginCheckValidator;

@AllArgsConstructor(staticName = "of")
@Builder
@Data
public class LoginFormModel {
  public LoginFormModel() {};

  @Constraints.Required public String loginId;
  @Constraints.Required public String password;

  @LoginCheck
  public LoginCheckValidator.Dto getLoginCheck() {
    return LoginCheckValidator.Dto.builder()
                .loginId(this.loginId)
                .password(this.password)
                .build();
  }
}

login.scala.html

HTMLについては今回の必要な部分だけ抜粋しておきます。

login.scala.html
...
(略)
...
<div class="form-group row mb-0">
    <div class="col-md-8 offset-md-4">
        <div>
            <ul class="list-group list-group-flush">
                @for(error <- response.validationError("loginCheck").errors) {
                <li class="list-group-item">@error.format(messages)</li>
                }
            </ul>
        </div>
        <button type="submit" class="btn btn-primary">
            ログイン
        </button>
    </div>
</div>
...
(略)
...

org.springframework.beans.AbstractNestablePropertyAccessorでエラー

デバッグでソースを追っていくと、怪しい部分がありました。

>org.springframework.beans.AbstractNestablePropertyAccessor.javaの620行目辺りのPropertyHandler ph = getLocalPropertyHandler(actualName);にてnullが帰って来ておりそのあとのハンドリングでthrow new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName);とされています。

getLocalPropertyHandler

では、getLocalPropertyHandlerメソッドでは何をやっているのでしょうか。

ざっくり言うと、LoginFormModel.javaに定義されているフィールド名をmap形式で持っており、引数で渡って来たpropertyNameの存在をチェックしていますね。

今回は、フィールドは定義せずgetterにアノテーションを付与してバリデーションを実現しています。

実際カスタムBeanValidator自体は正常に動いているので、そのあとのエラーメッセージのバインディングの部分でメソッドに対するBeanValidatorをサポートしていないようですね。

フィールドを定義する事で回避可能

取り急ぎ原因は分かりました。

解決するには、以下のように同名のフィールドを定義する事で正常に動くようになりました。

LoginFormModel.java
package models.admin.login.form;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import play.data.validation.Constraints;
import validations.auth.LoginCheck;
import validations.auth.LoginCheckValidator;

@AllArgsConstructor(staticName = "of")
@Builder
@Data
public class LoginFormModel {
  public LoginFormModel() {};

  @Constraints.Required public String loginId;
  @Constraints.Required public String password;
  public LoginCheckValidator.Dto loginCheck;

  @LoginCheck
  public LoginCheckValidator.Dto getLoginCheck() {
    return LoginCheckValidator.Dto.builder().loginId(this.loginId).password(this.password).build();
  }
}

loginCheckフィールド自体はフォームからは飛んでくる値ではないので、かなり気持ち悪いですが一旦はこれで解決しました。

何かもっと良い方法や筆者の認識や使い方が間違っている等あればご指摘頂ければなと思いますorz

PlayFrameworkには別の方法でカスタムバリデーションを実装出来る

ちなみに、PlayFrameworkではアノテーションではなくvalidateメソッドの@Overrideによって実現する事も出来ます。

Sometimes you need more sophisticated validation processes. E.g. when a user signs up you want to check if his email address already exists in the database and if so validation should fail.

Because constraints support both runtime Dependency Injection and , we can easily create our own custom (class-level) constraint which gets a Database object injected - which we can use later in the validation process. Of course you can also inject other components like MessagesApi, JPAApi, etc.Java Forms - 2.7.x

しかし、個人的な意見としては使い勝手はいまいちなのと複雑なのであまり使いたくありません。。。

終わりに

以上のように、フレームワークのかなりコアな部分が原因でした。

割と致命的な挙動だと思うので、お困りの方がいましたら是非試してみてください♪