Google
 
Web andore.com
Powered by SmartDoc

5. PropertyEditor, データバインディング, バリデーション, BeanWrapper (Ver 1.2.7)

5.1 イントロ

バリデーションがビジネスロジックを考慮すべきか否かは大きな問題だ。これには賛成、反対の両方の答えがあり、Springではそのどちらも除外しないバリデーション(とデータバインディング)のための設計を提示している。バリデーションは仕様的にウェブ層と結びつけられないようにすべきであり、簡単にローカライズできるべきであり、どんなバリデータでもプラグイン可能とすべきだ。上記を考慮して、Springでは基本的で尚且つアプリケーションの全ての層で利用可能なValidatorインタフェースを実現した。

データバインディングはユーザの入力をアプリケーション(もしくはユーザの入力を処理するために使うオブジェクト)のドメインモデルに動的にバインドできるようにする便利なものだ。Springではまさにそれを行う、いわゆるDataBinderが用意されている。ValidatorDataBinderはバリデーションパッケージをなし、これは主にMVCフレームワークで用いられるが、特にこれだけに限定されたものではない。

BeanWrapperはSpringフレームワークの基本的な概念であり、いろんな場面で利用される。しかしながら、おそらくBeanWrapperを直接使う必要性に駆られることはまだないと思う。でも、本書はリファレンスドキュメントなので、少しぐらい説明しないといけないと思う。本チャプターでBeanWrapperの説明をしているので、いづれにしても使うつもりがあるのであれば、データをオブジェクトにバインドしようとすると、BeanWrapperに強く関係があるので、おそらく使うことになるだろう。

SpringではPropertyEditorをいたるところで使っている。PropertyEditorのコンセプトはJavaBeans仕様の一部だ。BeanWrapperDataBinderと密接に関係しているのでBeanWrapperと同様に、本チャプターでPropertyEditorの説明をするのがベストだろう。

5.2 DataBinderを使ってデータをバインドする

DataBinderBeanWrapper(2)上に構築されている。

  1. 詳細はこのビーンに関するチャプターを参照して欲しい

5.3 ビーン操作とBeanWrapper

org.springframework.beansパッケージはSunによって提供されているJavaBeans標準に準拠したものだ。JavaBeanは、デフォルトで引数を持たないコンストラクタをもち、propという名前のプロパティに対してsetProp(...)というsetterとgetProp()というgetterという命名規約に従った単なるクラスだ。JavaBeansやその仕様についての詳細は、Sunのウェブサイトを参照して欲しい。

ビーンパッケージのとても重要な概念の1つはBeanWrapperインタフェースとそれに対応する実装(BeanWrapperImpl)だ。JavaDocから引用されるように、BeanWrapperは、プロパティの値を(個々に、もしくはまとめて)設定、取得する機能、プロパティデスクリプタを取得し、プロパティが読み出し可能か、あるいは書き込み可能かを問い合わせる機能を提供する。他にも、BeanWrapperは入れ子プロパティもサポートし、制限のない深さのサブプロパティに値の設定が可能だ。したがって、BeanWrapperはターゲットクラスに支援するコードがなくてもJavaBeans標準のPropertyChangeListenerVectoableChangeListenerにこれらの機能を追加するのをサポートする。重要なことを最後に述べるが、BeanWrapperはインデックスつきプロパティの設定のサポートを提供する。BeanWrapperは通常アプリケーションコードから直接使うのではなく、DataBinderBeanFactoryから使う。

BeanWrapperを動かす方法はその名前に示されている。つまり、プロパティの設定や検索のような動作をさせたいビーンをラップするのだ。

5.3.1 基本プロパティ、入れ子プロパティを設定、取得する

プロパティを設定したり、取得することはsetPropertyValue(s)getPropertyValue(s)メソッドを使って行う。これは共に2,3のオーバロードされた派生に属すものだ。これらは全てSpringのJavaDocにもっと詳細に書かれている。知っておくべき重要な点は、オブジェクトのプロパティを表すのに、2,3の書式がある、ということだ。いくつか例を挙げる。

プロパティの例
説明
name getName()もしくはisName()setName()のメソッドに対応するプロパティ名を表す
account.name accountプロパティにネストされたプロパティ名を表す。getAccount().setName()もしくはgetAccount().getName()のメソッドに対応する
account[2] インデックスが付与されたaccountプロパティの3番目の要素を表す。インデックスつきプロパティは配列やリスト、その他の通常順序つきコレクションの型にすることができる
account[COMPANYNAME] COMANYNAMEというキーでインデックスが付与されたMapプロパティaccountマップエントリの値を表す。

下記に、プロパティを設定、取得するためにBeanWrapperを使う例をお見せしよう。

注:もし、BeanWrapperを直接使うつもりがないのであれば、ここはあまり重要ではない。DataBinderBeanFactoryや他の優れた実装しか使わないのであれば、次のPropertyEditorのセクションに進んだ方がいい。

