10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GuardMan でふつうのバリデーション

Last updated at Posted at 2013-12-22

この記事は Java Advent Calendar 2013 23 日目の記事です。昨日は kokuzawa さんの「JAX-RSで複数ファイルをアップロードするには」でした。

何を書こうかなと思ったのですが、前回「最近の Java Web 開発について」 で JAX-RS + Backbone.js で Web 開発しているという話をしたので、その流れで GuardMan という拙作の小さなライブラリでも紹介しようかなと思います。

JSR-303

前回のプレゼンテーション で「JSR-303(Bean Validation)が使いづらい」と書きました。JSR-303 というのはこちらで紹介されている通り Java Beans のためのバリデーション仕様です。Hibernate Validator などが有名な実装だと思います。いかんせん Web 開発の経験が乏しいので自信が無いのですが、JAX-RS を使う場合、バリデーションには JSR-303 を使うことが多いようです。Dropwizard も JSR-303 を採用していますね。

JSR-303 を使うと、このようにアノテーションでバリデーションを定義することが出来ます。

public class TradeBean {

  private Long id;

  @NotNull
  private String tradeNo;

  @Max(2000)
  private String remarks;

  // …

}

これは単純なバリデーションのケースでは非常に直感的ですし実装も容易です。ところが少し複雑な例になってくると、アノテーションベースゆえにロジックを書くのが妙に大変になってきます。例えば相関バリデーションを実施するには

  • 必要なパラメータを返す専用の Getter を作り
  • Getter に独自のアノテーションを付加し
  • Validator を実装する

などの方法を採る必要が出てきます(ちょうど Java EE Advent Calendar 2013backpaper0 さんが記事を書かれていますね)。他にも条件付きバリデーションを実施するのが難しかったり、(少なくとも Hibernate Validator の場合は)自作した Validator の中でビルトインの Validator を使うのが難しいなどの問題もあります。特にプロ向きの業務アプリケーションの画面ではある程度複雑なバリデーションをすることが大半なので、単純にプロパティに注釈するだけで済むケースの方が稀でしょう。このような現状を鑑みて自分の場合は JSR-303 の採用は取りやめました。モデルレイヤで使うには良いのでしょうが、プレゼンテーションレイヤからの入力を検証するのに JSR-303 を使うと、コードの見通しが非常に悪くなり、実装も少々面倒だと判断したためです。

ふつうにバリデーションする

そんなわけでいつもリッチクライアントでやっているようにふつうにバリデーションすることにしました。「ふつう」というのは手続き的にバリデーションロジックを書くという意味です。何だかちょっと格好悪いですが、手続き的なバリデーションロジックを一箇所に纏めた方が見通しも良くなるので分かりやすいでしょう。単純には以下のようにすれば良いと思います(イメージです)。

public class TradeBeanValidator {

