Spring BeanUtils.copyProperties和apache commons-beanutils

业务系统中经常需要两个对象进行属性的拷贝,不能否认逐个的对象拷贝是最快速最安全的做法,但是当数据对象的属性字段数量超过程序员的容忍的程度,代码因此变得臃肿不堪,使用一些方便的对象拷贝工具类将是很好的选择。

性能对比: 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 Collection members;
    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属性
    Collection members = getProjectMembers(infoDto);
    projectInfo.setMembers(members);
    return projectInfo;
}

注意1:Spring 的BeanUtils.copyProperties在拷贝属性时忽略空值解决办法。

当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();
        Set emptyNames = 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);

上图一张,你们就明白了。

Spring BeanUtils.copyProperties和apache commons-beanutils_第1张图片

关于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

你可能感兴趣的:(Spring)