ねこじゃすり
猫を魅了する魔法の装備品!
Anker PowerCor
旅行には必須の大容量モバイルバッテリー!
【最新機種】GoPro hero11 Black
最新機種でVlogの思い出を撮影しよう!
ペヤング ソースやきそば 120g×18個
とりあえず保存食として買っておけば間違いなし!
ドラゴンクエスト メタリックモンスターズギャラリー メタルキング
みんな大好き経験値の塊をデスクに常備しておこう!
MOFT X 【新型 ミニマム版】 iPhone対応 スマホスタンド
Amazon一番人気のスマホスタンド!カード類も収納出来てかさ張らないのでオススメです!
サンディスク microSD 128GB
スマホからSwitchまで使える大容量MicroSDカード!
スポンサーリンク
目次
scalaテンプレートでバリデーションエラーを取ろうとするとエラーが発生する
とある案件にてPlayFramework2.7でWebシステムを開発している際に、バリデーションエラーを画面に表示しようと公式のように実装していたところ、以下のようなエラーが発生しました。
1 2 3 4 5 6 7 8 9 10 11 12 |
[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) |
ログを見ただけではちょっとエラーがまだ特定出来ませんね。
まずはデバッグでソースを追いながら原因を特定して対応していこうと思います
手順
前提
今回は以下のような一般的な独自アノテーションによるバリデーションを行おうと思っています。
インタフェース
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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 {}; } |
実装クラス
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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モデル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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
については今回の必要な部分だけ抜粋しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
... (略) ... <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
をサポートしていないようですね。
フィールドを定義する事で回避可能
取り急ぎ原因は分かりました。
解決するには、以下のように同名のフィールドを定義する事で正常に動くようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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
しかし、個人的な意見としては使い勝手はいまいちなのと複雑なのであまり使いたくありません。。。
終わりに
以上のように、フレームワークのかなりコアな部分が原因でした。
割と致命的な挙動だと思うので、お困りの方がいましたら是非試してみてください♪