以下のような2つのクラスがあるとしよう。

public class Company {
    private String name;
    private Employee managingDirector;

    public String getName()	{ 
        return this.name; 
    }
    public void setName(String name) { 
        this.name = name; 
    } 
    public Employee getManagingDirector() { 
        return this.managingDirector; 
    }
    public void setManagingDirector(Employee managingDirector) {
        this.managingDirector = managingDirector;
    }
}
public class Employee {
    private float salary;

    public float getSalary() {
        return salary;
    }
    public void setSalary(float salary) {
        this.salary = salary;
    }
}

以下のコードはインスタンス化されたCompanyEmployeeのプロパティのいくつかを検索して操作する方法の例を示したものだ。

Company c = new Company();
BeanWrapper bwComp = BeanWrapperImpl(c);
// setting the company name...
bwComp.setPropertyValue("name", "Some Company Inc.");
// ... can also be done like this:
PropertyValue v = new PropertyValue("name", "Some Company Inc.");
bwComp.setPropertyValue(v);

// ok, let's create the director and tie it to the company:
Employee jim = new Employee();
BeanWrapper bwJim = BeanWrapperImpl(jim);
bwJim.setPropertyValue("name", "Jim Stravinsky");
bwComp.setPropertyValue("managingDirector", jim);

// retrieving the salary of the managingDirector through the company
Float salary = (Float)bwComp.getPropertyValue("managingDirector.salary");

5.3.2 型を変換する組み込みPropertyEditor

SpringではPropertyEditorの概念を頻繁に用いる。オブジェクトそのものとは違う方法でプロパティを表現できるようにするには手軽かもしれない。例えば、人間に判別可能な形式をオリジナルの日付に戻せることができる(あるいは、人間に判別可能な形式で入力された任意の日付をDateオブジェクトへ戻せるとよりベターだ)が、日付は人間に判別可能な方法で表現できる。この動作は、java.beans.PropertyEditor型のカスタムエディタを登録することによって可能になる。以前の章で述べたように、カスタムエディタをBeanWrapperかあるいは、特定のアプリケーションコンテキストに対して登録することによってプロパティを求められた型に変換する方法についての知識が得られるようになる。より詳細はSunから提供されているjava.beansパッケージのJavaDocにあるPropertyEditorについての記述を呼んで欲しい。

Springで使われているプロパティを編集する例を2,3示す。

Springには簡単に使える組み込みがPropertyEditors多数用意されている。下記にリストアップされた各々がそれだ。これらは全てorg.springframework.beans.propertyeditorsパッケージにある。全部ではないがほとんど(下記に示した)はデフォルトでBeanWrapperImplに登録されている。プロパティエディタはいろんな設定が可能なので、デフォルトで登録されているものをオーバライドして独自の派生を登録することももちろん可能だ。

Built-in PropertyEditors
クラス 説明
ByteArrayPropertyEditor バイト配列用エディタ。文字列は対応するバイト表現へ単純に変換される。これはデフォルトでBeanWrapperImplに登録されている。
ClassEditor クラスが実際のクラスであること、あるいは逆にそうでないことを説明する文字列を解析する。そのクラスが見つからない場合は、IllegalArgumentExceptionがスローされる。デフォルトでBeanWrapperImplに登録されている。
CustomBooleanEditor Boolean型プロパティ用にカスタマイズされたプロパティエディタ。デフォルトで、BeanWrapperImplに登録されているが、独自のインスタンスをカスタムエディタとして登録することによりオーバライドが可能。
CustomCollectionEditor 任意のソースCollectionを指定されたターゲットのCollection型へ変換するCollection用プロパティエディタ。
CustomDateEditor 独自のDateFormatをサポートする、java.util.Date用のカスタマイズ可能なプロパティエディタ。デフォルトでは登録されていない。必要とするフォーマットに合わせて登録する必要がある。
CustomNumberEditor カスタマイズ可能でInteger,Long,Float,DoubleのようなNumberのサブクラス用プロパティエディタ。デフォルトでBeanWrapperImplに登録されているが、独自のインスタンスをカスタムエディタとして登録することによりオーバライドが可能。
FileEditor 文字列をjava.io.Fileオブジェクトへ変換することが可能。BeanWrapperImplにデフォルトで登録されている。
InputStreamEditor テキスト文字列を受け取って(ResourceEditorResourceを中間媒体として)InputStreamを生成するワンウェイプロパティエディタ。従ってInputStreamプロパティは直接文字列として設定される。デフォルトの使い方は、InputStreamに限定しない点に注意してほしい。これは、BeanWrapperImplにデフォルトで登録されている。
LocaleEditor 文字列をLocaleオブジェクトへ、あるいはその逆に変換できる(文字列のフォーマットは、[言語]_[カントリー]_[時差]で、これはLocaletoString()メソッドが返すものと同じものだ)。これは、デフォルトでBeanWrapperImplに登録されている。
PropertiesEditor 文字列(java.lang.PropertyクラスのJavaDocで定義されているフォーマットを用いてフォーマットされたもの)をPropertiesオブジェクトへ変換できる。これはデフォルトでBeanWrapperImplに登録されている。
StringArrayPropertyEditor コンマで区切られた文字列のリストを文字列配列に、あるいはその逆に変換できる。これはデフォルトでBeanWrapperImplに登録されている。
StringTrimmerEditor 文字列をトリミングするプロパティエディタ。空文字列をnull値に変換することも可能。デフォルトでは登録されていないので必要に応じて登録する必要がある。
URLEditor URLの文字列表現を実際のURLオブジェクトへ変換ができる。これは、デフォルトでBeanWrapperImplに登録されている。

