@ledsun blog

無味の味は佳境に入らざればすなわち知れず

Javaのソースを読む BeansUtil.populate()メソッド

strtusではリクエストからActionFormにデータを格納するのにBeansUtil.populate()メソッドを使います。このメソッドは任意のMapを指定してJavaBeanのプロパティに値を設定するという使い方ができます。使い方はcommons-beanutils 基礎の基礎 - 倭マン日記で紹介されています。

// サンプルの Bean
Person person = new Person();

// サンプルの Map
Map<String, String> props = new HashMap<String, String>();
props.put("name", "倭マン");
props.put("age", "97");

// プロパティを一括セット
BeanUtils.populate(person, props);

ソースを見るとこんな感じです。処理本体はBeanUtilsBean.populate()メソッドに実装されています。

public static void populate(Object bean, Map properties)
    throws IllegalAccessException, InvocationTargetException {
    
    BeanUtilsBean.getInstance().populate(bean, properties);
}

特に変わったところはありません。MapからentrySet()を取ってきて、ぐるぐる回してsetPropertyメソッドでbeanに値を設定していきます。

public void populate(Object bean, Map properties)
    throws IllegalAccessException, InvocationTargetException {

    // Do nothing unless both arguments have been specified
    if ((bean == null) || (properties == null)) {
        return;
    }
    if (log.isDebugEnabled()) {
        log.debug("BeanUtils.populate(" + bean + ", " +
                properties + ")");
    }

    // Loop through the property name/value pairs to be set
    Iterator entries = properties.entrySet().iterator();
    while (entries.hasNext()) {

        // Identify the property name and value(s) to be assigned
        Map.Entry entry = (Map.Entry)entries.next();
        String name = (String) entry.getKey();
        if (name == null) {
            continue;
        }

        // Perform the assignment for this property
        setProperty(bean, name, entry.getValue());

    }

}

setPropertyでは4つの処理を行っています。

  1. プロパティの取得(ネストしたプロパティに対応するためResolverを使ってる。)
  2. beanの型と設定するプロパティの型を取得(beanの型で分岐するゴリゴリの処理)
  3. 設定するプロパティの型に合わせてvalueを型変換(変換自体はConvertUtilsBeanを使用)
  4. プロパティに値を設定
