今天发现一个诡异的bug,根据前端请求的参数,返回的结果明显不符,最后发现是因为代码里使用了apache commons
包里的BeanUtils.copyProperties()
这个方法,写了几个测试用例,发现使用这个方法在从orig
对象取值,给dest
赋值的过程中,
默认情况下,如果dest
中属性类型是Integer
,orig
中对应字段是null
时, dest
会被赋值为0,其实,像Double
,Float
,BigDecimal
等这些类型都存在类似的问题。
在stackoverflow
和apache 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;
}
}
默认情况下,初始化后,默认提供的转换器定义如下,包括BigDecimal
,Integer
,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
的默认实现会指定defaultValue
为ZERO
,因此在调用它的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);
}
......
解决方案
- 提供指定类型的转换器,如官方回复指出的,注册自定义的转化器,可以实现将
orig
中的null
转换成dest
中null
,而不是0.
- 使用
PropertyUtils
,这个工具需要类型完全一致的赋值,不会存在转换,如果类型不符,会直接抛出异常,很多情况下,这正是我们需要的结果。
- 需要这种转换的场景其实并不常见,其实我们可以自己做转换,逐字段赋值,也未尝不可,或者优化bean的继承关系,避免转换。