业务系统中经常需要两个对象进行属性的拷贝,不能否认逐个的对象拷贝是最快速最安全的做法,但是当数据对象的属性字段数量超过程序员的容忍的程度,代码因此变得臃肿不堪,使用一些方便的对象拷贝工具类将是很好的选择。
性能对比: BeanCopier > PropertyUtils > BeanUtils. 其中BeanCopier的性能高出另外两个100数量级。
目前流行的较为公用认可的工具类:
Apache的两个版本:(反射机制)
org.apache.commons.beanutils.PropertyUtils.copyProperties(Object dest, Object orig)
org.apache.commons.beanutils.BeanUtils.copyProperties(Object dest, Object orig)
Spring版本:(反射机制)
org.springframework.beans.BeanUtils.copyProperties(Object source, Object target, Class editable, String[] ignoreProperties)
cglib版本:(使用动态代理,效率高)
net.sf.cglib.beans.BeanCopier.copy(Object paramObject1, Object paramObject2, Converter paramConverter)
缺陷预防:
@Document(collection = "project_info") public class ProjectInfo extends BaseEntity { @TextIndexed(weight = 3) private String name; @TextIndexed(weight = 2) private String description; @Field("nick_name") private String nickName; @Field("is_available") private boolean isAvailable = true; @Field("is_public") private boolean isPublic; @Field("create_at") private Date createAt; @Field("last_activity_at") private Date lastActivityAt; @Field("web_url") private String webUrl; @Field("repo_url") private String repoUrl; @Field("avatar_url") private String avatarUrl; private User owner; @Indexed private long stars; private Map, Collection > categories; private Collection members; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public boolean getIsAvailable() { return isAvailable; } public void setIsAvailable(boolean available) { isAvailable = available; } public boolean getIsPublic() { return isPublic; } public void setIsPublic(boolean aPublic) { isPublic = aPublic; } public Date getCreateAt() { return createAt; } public void setCreateAt(Date createAt) { this.createAt = createAt; } public Date getLastActivityAt() { return lastActivityAt; } public void setLastActivityAt(Date lastActivityAt) { this.lastActivityAt = lastActivityAt; } public String getWebUrl() { return webUrl; } public void setWebUrl(String webUrl) { this.webUrl = webUrl; } public String getRepoUrl() { return repoUrl; } public void setRepoUrl(String repoUrl) { this.repoUrl = repoUrl; } public String getAvatarUrl() { return avatarUrl; } public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; } public Map , Collection > getCategories() { return categories; } public void setCategories(Map , Collection > categories) { this.categories = categories; } public User getOwner() { return owner; } public void setOwner(User owner) { this.owner = owner; } public long getStars() { return stars; } public void setStars(long stars) { this.stars = stars; } public Collection getMembers() { return members; } public void setMembers(Collection members) { this.members = members; } public String getNickName() { return nickName; } public void setNickName(String nickName) { this.nickName = nickName; } }
** * Created by ${denghb} on 2016/8/9. */ public class ProjectInfoDto { private String id; private String name; private String nickName; private String description; private String avatarUrl; private Date createTime; private Date updateTime; private String status; private boolean isAvailable; private boolean isPublic; private UserDto creator; private Collectionmembers; private Collection releasedVersions; private Map , Collection > categories; public Map , Collection > getCategories() { return categories; } public void setCategories(Map , Collection > categories) { this.categories = categories; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getNickName() { return nickName; } public void setNickName(String nickName) { this.nickName = nickName; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getAvatarUrl() { return avatarUrl; } public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public Date getUpdateTime() { return updateTime; } public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public boolean getIsAvailable() { return isAvailable; } public void setIsAvailable(boolean available) { isAvailable = available; } public boolean getIsPublic() { return isPublic; } public void SetIsPublic(boolean aPublic) { isPublic = aPublic; } public UserDto getCreator() { return creator; } public void setCreator(UserDto creator) { this.creator = creator; } public Collection getMembers() { return members; } public void setMembers(Collection members) { this.members = members; } public Collection getReleasedVersions() { return releasedVersions; } public void setReleasedVersions(Collection releasedVersions) { this.releasedVersions = releasedVersions; } }
private ProjectInfo trans2ProjectInfo(ProjectInfoDto infoDto) { ProjectInfo projectInfo = getProjectInfo(infoDto); if ("DELETE".equalsIgnoreCase(infoDto.getStatus())) { projectInfo.setDeleteFlag(true); return projectInfo; } // 复制简单属性到ProjectInfo中 BeanUtils.copyProperties(infoDto, projectInfo, "members", "categories"); projectInfo.setCreateAt(infoDto.getCreateTime()); projectInfo.setLastActivityAt(infoDto.getUpdateTime()); projectInfo.setCategories(getCategories(infoDto, projectInfo)); // 添加owner属性 User owner = getOwner(infoDto); projectInfo.setOwner(owner); // 添加members属性 Collectionmembers = getProjectMembers(infoDto); projectInfo.setMembers(members); return projectInfo; }
当src对象的键值为Null时,就会把target对象的对应键值覆盖成空了。
package com.dhb.springmvc.util.beanUtil; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; import java.util.HashSet; import java.util.Set; /** * 当src对象的键值为Null时,使用如下方式就不会把target对象的对应键值覆盖成空了 * Created by ${denghb} on 2016/9/6. */ public class BeanUtilIgnore { public static String[] getNullPropertyNames ( Object source) { final BeanWrapper src = new BeanWrapperImpl(source); java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors(); SetemptyNames = new HashSet<>(); for(java.beans.PropertyDescriptor pd : pds) { Object srcValue = src.getPropertyValue(pd.getName()); if (srcValue == null) emptyNames.add(pd.getName()); } String[] result = new String[emptyNames.size()]; return emptyNames.toArray(result); } public static void copyPropertiesIgnoreNull( Object src, Object target){ BeanUtils.copyProperties(src, target, getNullPropertyNames(src)); } }:
然后在调用这个方法进行复制:
BeanUtils.copyProperties(examLifeStyle, examDetail, getNullPropertyNames(examLifeStyle));
注意2:beanutils.Converter使用案例,增强apache的beanUtils的拷贝属性,注册一些新的类型转换。
package com.dhb.springmvc.util.beanUtil; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.beanutils.ConvertUtils; import java.lang.reflect.InvocationTargetException; /** * Created by ${denghb} on 2016/9/6. */ public class BeanUtilsEx extends BeanUtils { public static void copyProperties(Object dest, Object orig) { try { BeanUtils.copyProperties(dest, orig); } catch (IllegalAccessException ex) { ex.printStackTrace(); } catch (InvocationTargetException ex) { ex.printStackTrace(); } } static { ConvertUtils.register(new DateConverter(null), java.util.Date.class); //ConvertUtils.register(new BigDecimalConvert(), BigDecimal.class); } }
package com.dhb.springmvc.util.beanUtil; import org.apache.commons.beanutils.Converter; import org.apache.commons.lang.StringUtils; import java.text.ParseException; import java.text.SimpleDateFormat; /** * 简易DateConverter. * 供Apache BeanUtils 做转换,默认时间格式为yyyy-MM-dd,可由构造函数改变. * Created by ${denghb} on 2016/9/6. */ public class DateConverter implements Converter { private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); public DateConverter(String formatPattern) { if (StringUtils.isNotBlank(formatPattern)) { format = new SimpleDateFormat(formatPattern); } } @Override public Object convert(Class aClass, Object value) { String dateStr = (String) value; if (StringUtils.isNotBlank(dateStr)) { try { return format.parse(dateStr); } catch (ParseException e) { e.printStackTrace(); } } return null; } }
注意3:BeanUtils拷贝null属性的问题
问题现象
1.当源对象(a)中存在一个java.sql.Date类型的属性并且值为null,目标对象(b)中也存在这个同名同类型的属性。把a对象属性值copy给b时BeanUtils.copyProperties(b, a);会抛出异常;
2.当源对象(a)中存在一个java.sql.Date类型的属性并且值为null,目标对象(b)不存在这个同名同类型的属性,copy时没问题。
解决方法
从convert方法可以看出只要想办法把useDefault和defaultValue设值就能解决,SqlDateConverter的另外一个构造方法可以设置这两个值,问题是从哪里把这个自己构造的converter 注册进去。从BeanUtils到BeanUtilsBean再到ConvertUtilsBean的找,发现都是写死的调用ConvertUtilsBean.deregister()方法注册的,最后发现ConvertUtilsBean有个register(Converter converter, Class clazz)方法可以注册,而ConvertUtilsBean又是在BeanUtilsBean里new出来的,那么只要获取到这个ConvertUtilsBean即可,BeanUtilsBean提供了获取的方法。因此在BeanUtils.copyProperties(b, a);加上下面这句代码即可
BeanUtilsBean.getInstance().getConvertUtils().register(new SqlDateConverter(null),Date.class);
另外有一个辅助类也可以,
ConvertUtils.register(new SqlDateConverter(null),Date.class);
上图一张,你们就明白了。
关于bean复制,如果属性较少,建议直接写个方法完成get/set即可。如果属性较多,可以自己采用反射实现一个满足自己需要的工具类,或者使用spring的那个beanutils类,不建议使用commons-beanutils包中的那个BeanUtils类,刚看了下,这个类对于内部静态类的对象复制也会出现问题,检验太复杂了,常会出现一些诡异的问题。毕竟我们bean复制一般就是简单的属性copy而已。
而且,由于这些BeanUtils类都是采用反射机制实现的,对程序的效率也会有影响。因此,慎用BeanUtils.copyProperties!!!
参考资料:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp37
http://jen.iteye.com/blog/1032521