おしりんブログ

新人PGおしりんの色々まとめるよブログ

バリデーション★アノテーション(改)

こんばんわ。


三連休初日キターーーーーーー!!!!
何しようかな!とりあえず昼寝かな!!!!
起きたらヤマダいこ!!!!

と、思ったのが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に変換されて、値がセットされたあとで
バリデートをかけてるよ。


うんまあとにかくあたしの実装がダサダサだったってことだすな。
精進精進。


うお~気づいたらこんな時間。
毎回毎回一つ記事投稿するのにめちゃくちゃ時間かかる…

明日は(数少ない)友達と遊ぶので、楽しみです。
おやスヤァ