BeanUtils.copyProperties()遇到的坑

今天发现一个诡异的bug,根据前端请求的参数,返回的结果明显不符,最后发现是因为代码里使用了apache commons 包里的BeanUtils.copyProperties()这个方法,写了几个测试用例,发现使用这个方法在从orig对象取值,给dest赋值的过程中, 默认情况下,如果dest中属性类型是Integerorig中对应字段是null时, dest会被赋值为0,其实,像Double,Float,BigDecimal等这些类型都存在类似的问题。

stackoverflowapache commons官方都有人遇到这个问题。
官方的回复:

BeanUtils has a converter for BigDecimal - but in the standard configuration it is not configured with a default value. You can simply register one that has a default to resolve your issue:
BigDecimal defaultValue = new BigDecimal(“0”);
Converter myConverter = new BigDecimalConverter(defaultValue);
ConvertUtils.register(myConverter, BigDecimal.class);

看来是对这个方法的使用不对,跟进源码:

/**
     * <p>Copy property values from the origin bean to the destination bean
     * for all cases where the property names are the same.</p>
     *
     * <p>For more details see <code>BeanUtilsBean</code>.</p>
     *
     * @param dest Destination bean whose properties are modified
     * @param orig Origin bean whose properties are retrieved
     *
     * @exception IllegalAccessException if the caller does not have
     *  access to the property accessor method
     * @exception IllegalArgumentException if the <code>dest</code> or
     *  <code>orig</code> argument is null or if the <code>dest</code> 
     *  property type is different from the source type and the relevant
     *  converter has not been registered.
     * @exception InvocationTargetException if the property accessor method
     *  throws an exception
     * @see BeanUtilsBean#copyProperties
     */
    public static void copyProperties(Object dest, Object orig)
        throws IllegalAccessException, InvocationTargetException {
        BeanUtilsBean.getInstance().copyProperties(dest, orig);
    }

对于每个orig中的匹配的成员变量,都会调用convert()方法,转换成dest中对应的类型,转换的规则就是 已经注册的一系列Converter定义的。

/**
     * <p>Convert the value to an object of the specified class (if
     * possible).</p>
     *
     * @param value Value to be converted (may be null)
     * @param type Class of the value to be converted to
     * @return The converted value
     *
     * @exception ConversionException if thrown by an underlying Converter
     * @since 1.8.0
     */
    protected Object convert(Object value, Class type) {
        Converter converter = getConvertUtils().lookup(type);
        if (converter != null) {
            log.trace("        USING CONVERTER " + converter);
            return converter.convert(type, value);
        } else {
            return value;
        }
    }

默认情况下,初始化后,默认提供的转换器定义如下,包括BigDecimalInteger,Boolean等类型都有默认实现。

/** Construct a bean with standard converters registered */
    public ConvertUtilsBean() {
        converters.setFast(false);   
        deregister();
        converters.setFast(true);
    }
/**
     * Remove all registered {@link Converter}s, and re-establish the
     * standard Converters.
     */
    public void deregister() {

        converters.clear();
        
        registerPrimitives(false);
        registerStandard(false, false);
        registerOther(true);
        registerArrays(false, 0);
        register(BigDecimal.class, new BigDecimalConverter());
        register(BigInteger.class, new BigIntegerConverter());
    }
private void registerPrimitives(boolean throwException) {
        register(Boolean.TYPE,   throwException ? new BooleanConverter()    : new BooleanConverter(Boolean.FALSE));
        register(Byte.TYPE,      throwException ? new ByteConverter()       : new ByteConverter(ZERO));
        register(Character.TYPE, throwException ? new CharacterConverter()  : new CharacterConverter(SPACE));
        register(Double.TYPE,    throwException ? new DoubleConverter()     : new DoubleConverter(ZERO));
        register(Float.TYPE,     throwException ? new FloatConverter()      : new FloatConverter(ZERO));
        register(Integer.TYPE,   throwException ? new IntegerConverter()    : new IntegerConverter(ZERO));
        register(Long.TYPE,      throwException ? new LongConverter()       : new LongConverter(ZERO));
        register(Short.TYPE,     throwException ? new ShortConverter()      : new ShortConverter(ZERO));
    }

IntegerConverter的默认实现会指定defaultValueZERO,因此在调用它的convert()方法时,如果value=null,就会使用默认值。

protected Object handleMissing(Class type) {

        if (useDefault || type.equals(String.class)) {
            Object value = getDefault(type);
            if (useDefault && value != null && !(type.equals(value.getClass()))) {
                try {
                    value = convertToType(type, defaultValue);
                } catch (Throwable t) {
                    log().error("    Default conversion to " + toString(type)
                            + "failed: " + t);
                }
                ......

解决方案

  1. 提供指定类型的转换器,如官方回复指出的,注册自定义的转化器,可以实现将orig中的null转换成destnull,而不是0.
  2. 使用PropertyUtils,这个工具需要类型完全一致的赋值,不会存在转换,如果类型不符,会直接抛出异常,很多情况下,这正是我们需要的结果。
  3. 需要这种转换的场景其实并不常见,其实我们可以自己做转换,逐字段赋值,也未尝不可,或者优化bean的继承关系,避免转换。

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @sxzhou Nov 28, 2017

奉献爱心