Java中几种属性copy工具比较

常见的属性copy工具及其性能对比

Java中几种属性copy工具比较

属性copy是日常开发中比较常见的操作,如DO转DTO等。常见的属性copy工具类方法有:

  • 直接调用类的getter setter方法(效率高,代码长)
  • Spring提供的BeanUtils.copyProperties(Object source, Object target[,String… ignoreProperties])工具方法。基于Java的反射机制,支持ignore操作
  • Apache提供的BeanUtils.copyProperties(),类似于Spring的BeanUtils,支持属性类型自动转换的功能,效率较低与Spring的BeanUtils
  • Apache提供的PropertyUtils.copyProperties(Object dest, Object orig)工具方法,不支持属性类型自动转换的功能,如果类型不同,会抛java.lang.IllegalArgumentException异常
  • cglib的BeanCopier,使用动态代理,效率高

测试代码

source数据CopyBeanDO

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.math.BigInteger;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CopyBeanDO {
    private int        intValue;
    private boolean    boolValue;
    private float      floatValue;
    private double     doubleValue;
    private long       longValue;
    private char       charValue;
    private byte       byteValue;
    private short      shortValue;
    private Integer    integerValue;
    private Boolean    boolObjValue;
    private Float      floatObjValue;
    private Double     doubleObjValue;
    private Long       longObjValue;
    private Short      shortObjValue;
    private Byte       byteObjValue;
    private BigInteger bigIntegerValue;
    private BigDecimal bigDecimalValue;
    private String     stringValue;

    public boolean getBoolValue() {
        return boolValue;
    }

    public void setBoolValue(boolean boolValue) {
        this.boolValue = boolValue;
    }
}

target数据DTO

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;