public void setProperty(Object bean, String name, Object value)
    throws IllegalAccessException, InvocationTargetException {

    // Trace logging (if enabled)
    if (log.isTraceEnabled()) {
        StringBuffer sb = new StringBuffer("  setProperty(");
        sb.append(bean);
        sb.append(", ");
        sb.append(name);
        sb.append(", ");
        if (value == null) {
            sb.append("<NULL>");
        } else if (value instanceof String) {
            sb.append((String) value);
        } else if (value instanceof String[]) {
            String[] values = (String[]) value;
            sb.append('[');
            for (int i = 0; i < values.length; i++) {
                if (i > 0) {
                    sb.append(',');
                }
                sb.append(values[i]);
            }
            sb.append(']');
        } else {
            sb.append(value.toString());
        }
        sb.append(')');
        log.trace(sb.toString());
    }

    // Resolve any nested expression to get the actual target bean
    Object target = bean;
    Resolver resolver = getPropertyUtils().getResolver();
    while (resolver.hasNested(name)) {
        try {
            target = getPropertyUtils().getProperty(target, resolver.next(name));
            name = resolver.remove(name);
        } catch (NoSuchMethodException e) {
            return; // Skip this property setter
        }
    }
    if (log.isTraceEnabled()) {
        log.trace("    Target bean = " + target);
        log.trace("    Target name = " + name);
    }

    // Declare local variables we will require
    String propName = resolver.getProperty(name); // Simple name of target property
    Class type = null;                            // Java type of target property
    int index  = resolver.getIndex(name);         // Indexed subscript value (if any)
    String key = resolver.getKey(name);           // Mapped key value (if any)

    // Calculate the property type
    if (target instanceof DynaBean) {
        DynaClass dynaClass = ((DynaBean) target).getDynaClass();
        DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
        if (dynaProperty == null) {
            return; // Skip this property setter
        }
        type = dynaProperty.getType();
    } else if (target instanceof Map) {
        type = Object.class;
    } else if (target != null && target.getClass().isArray() && index >= 0) {
        type = Array.get(target, index).getClass();
    } else {
        PropertyDescriptor descriptor = null;
        try {
            descriptor =
                getPropertyUtils().getPropertyDescriptor(target, name);
            if (descriptor == null) {
                return; // Skip this property setter
            }
        } catch (NoSuchMethodException e) {
            return; // Skip this property setter
        }
        if (descriptor instanceof MappedPropertyDescriptor) {
            if (((MappedPropertyDescriptor) descriptor).getMappedWriteMethod() == null) {
                if (log.isDebugEnabled()) {
                    log.debug("Skipping read-only property");
                }
                return; // Read-only, skip this property setter
            }
            type = ((MappedPropertyDescriptor) descriptor).
                getMappedPropertyType();
        } else if (index >= 0 && descriptor instanceof IndexedPropertyDescriptor) {
            if (((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod() == null) {
                if (log.isDebugEnabled()) {
                    log.debug("Skipping read-only property");
                }
                return; // Read-only, skip this property setter
            }
            type = ((IndexedPropertyDescriptor) descriptor).
                getIndexedPropertyType();
        } else if (key != null) {
            if (descriptor.getReadMethod() == null) {
                if (log.isDebugEnabled()) {
                    log.debug("Skipping read-only property");
                }
                return; // Read-only, skip this property setter
            }
            type = (value == null) ? Object.class : value.getClass();
        } else {
            if (descriptor.getWriteMethod() == null) {
                if (log.isDebugEnabled()) {
                    log.debug("Skipping read-only property");
                }
                return; // Read-only, skip this property setter
            }
            type = descriptor.getPropertyType();
        }
    }

    // Convert the specified value to the required type
    Object newValue = null;
    if (type.isArray() && (index < 0)) { // Scalar value into array
        if (value == null) {
            String[] values = new String[1];
            values[0] = null;
            newValue = getConvertUtils().convert(values, type);
        } else if (value instanceof String) {
            newValue = getConvertUtils().convert(value, type);
        } else if (value instanceof String[]) {
            newValue = getConvertUtils().convert((String[]) value, type);
        } else {
            newValue = convert(value, type);
        }
    } else if (type.isArray()) {         // Indexed value into array
        if (value instanceof String || value == null) {
            newValue = getConvertUtils().convert((String) value,
                                            type.getComponentType());
        } else if (value instanceof String[]) {
            newValue = getConvertUtils().convert(((String[]) value)[0],
                                            type.getComponentType());
        } else {
            newValue = convert(value, type.getComponentType());
        }
    } else {                             // Value into scalar
        if (value instanceof String) {
            newValue = getConvertUtils().convert((String) value, type);
        } else if (value instanceof String[]) {
            newValue = getConvertUtils().convert(((String[]) value)[0],
                                            type);
        } else {
            newValue = convert(value, type);
        }
    }

    // Invoke the setter method
    try {
      getPropertyUtils().setProperty(target, name, newValue);
    } catch (NoSuchMethodException e) {
        throw new InvocationTargetException
            (e, "Cannot set " + propName);
    }

}

処理が4つもあるのは「単一責任の原則」に反しているのでメソッドを分けた方がいいと思います。そもそもBeanUtilはstruts1.0用のクラスでした。それをcommons-beanutilsに持って来たため影響範囲が恐ろしく広いです*1。今更変更できないのかもしれません。技術的負債がフレームワークで公開されてしまうのは恐ろしいですね。

*1:つまり、すべてのバージョンのstrutsで使われている