  public Violations validate(TradeBean bean) {

    Violations violations = new Violations();

    if (bean.getTradeNo() == null) {
      violations.addErrorToProperty("tradeNo", "Required");
    }
    if (bean.getRemarks != null && bean.getRemarks.length() > 2000) {
      violations.addErrorToProperty("remarks", "Must be less than or equal to 2000 characters");
    }
    if (bean.getCashFlows.isEmpty()) {
      violations.addError("CashFlow(s) must not be empty");
    }

    return violations;

}

Bean の状態を検査し、問題があればエラーメッセージを追加するだけです。違反メッセージはプロパティをキーにして取得したいことが多いと思うので専用のモデルを作ります。

ただまぁちょっと冗長ですよね。バリデーションロジックや違反メッセージも共通化したいところです。値に対する Validator を切り出してメッセージはプロパティファイルから読み込めば良いでしょう。自分で作る場合、このように考えてプロジェクトの度にちょっとしたバリデーション機能を作り込むことが多いのではないでしょうか。

というわけで今回もいつも通りふつうにバリデーション機能を作ったのですが、(半ば Advent Calendar のネタを作るために)ちょっとだけ独立させてみたのが GuardMan です。週末ちょっと時間があったので作ってみました。バリデーションなのでライブラリ化する意味があるほど大したことはしてないんですが、単純なバリデーションの実装がちょっとだけ楽になる API を実装しています。デフォルトではこんな感じです。あまり黒魔術的な感じにはしておらず、素朴で素直な感じです。

BeanValidationContext<Trade> context = new BeanValidationContext<>(bean);
context.<String> property("tradeNo").required().validate(
  minLength(0),
  maxLength(10),
  alphaNumeric(false)
);
context.<String> property("remarks").validate(maxLength(1000));
context.<List<CashFlow>> property("cashFlows").required().validate(notEmpty());
for (CashFlow cashFlow : bean.getCashFlows()) {
    BeanValidationContext<CashFlow> c = new BeanValidationContext<>(String.format("CashFlow %d", cashFlow.getSeqNo()), cashFlow);
    c.<Integer> property("seqNo").required().validate(max(100));
    c.<BigDecimal> property("amount").required().validate(
      min(BigDecimal.ZERO),
      max(new BigDecimal("100000000"))
    );
    c.<Date> property("startDate").required().lt(cashFlow.getEndDate()).params("End Date");
    c.<Date> property("endDate").required();
    context.addViolations("cashFlows", c);
}
for (Violation violation : context) {
    System.out.println(violation.getKey());
    System.out.println(violation.getSeverity());
    System.out.println(violation.getMessage());
}

context.property("propertyName") でプロパティに対するバリデーションを実行出来ます。また結果は Violation という形式で key / severity / message を保持しています。BeanValidationContextViolations を保持しており、様々な形で取り出すことが可能です。バリデーションロジックは ValueValidator を実装することで追加・拡張出来ます。

上記の例では個別に Validator を生成していますが、ドメインに対応した Validator を定義して共有することが多いと思います。その場合は以下のように複数の ValueValidator を組み合わせると良いでしょう。

public static final ValueValidator<String> TRADE_NO_VALIDATOR = validators(
  minLength(0), 
  maxLength(10),
  alphaNumeric(false)
);

なお当然ですが i18n 対応もしているのでメッセージも自由に変更出来ます。さらにオマケとして guardman-generator という APT も付けています。上記の例では Bean のプロパティ名が変更されたときにコンパイルエラーにならないので、修正が漏れるケースがあります。そこで APT を使ってメタデータを利用すると少しだけ保守に優しいプログラムになります。guardman-generator を使って Bean に @Guard アノテーションを注釈するとメタクラスが生成されるので、これを使うと先ほどのバリデーションはこのように書くことが出来ます。

TradeMeta m = TradeMeta.get();
BeanValidationContext<Trade> context = new BeanValidationContext<>(bean);
context.property(m.tradeNo).required().validate(
  minLength(0),
  maxLength(10),
  alphaNumeric(false)
);
context.property(m.remarks).validate(maxLength(1000));
context.property(m.cashFlows).required().validate(notEmpty());
CashFlowMeta cm = CashFlowMeta.get();
for (CashFlow cashFlow : bean.getCashFlows()) {
    BeanValidationContext<CashFlow> c = new BeanValidationContext<>(String.format("CashFlow %d", cashFlow.getSeqNo()), cashFlow);
    c.property(cm.seqNo).required().validate(max(100));
    c.property(cm.amount).required().validate(
      min(BigDecimal.ZERO),
      max(new BigDecimal("100000000"))
    );
    c.property(cm.startDate).required().lt(cm.endDate);
    c.property(cm.endDate).required();
    context.addViolations("cashFlows", c);
}

メタクラスのプロパティを使ってバリデーションを実行しているので、プロパティが変更された場合はバリデーション実装箇所が自動的にコンパイルエラーになってくれます。

というわけで最近のバリデーション事情という小ネタでした。もし良ければ簡単にインストール出来るので遊んでみて下さい。ちなみに Validator が Predicate を実装してほしかったので Google Guava に依存しちゃってます。個人的には数年前から Java で開発する際に Guava が無い状態というのは考えづらい状態なのでこうしています。同様の理由で、JDK7 未満のバージョンの Java もここ数年使った記憶が無いので JDK7+ を対象にしていますスミマセン。多分 APT を除けばそのまま JDK6 で動くと思うので、もし使いたい方が居られたらご連絡頂ければ対応します。

それでは明日クリスマスイブの担当は eiryu さんです。

10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?