/**
 * Created by RuzzZZ on 2017/2/17.
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CopyBeanDTO implements Serializable {
    private static final long serialVersionUID = 2977329629013865582L;
    private int        intValue;
    private boolean    boolValue;
    private float      floatValue;
    private double     doubleValue;
    private long       longValue;
    private char       charValue;
    private byte       byteValue;
    private short      shortValue;
    private Integer    integerValue;
    private Boolean    boolObjValue;
    private Float      floatObjValue;
    private Double     doubleObjValue;
    private Long       longObjValue;
    private Short      shortObjValue;
    private Byte       byteObjValue;
    private BigInteger bigIntegerValue;
    private BigDecimal bigDecimalValue;
    private String     stringValue;


    public boolean getBoolValue() {
        return boolValue;
    }

    public void setBoolValue(boolean boolValue) {
        this.boolValue = boolValue;
    }

    public static void main(String[] args) {
        System.out.println(System.currentTimeMillis());
    }
}

测试main方法

import com.alibaba.fastjson.JSONObject;
import com.esotericsoftware.reflectasm.ConstructorAccess;
import org.apache.commons.beanutils.PropertyUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.cglib.beans.BeanCopier;

import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;

public class BeanUtilsCopyDemo {

    public static final Integer LOOP_TIME = 500000;

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        {
            CopyBeanDO copyBeanDO = new CopyBeanDO(100, true, 3.21f, 3.21, 100L, 'c', (byte) 3, (short) 3, Integer.MIN_VALUE, Boolean.TRUE,
                    Float.MAX_VALUE, Double.MAX_VALUE, Long.MAX_VALUE, Short.MAX_VALUE, Byte.MAX_VALUE, BigInteger.ONE, BigDecimal.ONE, "test");
            CopyBeanDTO copyBeanDTO = new CopyBeanDTO();
            Long startTime = System.currentTimeMillis();
            for (int i = 0; i < LOOP_TIME; i++) {
                BeanUtils.copyProperties(copyBeanDO, copyBeanDTO);
            }
            Long end = System.currentTimeMillis();
            System.out.println("Spring.BeanUtils spend time : " + (end - startTime));
            System.out.println("CarSnapInfoDTO : " + JSONObject.toJSONString(copyBeanDTO));
        }

        {
            CopyBeanDO copyBeanDO = new CopyBeanDO(100, true, 3.21f, 3.21, 100L, 'c', (byte) 3, (short) 3, Integer.MIN_VALUE, Boolean.TRUE,
                    Float.MAX_VALUE, Double.MAX_VALUE, Long.MAX_VALUE, Short.MAX_VALUE, Byte.MAX_VALUE, BigInteger.ONE, BigDecimal.ONE, "test");
            CopyBeanDTO copyBeanDTO = new CopyBeanDTO();
            Long startTime = System.currentTimeMillis();
            for (int i = 0; i < LOOP_TIME; i++) {
                PropertyUtils.copyProperties(copyBeanDO, copyBeanDTO);
            }
            Long end = System.currentTimeMillis();
            System.out.println("Apache.PropertyUtils spend time : " + (end - startTime));
            System.out.println("CarSnapInfoDTO : " + JSONObject.toJSONString(copyBeanDTO));
        }


        {
            CopyBeanDO copyBeanDO = new CopyBeanDO(100, true, 3.21f, 3.21, 100L, 'c', (byte) 3, (short) 3, Integer.MIN_VALUE, Boolean.TRUE,
                    Float.MAX_VALUE, Double.MAX_VALUE, Long.MAX_VALUE, Short.MAX_VALUE, Byte.MAX_VALUE, BigInteger.ONE, BigDecimal.ONE, "test");
            CopyBeanDTO copyBeanDTO = new CopyBeanDTO();
            Long startTime = System.currentTimeMillis();
            for (int i = 0; i < LOOP_TIME; i++) {
                copyBean(copyBeanDO, copyBeanDTO);
            }
            Long end = System.currentTimeMillis();
            System.out.println("direct setter spend time : " + (end - startTime));
            System.out.println("CarSnapInfoDTO : " + JSONObject.toJSONString(copyBeanDTO));
        }


        {
            CopyBeanDO copyBeanDO = new CopyBeanDO(100, true, 3.21f, 3.21, 100L, 'c', (byte) 3, (short) 3, Integer.MIN_VALUE, Boolean.TRUE,
                    Float.MAX_VALUE, Double.MAX_VALUE, Long.MAX_VALUE, Short.MAX_VALUE, Byte.MAX_VALUE, BigInteger.ONE, BigDecimal.ONE, "test");
            ConstructorAccess<CopyBeanDTO> constructorAccess = ConstructorAccess.get(CopyBeanDTO.class);
            CopyBeanDTO copyBeanDTO = constructorAccess.newInstance();
            BeanCopier copier = BeanCopier.create(CopyBeanDO.class, CopyBeanDTO.class, false);
            Long startTime = System.currentTimeMillis();
            for (int i = 0; i < LOOP_TIME; i++) {
                copier.copy(copyBeanDO, copyBeanDTO, null);
            }
            Long end = System.currentTimeMillis();
            System.out.println("BeanCopier : " + (end - startTime));
            System.out.println("CarSnapInfoDTO : " + JSONObject.toJSONString(copyBeanDTO));
        }

    }

    private static void copyBean(CopyBeanDO copyBeanDO, CopyBeanDTO copyBeanDTO) {
        copyBeanDTO.setIntValue(copyBeanDO.getIntValue());
        copyBeanDTO.setBoolValue(copyBeanDO.getBoolObjValue());
        copyBeanDTO.setFloatValue(copyBeanDO.getFloatValue());
        copyBeanDTO.setDoubleValue(copyBeanDO.getDoubleValue());
        copyBeanDTO.setLongValue(copyBeanDO.getLongValue());
        copyBeanDTO.setCharValue(copyBeanDO.getCharValue());
        copyBeanDTO.setByteValue(copyBeanDO.getByteValue());
        copyBeanDTO.setShortValue(copyBeanDO.getShortValue());
        copyBeanDTO.setIntegerValue(copyBeanDO.getIntegerValue());
        copyBeanDTO.setBoolObjValue(copyBeanDO.getBoolObjValue());
        copyBeanDTO.setFloatObjValue(copyBeanDO.getFloatObjValue());
        copyBeanDTO.setDoubleObjValue(copyBeanDO.getDoubleObjValue());
        copyBeanDTO.setLongObjValue(copyBeanDO.getLongObjValue());
        copyBeanDTO.setShortObjValue(copyBeanDO.getShortObjValue());
        copyBeanDTO.setByteObjValue(copyBeanDO.getByteObjValue());
        copyBeanDTO.setBigIntegerValue(copyBeanDO.getBigIntegerValue());
        copyBeanDTO.setBigDecimalValue(copyBeanDO.getBigDecimalValue());
        copyBeanDTO.setStringValue(copyBeanDO.getStringValue());
    }
}

运行结果:

Spring.BeanUtils spend time : 1228
CarSnapInfoDTO : {"bigDecimalValue":1,"bigIntegerValue":1,"boolObjValue":true,"boolValue":true,"byteObjValue":127,"byteValue":3,"charValue":"c","doubleObjValue":1.7976931348623157E308,"doubleValue":3.21,"floatObjValue":3.4028235E38,"floatValue":3.21,"intValue":100,"integerValue":-2147483648,"longObjValue":9223372036854775807,"longValue":100,"shortObjValue":32767,"shortValue":3,"stringValue":"test"}
Apache.PropertyUtils spend time : 6642
CarSnapInfoDTO : {"boolValue":false,"byteValue":0,"charValue":"\u0000","doubleValue":0,"floatValue":0,"intValue":0,"longValue":0,"shortValue":0}
direct setter spend time : 15
CarSnapInfoDTO : {"bigDecimalValue":1,"bigIntegerValue":1,"boolObjValue":true,"boolValue":true,"byteObjValue":127,"byteValue":3,"charValue":"c","doubleObjValue":1.7976931348623157E308,"doubleValue":3.21,"floatObjValue":3.4028235E38,"floatValue":3.21,"intValue":100,"integerValue":-2147483648,"longObjValue":9223372036854775807,"longValue":100,"shortObjValue":32767,"shortValue":3,"stringValue":"test"}
BeanCopier : 9
CarSnapInfoDTO : {"bigDecimalValue":1,"bigIntegerValue":1,"boolObjValue":true,"boolValue":true,"byteObjValue":127,"byteValue":3,"charValue":"c","doubleObjValue":1.7976931348623157E308,"doubleValue":3.21,"floatObjValue":3.4028235E38,"floatValue":3.21,"intValue":100,"integerValue":-2147483648,"longObjValue":9223372036854775807,"longValue":100,"shortObjValue":32767,"shortValue":3,"stringValue":"test"}

Spring的BeanUtils、Apache的BeanUtils、Apache的PropertyUtils对比

对比代码

import lombok.Data;

@Data
public class TestA {

    private int testInteger;

    private String testStr;

    private String testDiff;

}

import lombok.Data;

@Data
public class TestB {

    private Integer testInteger;

    private String testStr;

    private Integer testDiff;
}
import java.lang.reflect.InvocationTargetException;

public class BeanUtilsTestDemo {

    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
        TestA testA = new TestA();
        testA.setTestStr("test12321");
        testA.setTestDiff("10");
        TestB testB1 = new TestB();
        TestB testB2 = new TestB();
        TestB testB3 = new TestB();
        org.springframework.beans.BeanUtils.copyProperties(testA, testB1);
        System.out.println(testB1);
        org.apache.commons.beanutils.BeanUtils.copyProperties(testB2, testA);
        System.out.println(testB2);
        org.apache.commons.beanutils.PropertyUtils.copyProperties(testB3, testA);
        System.out.println(testB3);
    }
}

运行结果

TestB(testInteger=0, testStr=test12321, testDiff=null)
TestB(testInteger=0, testStr=test12321, testDiff=10)
Exception in thread "main" java.lang.IllegalArgumentException: Cannot invoke com.xzy.copy.TestB.setTestDiff on bean class 'class com.xzy.copy.TestB' - argument type mismatch - had objects of type "java.lang.String" but expected signature "java.lang.Integer"
    at org.apache.commons.beanutils.PropertyUtilsBean.invokeMethod(PropertyUtilsBean.java:2195)
    at org.apache.commons.beanutils.PropertyUtilsBean.setSimpleProperty(PropertyUtilsBean.java:2108)
    at org.apache.commons.beanutils.PropertyUtilsBean.copyProperties(PropertyUtilsBean.java:329)
    at org.apache.commons.beanutils.PropertyUtils.copyProperties(PropertyUtils.java:219)
    at com.xzy.copy.BeanUtilsTestDemo.main(BeanUtilsTestDemo.java:18)
Caused by: java.lang.IllegalArgumentException: argument type mismatch
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.commons.beanutils.PropertyUtilsBean.invokeMethod(PropertyUtilsBean.java:2127)
    ... 4 more

基于cglib的copy工具类

cglib的性能最好,效率最高,几乎等价于直接调用set、get方法,但是其中创建BeanCopier和利用ASM的ConstructorAccess是比较耗时的操作,可以利用缓存减少不必要的耗时。 下面是一个基于以上原因而写的一个工具类

/**
 * 快速拷贝
 */