Springでは、プロパティエディタで必要になるかもしれないサーチパスを設定するのにjava.beans.PropertyEditorManagerを使う。このサーチパスは他にもsun.bean.editorsが含まれており、フォント、色などのプリミティブ型用PropertyEditorが含まれている。標準のJavaBeansインフラは、使っているクラスと同じパッケージにあり、その名前と同じ名前に'Editor'がついたものであれば、自動的にPropertyEditors(わざわざ登録する必要はない)を発見する。

5.3.3 触れておいた方がいいその他の機能

これまでのセクションで見てきた機能に加えて、興味を持つかもしれないクラスが2,3あるが、セクションをわざわざ設けるほどのものではないだろう。

5.4 SpringのValidatorインタフェースを使ったバリデーション

Springはオブジェクトをバリデートするために使うことができるをValidatorインタフェースを特徴とする。Validatorインタフェースは、かなり愚直で、Errorsオブジェクトといっしょに呼び出して動作する。言い換えると、バリデーションを実行している間、バリデータはバリデーションの失敗をErrorsオブジェクトに通知する。

すでに述べたように、Validatorインタフェースはちょうど、読者の方々が自分で実装するのと同じように、かなり愚直なものだ。小さなデータオブジェクトを取り上げてみよう。

public class Person {
  private String name;
  private int age;

  // the usual suspects: getters and setters
}

org.springframework.validation.Validatorインタフェースを使い、バリデーションの振る舞いをこのPersonクラスに追加する。これは、Validatorインタフェースだ。

バリデータを実装するのは、かなり愚直で、Springが別に提供しているValidationUtilsを知っている場合はなおさらだろう。バリデータを生成する方法について見てみよう。

public class PersonValidator implements Validator {
	
	public boolean supports(Class clzz) {
		return Person.class.equals(clzz);
	}
	
	public void validate(Object obj, Errors e) {
		ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
		Person p = (Person)obj;
		if (p.getAge() < 0) {
			e.rejectValue("age", "negativevalue");
		} else if (p.getAge() > 110) {
			e.rejectValue("age", "tooold");
		}
	}
}

これでわかるように、ValidationUtilsnameプロパティを拒否するのに用いられている。今ここで示した例で挙げた機能については、ValidationUtilsのJavaDocを参照してほしい。

5.5 Errorsインタフェース

バリデーションエラーは、バリデータに渡されるErrorsオブジェクトに通知される。Spring Web MVCでは、そのエラーメッセージを調べるのに、spring:bindタグを使うことができるが、もちろん自前でエラーオブジェクトを調べることもできる。そのメソッドはかなり愚直だ。詳細についてはJavaDocに載っている。

5.6 エラーコードをエラーメッセージに変換する

ここまで、データバインディングとバリデーションについて述べてきた。バリデーションエラーに対応するメッセージの出力は、あまり議論を必要とはしない。前述した例の中で、nameageフィールドをリジェクトした。もし、MessageSourceを使って、エラーメッセージを出力すると、フィールド(この場合'name''age')をリジェクトする際に指定したエラーコードを使って行うことになる。(直接、あるいは間接的に、例えばValidationUtilsクラスを使って)ErrorsインタフェースからrejectValue、もしくは別のリジェクトメソッドを叩けば、渡されたエラーコードだけでなく、多くのエラーコードを登録しなくていい実装になる。登録するエラーコードはこれを用いるMessageCodesResolverによって決定される。デフォルトでは、これは例えばメッセージとエラーコードだけでなく、リジェクトメソッドに渡すフィールド名を含んだメッセージを登録しないDefaultMEssageCodesResolverが用いられる。よって、rejectValue("age", "tooold")を使ってフィールドをリジェクトする場合、toooldのコードとは別に、Springは、tooold.agetooold.age.intも登録する(よって、最初のは、フィールド名が含まれており、2つ目のはフィールドの型が含まれている)。

MessageCodesResolverや、デフォルトのストラテジについての詳細は、MessageCodesResolverに関するJavaDoc(http://www.springframework.org/docs/api/org/springframework/validation/MessageCodesResolver.html)やDefaultMessageCodesResolverのJavaDoc(http://www.springframework.org/docs/api/org/springframework/validation/DefaultMessageCodesResolver.htmlを参照してほしい。