陷阱,看上去很美
慎用BeanUtils.copyProperties(Object,Object)转换Bean(VO,PO)
问题描述:
最近在帮忙ERefund DEBUG一些问题。在ERefund里出现这样一个问题,前台一个NumberField不输入任何值,但是在后台查询数据库的时候却是以0去作为查询条件来查询数据,这样查出的结果肯定跟用户期望的结果不一致。
起初,怀疑是DWR或者数据类型在作祟,在将UI上JavaScript的值传给后台时赋了默认值?但是DEBUG后发现,事实并非如此,DWR并没有强加默认值,所以可以排除DWR了。Façade层再没有任何针对该VO的操作了,所以Façade可以PASS了。而且检查了相应的Bean,所有属性都没有定义简单数据类型,比如long、int、double、float之类的,所以也排除了使用了简单数据类型的错误。
“杯具”往往由“洗具”开始,这话真不假。
接着,又大略浏览了一下整个VO在Service层传输之后,我似乎发现了有可能导致问题的关键点。那就是在Service层里,前台的VO和后台数据库操作的PO会有一个转换(背景:整个项目是MVC架构,Struts+Spring+iBatis,前台和后台各有各自的VO,无可厚非,而且这样做避免了前台的VO一捅到底,将PO直接暴露给客户,也避免了将来UI的变化导致的牵一动百的“杯具”),在做VO和PO的转换时,由于前台的VO和后台的PO属性完全相同,所以只用一句代码就搞定了VO和PO的转换,即:BeanUtils.copyProperties(PO,VO)或者BeanUtilsBean.getInstance().copyProperties(PO,VO),其实这两个最终的结果是一样的,BeanUtils的copyPreperties(PO,VO)方法就是调用的BeanUtilsBean的copyPreperties(PO,VO)。其实这是“洗具”,因为一个庞大的VO和PO就用一句代码搞定了他俩的转换,一是省去了写大量的get/set方法减少了大量代码,二是代码更加易读,是可喜可贺的。但是“杯具”开始了,我看了下这两个类所在Jar(commons-beanutils.jar)的源代码,发现它会去根据这个属性的type在ConvertUtilsBean里lookup(Converter是一个接口)而在ConvertUtilsBean的构造方法里面有如下代码:
/* 151*/ defaultBoolean = Boolean.FALSE;
/* 179*/ defaultByte = new Byte((byte)0);
/* 207*/ defaultCharacter = new Character(‘ ‘);
/* 237*/ defaultDouble = new Double(0.0D);
/* 265*/ defaultFloat = new Float(0.0F);
/* 293*/ defaultInteger = new Integer(0);
/* 321*/ defaultLong = new Long(0L);
很显然,这就是“杯具”的发源地咯。它会自己将Boolean、Byte、Character、Double、Float、Integer、Long型初始化,并且赋予默认值。事实摆在眼前了,百口莫辩了,导致问题的根源就是这个BeanUtils.copyProperties(PO,VO)啦。
解决方案:
其实在commons-beanutils.jar里面也提供了另外一个用来作为Bean转换的工具类:PropertyUtils.copyProperties(PO,VO),这个方法就不会给Boolean、Byte、Character、Double、Float、Integer、Long这些类型赋默认值。
用PropertyUtils.copyProperties方法来转换Bean固然省事,但对需要转换的两个Bean的要求也高,那就是VO和PO里面需要转换的属性必须完全一致,如果出现不一致,将不会被转换或者会出现Exception,我试过一个属性数相同,但是最后一个属性不同名称,测试结果是,其他属性都转换了,最后一个属性不会转换。
BeanUtils.copyProperties方法和PropertyUtils.copyProperties方法的大概区别是:前者会给某些类型强加默认值,后者不会;前者会自动智能进行类型转换(在允许转换的类型中)只要属性名一样,后者不会,也正因为如此,后者在处理效率上会比前者快。而这两者都是基于反射的机制,所以相比get/set效率都会略有下降。
除了这两种转换Bean的简单办法(不用自己写get/set方法来转换)之外,还有一个Dozer的DozerBeanMapper可以做,而且功能很强大,比上述两种都强大。通过在XML文件里灵活的配置,可以达到不同对象间的拷贝,而且如果两个对象属性基本一致,那你只需要将属性名称不一致的匹配写在XML文件里面,其他一致的,你可以不写,但需要将wildcard配置为true,就是意味着对所有属性都进行转换。如果为false,则只对在XML文件中配置的属性进行转换。
测试目的:
测试在做VO和PO转换时,是否会对NULLABLE的属性强加默认值。
测试用例:
package test;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.PropertyUtils;
import org.dozer.DozerBeanMapper;
/*
* Copyright(C) 2010.
* Application : strutsT
* Class Name : .TestBeanUtils
* Date Created: Jul 13, 2010
* Author : Joe Jiang
*/
/**
* @author Joe Jiang
*
*/
public class TestBeanUtils {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
TestVO tvo = new TestVO();
System.out.println(tvo.toString());
ATestVO atvo = new ATestVO();
BeanUtils.copyProperties(atvo, tvo);
System.out.println(atvo.toString());
ATestVO btvo = new ATestVO();
BeanUtilsBean bu = BeanUtilsBean.getInstance();
bu.copyProperties(btvo, tvo);
System.out.println(btvo.toString());
ATestVO ctvo = new ATestVO();
PropertyUtils.copyProperties(ctvo, tvo);
System.out.println(ctvo.toString());
ATestVO dtvo = new ATestVO();
DozerBeanMapper mapper = new DozerBeanMapper();
List myMappingFiles = new ArrayList();
myMappingFiles.add("dozerBeanMapping.xml");
mapper.setMappingFiles(myMappingFiles);
dtvo = mapper.map(tvo, ATestVO.class);
System.out.println(dtvo.toString());
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
TestVO属性:
private String tString;
private Long tLong;
private long tlong;
private Integer tInteger;
private int tint;
private Float tFloat;
private float tfloat;
private Double tDouble;
private double tdouble;
private boolean tboolean;
private Boolean tBoolean;
private byte tbyte;
private Byte tByte;
private short tshort;
private Short tShort;
private char tchar;
private Character tCharacter;
private Date tDate;
测试结果:
====new TestVO()====
TestVO[tdouble:0.0 tfloat:0.0 tint:0 tlong:0 tString:null tDouble:null tFloat:null tInteger:null tLong:null tboolean:false tBoolean:null tbyte:0 tByte:null tshort:0 tShort:null tchar: tCharacter:null tDate:null]
====BeanUtils.copyProperties====
ATestVO[tdouble:0.0 tfloat:0.0 tint:0 tlong:0 tString:null tDouble:0.0 tFloat:0.0 tInteger:0 tLong:0 tboolean:false tBoolean:false tbyte:0 tByte:0 tshort:0 tShort:0 tchar: tCharacter: tDate:null]
====BeanUtilsBean.getInstance().copyProperties====
ATestVO[tdouble:0.0 tfloat:0.0 tint:0 tlong:0 tString:null tDouble:0.0 tFloat:0.0 tInteger:0 tLong:0 tboolean:false tBoolean:false tbyte:0 tByte:0 tshort:0 tShort:0 tchar: tCharacter: tDate:null]
====PropertyUtils.copyProperties====
ATestVO[tdouble:0.0 tfloat:0.0 tint:0 tlong:0 tString:null tDouble:null tFloat:null tInteger:null tLong:null tboolean:false tBoolean:null tbyte:0 tByte:null tshort:0 tShort:null tchar: tCharacter:null tDate:null]
====dozer====
ATestVO[tdouble:0.0 tfloat:0.0 tint:0 tlong:0 tString:null tDouble:null tFloat:null tInteger:null tLong:null tboolean:false tBoolean:null tbyte:0 tByte:null tshort:0 tShort:null tchar: tCharacter:null tDate:null]
总结:个人推荐还是用Dozer来做前台VO和后台PO的转换,相比之下,dozer更能适合项目后期的各种Change Request。







最新评论