今回の範囲
今回はサーブレットで最も面倒な処理であるパラメータの処理をリフレクションAPIを使って改善していきます
パラメータとモデルのフィールドの紐づけ
サーブレットAPIの問題
サーブレットでのパラメータ処理と言えば
Book newBook = new Book();
newBook.setTitle(request.getParameter("title"));
newBook.setAuthor(request.getParameter("author"));
newBook.setPrice(Integer.valueOf(request.getParameter("price")));
DateFormat dateFormat = new SimpleDateFormat();
newBook.setPublished(dateFormat.parse(request.getParameter("published")));
// 以下この退屈で冗長なコードが続く
このようにひたすら煩雑で面倒なものになりがちです
ここにある問題を整理してみましょう
・リクエストパラメータを対応するフィールドに代入するだけの単純な作業にも関わらず、記述が冗長になってしまう
・特にリクエストパラメータは常に文字列のため、変換を伴う場合はさらに面倒さが増す(特に日付など)
少なくとも前者はリフレクションAPIを活用することで解決することができます
早速対策を考えていきましょう
ではどういうコードなら嬉しいのか
結局のところここで私たちがやりたいことは「モデルのフィールドに対応するリクエストパラメータをセットする」ことです
だとしたらこういう風に書けると素敵だと思いませんか?
// フィールドに対応するリクエストパラメータを自動でセットしてもらう
// requestは自作のHttpServletRequestのラッパであるRequestクラス
Book newBook = request.bindParams(Book.class);
ここではBookのクラスを指定して、この中にパラメータの値を詰めてくれと頼んでいるわけですね
さて、早速どうやって実現するか考えていきましょう
最初に仕様ありき
ではまず仕様を考えましょう
・リクエストパラメータと同じ名前を持つフィールドに、その値を流し込みます
(マスアサインメント対策や同じ名前にできない状況はいったん置いておきます)
・そのフィールドの型がStringでない場合、型変換をしてくれるようにします
・対応するフィールドが1つもない場合は、空のインスタンス(インスタンス化されてから何も変更されていないインスタンス)を返します
コーディング開始
では、早速はじめましょう
まずはメソッドの定義です
public <T> T bindParams(Class<T> type)
Tというのは総称型と呼ばれる仕組みで、これを使うことで戻り値をキャストする必要がなくなったりします
次は指定されたクラスのインスタンスを作ります
T instance = type.newInstance();
次にパラメータに対応するフィールドを探していきます
// wrapedRequestはHttpServletRequest
for(String name:wrapedRequest.getParameterNames()){
// リフレクションAPIにはhasFieldのようなメソッドが無いので少し汚いコードになる
try {
// パラメータ名と同じ名前のフィールドがあれば取得。無ければ例外
Field field = type.getDeclaredField(name);
// フィールドがあれば、パラメータの値を流し込みたい
// メソッドの中身は後述
setFieldValue(instance, field, wrapedRequest.getParameter(name));
} catch {
// 例外に入ってもそれは正常フローなので無視
}
例外を分岐代わりに使っているのは気持ちが悪いですが、致し方ありません
何はともあれ、これで同じ名前のフィールドを手に入れるところまでは来ました
後はこのフィールドにパラメータを流し込んでやるだけです
private void setFieldValue(Object instance, Field field, String value) {
// フィールドの型がString以外の場合は適切な型に変換してやる
Object setValue;
Class<?> fieldType = field.getType();
if(fieldType == String.class) {
// フィールドがStringの場合はそのまま使う
setValue = value;
} else if(fieldType == Byte.TYPE || fieldType == Byte.class) {
// フィールドがbyteかByteの場合
setValue = Byte.valueOf(value);
} else if(fieldType == Integer.TYPE || fieldType == Integer.class) {
// フィールドがintかIntegerの場合
setValue = Integer.valueOf(value);
// 中略
}
// 変換した値をフィールドにセットしてやる
try {
field.set(instance, setValue);
} catch (IllegalArgumentException | IllegalAccessException e){
// セットに失敗するとここに来る
// 大体型が合わないか、そのフィールドへのアクセスが許可されないかのどちらか
}
とまあこんな感じです
少しばかり面倒な作業ですが、ここでその面倒さを我慢すればその後は快適なパラメータ処理を行えるようになります
この方法の問題点
さて、ここまで来て言うのもなんですがこの方法は若干問題があります
というのも通常フィールドはprivateにし、アクセサメソッドでそのフィールドにアクセスするのが普通です
しかしここでは直接フィールドにアクセスしようとしており、Javaのルール(あるいはマナー)に反しています
また、実行環境によってはそのprivateフィールドへのアクセスが出来ないこともあります
ここでは説明の簡略化のためにこうした方式にしましたが、本来ならsetterメソッドを探し、そのメソッド経由で値のセットを行うべきだということは申し伝えておきます
この設計の問題点
また、処理の流れをわかりやすくするために設計も簡略化してあります
少なくともパラメータを適切な型に変換する処理はcovertFieldValueなどというメソッドに切り分けるべきです
また、この変換処理は再利用性が高いと思いますので別クラスに切り出すのも考え方の1つです
その場合はTypeConverter#convert(type:Class, value:String):Object
のようになるかと思います
モデルとパラメータのバインド処理自体を別クラスに委譲するかは議論の余地がありますね
結び
いかがだったでしょうか
前回よりはいくらか実践的な内容だったとは思いますが
蛇足ですが、今回やった変換処理などはOgnlというライブラリを使うともっと簡単に実現できます
内部でやっていることは概ね同じですが、自分で1から処理を書く必要がなくなるので、もし実際に自分でここで書いたようなWebフレームワークを作りたい、という方は試してみてください