Java屬性拷貝——大道至簡

寫業務代碼的同學,一定經常使用一個API:

org.springframework.beans.BeanUtils#copyProperties(java.lang.Object, java.lang.Object)

從一個實例中拷貝屬性值到另外一個實例(可以是相同class或不同class)
對於寫業務代碼同學來說,確實很“省”代碼。員外近仨月就節省了很多代碼。
例如,歷史文章中有一篇,就是自己爲了偷懶,寫的一個工具方法:傳送門

昨天review同事的一段代碼(壓測報告響應時間長),業務邏輯非常簡單:數據庫查詢數據(POJO List),然後copyProperties到VO List。也就是數據量會大一些(目前返回測試數據千條級別,企業級應用,單條數據的字段值也蠻多)。同事採用的是序列化與反序列化的方式進行屬性copy的。

既然業務邏輯(不好意思稱之爲算法)簡單,爲什麼會"慢"呢?
自己能想到對象(屬性值)copy的方式有三種:
①:基於ObjectMapper做序列化與反序列化
source和target field大面積相同,例如一個dto對應的vo。

public class BeanUtils {
    private static Logger logger = LogManager.getLogger(CollectionsUtils.class);
    private static ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 從類實例中copy屬性值
     * 並返回一個指定類的實例作爲返回值
     * 基於{@link ObjectMapper}進行序列化與反序列化
     *
     * @param source
     * @param type
     * @param <T>
     * @return
     * @throws IOException
     */
    public static <T, E> T copyProperties(E source, Class<T> type) {
        String jsonString = null;
        try {
            jsonString = objectMapper.writeValueAsString(source);
            return objectMapper.readValue(jsonString, type);
        } catch (JsonProcessingException e) {
            logger.error(e.getMessage(), e);
            throw new RuntimeException();
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
            throw new RuntimeException();
        }
    }
}

②Spring中的接口:

org.springframework.beans.BeanUtils#copyProperties(java.lang.Object, java.lang.Object)

當然還有其它API,不做列舉
③Getter/Setter方法使用

於是想着對三種方式進行實踐測試:

public class User implements Cloneable{
    private String name;
    private String sex;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }

}
public class UserMirror implements Cloneable{
    private String name;
    private String sex;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }

}
public class BeanUtilsTest {
    private static final int count = 1000000;

    @Test
    public void copyProperties() throws IOException {
        System.out.println(String.format("拷貝數據:%d條", count));
        this.copyBySerialize();
        this.copyByReflex();
        this.copyByMethod();
    }

    private List<UserMirror> copyBySerialize() throws IOException {
        long begin = System.currentTimeMillis();
        User user = new User();
        user.setName("Young");
        user.setSex("male");
        List<UserMirror> list = new ArrayList<>(count);
        for (int index = 0; index < count; index++) {
            list.add(BeanUtils.copyProperties(user, UserMirror.class));
        }
        long end = System.currentTimeMillis();
        System.out.println(String.format("序列化反序列化方式耗時:%d millis", end - begin));
        return list;
    }

    private List<UserMirror> copyByReflex() throws IOException {
        long begin = System.currentTimeMillis();
        User user = new User();
        user.setName("Young");
        user.setSex("male");
        List<UserMirror> list = new ArrayList<>(count);
        for (int index = 0; index < count; index++) {
            UserMirror userMirror = new UserMirror();
            org.springframework.beans.BeanUtils.copyProperties(user, userMirror);
            list.add(userMirror);
        }
        long end = System.currentTimeMillis();
        System.out.println(String.format("反射方式耗時:%d millis", end - begin));
        return list;
    }

    /**
     * Getter/Setter
     *
     * @return
     * @throws IOException
     */
    private List<UserMirror> copyByMethod(){
        long begin = System.currentTimeMillis();
        User user = new User();
        user.setName("Young");
        user.setSex("male");
        List<UserMirror> list = new ArrayList<>(count);
        for (int index = 0; index < count; index++) {
            UserMirror userMirror = new UserMirror();
            userMirror.setName(user.getName());
            userMirror.setSex(user.getSex());
            list.add(userMirror);
        }
        long end = System.currentTimeMillis();
        System.out.println(String.format("Getter/Setter方式耗時:%d millis", end - begin));
        return list;
    }
}

然後來看看測試結果:
在這裏插入圖片描述
然後用數據來說說化:
從效率來看③優於②優於①,思考一下原因:
①爲什麼最慢?看看方法,細心點可以發現有一個IOException異常處理,因爲序列化與反序列化是需要涉及到IO開銷的。
②看看API的源碼:其實質還是利用反射來調用Getter/Setter方法

/**
	 * Copy the property values of the given source bean into the given target bean.
	 * <p>Note: The source and target classes do not have to match or even be derived
	 * from each other, as long as the properties match. Any bean properties that the
	 * source bean exposes but the target bean does not will silently be ignored.
	 * @param source the source bean
	 * @param target the target bean
	 * @param editable the class (or interface) to restrict property setting to
	 * @param ignoreProperties array of property names to ignore
	 * @throws BeansException if the copying failed
	 * @see BeanWrapper
	 */
	private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
			@Nullable String... ignoreProperties) throws BeansException {

		Assert.notNull(source, "Source must not be null");
		Assert.notNull(target, "Target must not be null");

		Class<?> actualEditable = target.getClass();
		if (editable != null) {
			if (!editable.isInstance(target)) {
				throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
						"] not assignable to Editable class [" + editable.getName() + "]");
			}
			actualEditable = editable;
		}
		PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
		List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);

		for (PropertyDescriptor targetPd : targetPds) {
			Method writeMethod = targetPd.getWriteMethod();
			if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
				PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
				if (sourcePd != null) {
					Method readMethod = sourcePd.getReadMethod();
					if (readMethod != null &&
							ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
						try {
							if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
								readMethod.setAccessible(true);
							}
							Object value = readMethod.invoke(source);
							if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
								writeMethod.setAccessible(true);
							}
							writeMethod.invoke(target, value);
						}
						catch (Throwable ex) {
							throw new FatalBeanException(
									"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
						}
					}
				}
			}
		}
	}

③雖然代碼量看起來多一點,可實質上就是方式②的一個詳細解釋。

當然呢,①和②更具通用性,③的效率就高一點。那麼當一個產品的功能十分穩定以後,想要進行效率上的提升,那麼①②替換爲③,不失爲一個思路。

總結一下:大道至簡

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章