Commons Beanutils の不思議

Commons Beanutils の PropertyUtils.setSimpleProperty() とか使ってて、おやっ、と不思議だったのでメモ。

謎な現象

以下のようなおかしな命名規則のBeanがあり、setSimpleProperty()にて値を設定した場合、

public class Bean {
    private String lll; // 全部小文字
    private String UUU; // 全部大文字
    private String Ull; // 先頭が大文字

    public void setLll(String lll) { this.lll = lll; }
    public void setUUU(String uuu) { this.UUU = uuu; }
    public void setUll(String ull) { this.Ull = ull; }
    以下略


以下はもちろん普通に上手くいき、Bean の該当プロパティに"hoge"が設定されます。

    Bean b = new Bean();
    PropertyUtils.setSimpleProperty(b, "lll", "hoge");
    PropertyUtils.setSimpleProperty(b, "UUU", "hoge");


でも、

    PropertyUtils.setSimpleProperty(b, "Ull", "hoge");

とすると、java.lang.NoSuchMethodException: Unknown property 'Ull' と例外になってしまいます。


そこで、先頭文字を小文字にすると、

    PropertyUtils.setSimpleProperty(b, "ull", "hoge");

何事もなく上手くいきます。


setter 名を作るために、先頭文字を toUpperCase してるんだろうなぁと思い、以下を試すと

    PropertyUtils.setSimpleProperty(b, "uUU", "hoge");

あれれ? java.lang.NoSuchMethodException: Unknown property 'uUU' 失敗します・・謎です。
そういえば、昔URLのパラメータで先頭文字が大文字だと上手くいかなかったからURLパラメータにフィルタかました・・ なんてのを聞いたのを思い出しつつ謎を追います。

謎を追う

意味不明なので Beanutils のソースを追います。
setSimpleProperty() の呼出は、内部で org.apache.commons.beanutils.PropertyUtilsBean を使用しています。PropertyUtilsBean 内部では、Beanのプロパティの管理にjava.beans.PropertyDescriptor を使用しており、PropertyDescriptorをキャッシュしていたりします。
ここからは、Beanutils の世界ではなく、Javaの標準APIの世界に入っていきます。
PropertyUtilsBean を取得するために使われているのが、java.beans.BeanInfo になり、BeanInfo は、Introspector.getBeanInfo() として取得されます。さらに深く追っていくと、Introspector.decapitalize() というメソッドで、Beanのプロパティ名を編集しています。このメソッドは以下の様になっており、プロパティ名のプレフィックスである"set"などを除去した文字列が渡されます。

    public static String decapitalize(String name) {
        if (name == null || name.length() == 0) {
	    return name;
	}
	if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
			Character.isUpperCase(name.charAt(0))){
	    return name;
	}
	char chars[] = name.toCharArray();
	chars[0] = Character.toLowerCase(chars[0]);
	return new String(chars);
    }


なるほど、謎は解けました。
プロパティ名の先頭の2文字が大文字の場合は、そのままプロパティ名として利用。そうでない場合は先頭文字を小文字にしてプロパティ名とする、なんてことがされています。
つまり、getUUUの場合、先頭2文字が大文字なので、プロパティ名はUUU
getUllの場合、1文字目を小文字にして、ullがプロパティ名となります。

JavaDocを見ると

文字列を引数に取り、通常の Java 変数名の大文字使用法に従って変換するユーティリティメソッドです。通常は最初の文字を大文字から小文字に変換しますが、この変換を行わない特殊なケースもあります (複数の文字が存在し、先頭の文字と 2 番目の文字がどちらも大文字である場合など)

とあり、URL などをgetURLとして普通に使いたいためにこんな仕様になっている模様・・やるなら全部大文字の場合、とかでいいんじゃない? という気がしますが。

対応方法は、

仕組みが分かればあとは簡単で、以下の様にすれば上手くいきます。

    PropertyUtils.setSimpleProperty(b, Introspector.decapitalize("Ull"), "hoge");

色々な仕様の縛りで、Bean名に則さない名前を使わなければならなくなった時には思い出して下さい。BeanUtilsの不思議としましたが、根っこはJava標準APIの中でしたね。