読者です 読者をやめる 読者になる 読者になる

getterやsetter以外のメソッドをJavaBeansのアクセッサにする

ネット上で、Java への dis として「なんで Java のプロパティアクセッサはわざわざ get/set で始まる名前のメソッドにしないといけないんだ!」というのをよく見かけます。ですが実は JavaBeans の仕様としてはいわゆる getter/setter でないメソッドもプロパティのアクセッサにすることができます。余りにも JavaBeans のことがボロクソに言われるのでかっとなって書きましたw

まずは以下の Java クラスを見てください。

/**
 * getter/setterを使わないJavaBeansの例.
 */
public class Test {
    private String わーい;

    public String すごーい() {
        return わーい;
    }

    public void たーのしー(String myProperty) {
        this.わーい = myProperty;
    }

    @Override
    public String toString() {
        return "Test{" +
                "わーい='" + わーい + '\'' +
                '}';
    }
}

いわゆる getter/setter がありませんね。このクラスを次のように Introspector に掛けてプロパティを抽出し、実行してみます。

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;

/**
 * Beanの利用例.
 */
public class JavaBeansSampleApp {
    public static void main(String[] args) {
        try {
            Test test = new Test();
            BeanInfo testBeanInfo = Introspector.getBeanInfo(test.getClass());
            PropertyDescriptor[] propertyDescriptors = testBeanInfo.getPropertyDescriptors();
            for (PropertyDescriptor pd : propertyDescriptors) {
                if ("わーい".equals(pd.getName())) {
                    System.out.println("わーいプロパティのsetter: " + pd.getWriteMethod());
                    System.out.println("わーいプロパティのgetter: " + pd.getReadMethod());
                    pd.getWriteMethod().invoke(test, "なにこれー?");
                }
            }
            System.out.println(test);
        } catch (IntrospectionException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

結果は次のようになります。

わーいプロパティのsetter: public void aoetk.sample.Test.たーのしー(java.lang.String)
わーいプロパティのgetter: public java.lang.String aoetk.sample.Test.すごーい()
Test{わーい='なにこれー?'}

set で始まっていない たーのしー() メソッドが setter として認識され、get で始まっていない すごーい() メソッドが getter として認識されています。プロパティの writeMethod を実行するとちゃんと たーのしー() メソッドが実行されて値が設定されています。

からくりについて解説しましょう。次のように Bean に対応した BeanInfo 実装クラスを用意し、Bean と同じパッケージに置きます。以下の例では BeanInfo インターフェースのデフォルトメソッドを実装した SimpleBeanInfo クラスを継承し、必要なメソッドだけ実装しています。

/**
 * {@link Test}のJavaBeans情報.
 */
public class TestBeanInfo extends SimpleBeanInfo {
    @Override
    public PropertyDescriptor[] getPropertyDescriptors() {
        try {
            return new PropertyDescriptor[]{
                    new PropertyDescriptor("わーい", Test.class, "すごーい", "たーのしー")
            };
        } catch (IntrospectionException e) {
            return null;
        }
    }
}

ここでは getPropertyDescriptors() メソッドをオーバーライドし、 PropertyDescriptorインスタンスを返しています。もうお分かりですね。ここでこの JavaBean のプロパティの情報 (プロパティ名、アクセッサメソッド) を返しているわけです。

Introspector の API ドキュメント には次のような解説が記載されています。

Fooクラスについては、情報の問い合わせ時にnull以外の値を提供するFooBeanInfoクラスがあれば、明示的な情報を取得できます。まず、ターゲットのBeanクラスの完全指定されたパッケージ名に「BeanInfo」を付加して新規のクラス名とし、BeanInfoクラスを検索します。これに失敗した場合は、この完全指定されたパッケージ名の最後のクラス名にあたる部分を使って、BeanInfoパッケージ検索パスに指定されたパッケージごとに該当クラスを検索します。
(中略)
クラスの明示的なBeanInfoが見つからない場合は、低レベルのリフレクションを使ってクラスのメソッドを調べ、標準設計パターンを適用して、プロパティのアクセス用メソッド、イベント・ソース・メソッド、publicメソッドを識別します。
(以下略)

つまり、JavaBeans を作るとき、本来は対になる BeanInfo 実装クラスを用意し、Bean の情報を提供する必要があるのです。ですが、毎回それをやっては面倒なので、便宜的にあの命名規則が用意されているわけです。CoC の先駆けだったわけですね (ちなみに JavaBeans は 20 世紀に誕生した仕様です) 。

この BeanInfo ですが次のような情報を提供することができます。

  • プロパティの情報
  • イベントの情報
  • メソッドの情報
  • アイコン

アイコンとかイベントって何?と思いましたか。JavaBeans は本来可搬性のあるソフトウェアコンポーネント、特に GUI コンポーネントを開発するために作られた規格です。つまり、IDE の「ポトペタツール」に追加して使えるソフトウェア部品を作るためのものなのです。BeanInfo ではそのツールに対して提供する情報を定義します。

f:id:aoe-tk:20170426222842p:plain

こうして考えると、単なる DTO として JavaBeans を使うのはオーバースペックであることも分かりますね。なぜこんなにあちこちで JavaBeans が使われるようになったのかは歴史的な経緯が色々あるのですが、まあそれはまたの機会で。

なお、このエントリでやったいたずらは業務のコードではやらないでくださいね。いたずらに混乱を招くだけです。酒の席でのネタにとどめてくださいw

全体のコードは gist にアップしてあります。

https://gist.github.com/aoetk/e5a09f67f2ebbc206d0770b21116b69e