バリデーション★アノテーション(改)
こんばんわ。
三連休初日キターーーーーーー!!!!
何しようかな!とりあえず昼寝かな!!!!
起きたらヤマダいこ!!!!
と、思ったのが15時くらい。
気づいたら夜。夕飯食べてお風呂入って今に至る。
結局何も出来ずに
私の三連休初日は終わりを告げました。
そんな今日は、今参加しているプロジェクトの中で作成した
独自のBean Validationについてまとめたいと思うよ。
まず、
・Bean Validationとは
JavaBeansのバリデーションのためのメタデータモデルとAPIを定めた
Javaのソフトウェアフレームワークのこと。
バリデーションのためのメタデータはアノテーションにより指定される。
Bean Validationのインターフェースを実装し、
独自の追加バリデータを作成することができる。
有名なバリデーションフレームワークとして
Hibernate Validatorが挙げられる。
簡単な例をあげるとこんな感じ。
class Bean { @NotNull @Length(min = 8, max = 20) private String userId; String getUserId() { return userId; } void setUserId(String userId) { this.userId = userId; } }
userIdっていうフィールドに対して
@NotNull
→「値が null でないか確認」
@Length(min = 8, max = 20)
→「文字列の長さが指定の範囲とマッチしているか確認」
っていう2つの制約を設けてるよ。
ちなみに制約に違反している時は
エラーメッセージを表示してくれるんだけど、
HibernateValidatorのメッセージ内容は
全て英語で記述されているので、
実際に使う時はValidationMessages.propertiesの定義内容を
日本語に修正してから使うのがベター。
(ベターって言ってみたかっただけ)
こんな風に
javax.validation.constraints.NotNull.message=may not be null
↓
javax.validation.constraints.NotNull.message=Nullは許可されていません。
自分でいちいち
@NotNull(message = "ユーザIDは入力必須項目です")とか
メッセージ指定してもいいんだけどね。
とりあえず、アノテーション一つでバリデートできるなんて、
すごい便利。
上の例みたいに、単一のフィールドに対して
制約を設けることは簡単なんだけど、
例えば
String first; String second; String third;
っていう3つのフィールドがあって、
この3つのフィールドに対して相関的に制約を設けたい
ってなると、
相関的に制約を設けるバリデーションは
Bean Validationや Hibernate Validatorには
今のところ用意されてない(多分…)ので、
自分でバリデーションを作成してあげないといけない。
で、作ってみました。
作成したバリデータは
3つのフィールド
String first; String second; String third;
のうち、
firstに値が設定されている場合はsecondにも値が設定されていること
かつ、thirdには値が設定されていない(null)こと
または
firstに値が設定されていない場合はsecondにも値が設定されていない(null)こと
かつ、thirdには値が設定されている
場合、trueを返す、というもの。
もう一度同じようなバリデータ作れって言われたら
30分もあれば作れると思うけど、
当時は(ほんの数日前w)
アノテーションってどうやって作るの?
バリデータってどうやって作るの?
レベルだったので、結構色んなサイト見たり、
悩んだりして作りました。
で、結果的に、
ちゃんと動作するバリデータを作成することはできたんだけど、
先輩にレビューしてもらった際に
「バリデータの使い方がダサすぎ!」と言われ、
バリデータを使用したクラスについては
大分修正がかかりました。
そんなこんなで完成したバリデータと、
そのバリデータを使用したクラス(レビュー済み)がこちら。
バリデータクラス
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import EvaResource.ChildlenParam; import Childlen; import org.apache.commons.lang3.StringUtils; public class ChildrenValidator implements ConstraintValidator<Children, ChildrenParam> { @Override public void initialize(Children constraintAnnotation) { } @Override public boolean isValid(ChildrenParam value, ConstraintValidatorContext context) { boolean result; if (StringUtils.isNotEmpty(value.first) && StringUtils.isNotEmpty(value.def) && StringUtils.isEmpty(value.third)) { result = true; } else if (StringUtils.isEmpty(value.first) && StringUtils.isEmpty(value.second) && StringUtils.isNotEmpty(value.third)) { result = true; } else { result = false; } return result; } }
import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; import ChildrenValidator; @Target({ TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = { ChildrenValidator.class }) public @interface Children { String message() default "入力値に誤りがあります。"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) @Documented @interface List { Children[] value(); } }
バリデータを使用したクラス
import javax.validation.Valid; import annotation.Children; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @Path("/eva") public class EvaResource { @POST @Path("/findChildren") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Response findChildren(@Valid ChildrenParam param) { /** サンプルコードなのでBeanそのまま返すよ */ return Response.status(200).entity(param).build(); } /** ここでバリデータ使ってる */ @Children @JsonIgnoreProperties(ignoreUnknown = true) public static class ChildrenParam { public String first; public String second; public String third; } }
お分かりいただけただろうか…
サンプルコードなのでスッキリしてるけど、
実際のプロジェクトで書いたソースはこんなもんじゃないんだミソ。
例であげてる3つのフィールドを持ってるクラスが
継承しているクラスがいたり、
フィールドのうち一つがListだったりするんだミソ。
(言い訳)(Maybe)
ちなみに、このアプリケーションを呼ぶときのcurlコマンドはこんな感じかな。
curl -X POST -H "Content-Type:application/json" -d "{\"first\":\"レイ\", \"second\":\"アスカ\", \"third\":\"シンジ\"}" "http://localhost:8080/nameOfProject/eva/findChildren"
現場ではJmeter使ってたので、
実際にはcurlのお世話にはなっておりませぬ。
最後に供養程度にあたしの書いたソースコード
(ダサいと言われたソースコード)も載っけておこうか…
バリデータクラスとアノテーションは一緒です。
バリデータを使ってるクラスが大きく違います。
@Path("/eva") public class EvaResource { @POST @Path("/findChildren") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Response findChildren(@Valid ChildrenParam param) { Children children = param.getValue(); param.first = children.first; param.second = children.second; param.third = children.third; return Response.status(200).entity(param).build(); } public static class ChildrenParam extends /** 継承しているクラス */ { public ChildrenParam() { super(); } public String first; public String second; public String third; @Children public EvaResource.Children getValue() { return new Children(first, second, third); } } public static class Children { public String first; public String second; public String third; public Children(String first, String second, String third) { this.first = first; this.second = second; this.third = third; } } }
いやね…なんかどうしても
メソッドにアノテーション付けなきゃいけない気がしてましてね…
こんな二段階右折みたいな構成のクラス作ってたんですよ…
先輩にレビューしてもらったクラス(上)は、
リクエストボディをfindChildrenメソッドの引数に指定したBeanに
変換するタイミングでバリデートをかけてるのに対して、
あたしの作ったクラス(下)は、
ボディ部がBeanに変換されて、値がセットされたあとで
バリデートをかけてるよ。
うんまあとにかくあたしの実装がダサダサだったってことだすな。
精進精進。
うお~気づいたらこんな時間。
毎回毎回一つ記事投稿するのにめちゃくちゃ時間かかる…
明日は(数少ない)友達と遊ぶので、楽しみです。
おやスヤァ