public class WrappedBeanCopier {

    private static final int MAX_CACHE_SIZE = 500;

    private static final Map<String, BeanCopier> beanCopierCache = new ConcurrentHashMap<>();
    private static final Map<String, ConstructorAccess> constructorAccessCache = new ConcurrentHashMap<>();

    protected static void copyProperties(Object source, Object target) {
        if (source == null || target == null) return;
        BeanCopier copier = getBeanCopier(source.getClass(), target.getClass());
        copier.copy(source, target, null);
    }

    private static BeanCopier getBeanCopier(Class sourceClass, Class targetClass) {
        String beanKey = generateKey(sourceClass, targetClass);
        BeanCopier copier = null;
        if (!beanCopierCache.containsKey(beanKey)) {
            copier = BeanCopier.create(sourceClass, targetClass, false);
            if (beanCopierCache.size() > MAX_CACHE_SIZE) {
                beanCopierCache.clear();
            }
            beanCopierCache.put(beanKey, copier);
        } else {
            copier = beanCopierCache.get(beanKey);
        }
        return copier;
    }

    private static String generateKey(Class<?> class1, Class<?> class2) {
        return class1.getName() + class2.getName();
    }

    protected static <T> T copyProperties(Object source, Class<T> targetClass) {
        if (source == null) return null;
        T t = null;
        try {
            ConstructorAccess<T> constructorAccess = getConstructorAccess(targetClass);
            t = constructorAccess.newInstance();
        } catch (RuntimeException e) {
            try {
                t = targetClass.newInstance();
            } catch (InstantiationException | IllegalAccessException e1) {
                throw new RuntimeException(format("Create new instance of %s failed: %s", targetClass, e.getMessage()));
            }
        }
        copyProperties(source, t);
        return t;
    }

    protected static <T> List<T> copyPropertiesOfList(List<?> sourceList, Class<T> targetClass) {
        if (sourceList == null || sourceList.isEmpty()) {
            return Collections.emptyList();
        }
        ConstructorAccess<T> constructorAccess = getConstructorAccess(targetClass);
        List<T> resultList = new ArrayList<>(sourceList.size());
        for (Object o : sourceList) {
            T t = null;
            try {
                t = constructorAccess.newInstance();
                copyProperties(o, t);
                resultList.add(t);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return resultList;
    }

    private static <T> ConstructorAccess<T> getConstructorAccess(Class<T> targetClass) {
        ConstructorAccess<T> constructorAccess = constructorAccessCache.get(targetClass.getName());
        if(constructorAccess != null) {
            return constructorAccess;
        }
        try {
            constructorAccess = ConstructorAccess.get(targetClass);
            if (constructorAccessCache.size() > MAX_CACHE_SIZE) {
                constructorAccessCache.clear();
            }
            constructorAccessCache.put(targetClass.getName(),constructorAccess);
        } catch (Exception e) {
            throw new RuntimeException(format("Create new instance of %s failed: %s", targetClass, e.getMessage()));
        }
        return constructorAccess;
    }

}