一、概述
選擇Cglib的BeanCopier進行Bean拷貝的理由是,其性能要比Spring的BeanUtils,Apache的BeanUtils和PropertyUtils要好很多,尤其是數據量比較大的情況下。
之前的一篇文章:Easy-mapper教程——模型轉換工具 提到了Cglib的BeanCopier使用ASM字節碼生成技術,所以性能會非常好。
下面的文章內容直接整理自網上資源,有錯誤之處敬請諒解,后續再整理。
二、相關使用案例
1、引入maven依賴
<dependency> <groupId>asm</groupId> <artifactId>asm</artifactId> <version>3.3.1</version> </dependency> <dependency> <groupId>asm</groupId> <artifactId>asm-commons</artifactId> <version>3.3.1</version> </dependency> <dependency> <groupId>asm</groupId> <artifactId>asm-util</artifactId> <version>3.3.1</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib-nodep</artifactId> <version>2.2.2</version> </dependency>
2、使用Demo
public class OrderEntity { private int id; private String name; // Getters and setters are omitted }
public class OrderDto { private int id; private String name; // Getters and setters are omitted }
public class PropWithDiffType { private Integer id; private String name; // Getters and setters are omitted }
public class LackOfSetter { private int id; private String name; public LackOfSetter() { } public LackOfSetter(int id, String name) { this.id = id; this.name = name; } // Getters and setters are omitted // public void setName(String name) { // this.name = name; // } }
2.1屬性名稱、類型都相同:
@Test public void normalCopyTest() { OrderEntity entity = new OrderEntity(); entity.setId(1); entity.setName("orderName"); final BeanCopier copier = BeanCopier.create(OrderEntity.class, OrderDto.class, false); OrderDto dto = new OrderDto(); copier.copy(entity, dto, null); Assert.assertEquals(1, dto.getId()); Assert.assertEquals("orderName", dto.getName()); }
結論:拷貝OK。
2.2屬性名稱相同、類型不同:
@Test public void sameNameDifferentTypeCopyTest() { OrderEntity entity = new OrderEntity(); entity.setId(1); entity.setName("orderName"); final BeanCopier copier = BeanCopier.create(OrderEntity.class, PropWithDiffType.class, false); PropWithDiffType dto = new PropWithDiffType(); copier.copy(entity, dto, null); Assert.assertEquals(null, dto.getId()); // OrderEntity的id為int類型,而PropWithDiffType的id為Integer類型,不拷貝 Assert.assertEquals("orderName", dto.getName()); }
結論:名稱相同而類型不同的屬性不會被拷貝。
注意:即使源類型是原始類型(int, short和char等),目標類型是其包裝類型(Integer, Short和Character等),或反之:都不會被拷貝。
2.3源類和目標類有相同的屬性(兩者的getter都存在),但目標類的setter不存在
@Test public void targetLackOfSetterCopyTest() { OrderEntity entity = new OrderEntity(); entity.setId(1); entity.setName("orderName"); final BeanCopier copier = BeanCopier.create(OrderEntity.class, LackOfSetter.class, false); // 拋NullPointerException LackOfSetter dto = new LackOfSetter(); copier.copy(entity, dto, null); }
結論:創建BeanCopier的時候拋異常。
導致異常的原因是BeanCopier類的第128~133行
for (int i = 0; i < setters.length; i++) { // 遍歷目標類的屬性描述集 PropertyDescriptor setter = setters[i]; PropertyDescriptor getter = (PropertyDescriptor)names.get(setter.getName()); // 從源類獲取和目標類屬性名稱相同的屬性描述 if (getter != null) { MethodInfo read = ReflectUtils.getMethodInfo(getter.getReadMethod()); // 獲取源類屬性的getter方法 MethodInfo write = ReflectUtils.getMethodInfo(setter.getWriteMethod()); // 獲取目標類屬性的setter方法。LackOfSetter類name屬性的setter方法沒有,所以報錯
3、小結:
1. BeanCopier只拷貝名稱和類型都相同的屬性。
2. 當目標類的setter數目比getter少時,創建BeanCopier會失敗而導致拷貝不成功。
三、自定義Converter轉換器
當源和目標類的屬性類型不同時,不能拷貝該屬性,此時我們可以通過實現Converter接口來自定義轉換器
源類和目標類:
public class AccountEntity { private int id; private Timestamp createTime; private BigDecimal balance; // Getters and setters are omitted }
public class AccountDto { private int id; private String name; private String createTime; private String balance; // Getters and setters are omitted }
1、不使用Converter
public class BeanCopierConverterTest { @Test public void noConverterTest() { AccountEntity po = new AccountEntity(); po.setId(1); po.setCreateTime(new Timestamp(10043143243L)); po.setBalance(BigDecimal.valueOf(4000L)); BeanCopier copier = BeanCopier.create(AccountEntity.class, AccountDto.class, false); AccountDto dto = new AccountDto(); copier.copy(po, dto, null); Assert.assertNull(dto.getCreateTime()); // 類型不同,未拷貝 Assert.assertNull(dto.getBalance()); // 類型不同,未拷貝 } }
2、使用Converter
基於目標對象的屬性出發,如果源對象有相同名稱的屬性,則調一次convert方法:
package net.sf.cglib.core; public interface Converter { // value 源對象屬性,target 目標對象屬性類,context 目標對象setter方法名 Object convert(Object value, Class target, Object context); }
@Test public void converterTest() { AccountEntity po = new AccountEntity(); po.setId(1); po.setCreateTime(Timestamp.valueOf("2014-04-12 16:16:15")); po.setBalance(BigDecimal.valueOf(4000L)); BeanCopier copier = BeanCopier.create(AccountEntity.class, AccountDto.class, true); AccountConverter converter = new AccountConverter(); AccountDto dto = new AccountDto(); copier.copy(po, dto, converter); Assert.assertEquals("2014-04-12 16:16:15", dto.getCreateTime()); Assert.assertEquals("4000", dto.getBalance()); } static class AccountConverter implements Converter { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @SuppressWarnings("rawtypes") @Override public Object convert(Object value, Class target, Object context) { if (value instanceof Integer) { return (Integer) value; } else if (value instanceof Timestamp) { Timestamp date = (Timestamp) value; return sdf.format(date); } else if (value instanceof BigDecimal) { BigDecimal bd = (BigDecimal) value; return bd.toPlainString(); } return null; } }
注:一旦使用Converter,BeanCopier只使用Converter定義的規則去拷貝屬性,所以在convert方法中要考慮所有的屬性。
四、提供個工具類
package com.yusj.utils; import java.util.HashMap; import java.util.Map; import net.sf.cglib.beans.BeanCopier; /** * * 將beancopier做成靜態類,方便拷貝 * <br>創建日期:2015年12月1日 * <br><b>Copyright 2015 UTOUU All Rights Reserved</b> * @author yushaojian * @since 1.0 * @version 1.0 */ public class CglibBeanCopierUtils { /** * */ public static Map<String, BeanCopier> beanCopierMap = new HashMap<String, BeanCopier>(); /** * @Title: copyProperties * @Description: TODO(bean屬性轉換) * @param source 資源類 * @param target 目標類 * @author yushaojian * @date 2015年11月25日下午4:56:44 */ public static void copyProperties(Object source,Object target){ String beanKey = generateKey(source.getClass(),target.getClass()); BeanCopier copier = null; if (!beanCopierMap.containsKey(beanKey)) { copier = BeanCopier.create(source.getClass(), target.getClass(), false); beanCopierMap.put(beanKey, copier); }else { copier = beanCopierMap.get(beanKey); } copier.copy(source, target, null); } private static String generateKey(Class<?>class1,Class<?>class2){ return class1.toString() + class2.toString(); } /*注: (1)相同屬性名,且類型不匹配時候的處理,ok,但是未滿足的屬性不拷貝; (2)get和set方法不匹配的處理,創建拷貝的時候報錯,無法拷貝任何屬性(當且僅當sourceClass的get方法超過set方法時出現) (3)BeanCopier 初始化例子:BeanCopier copier = BeanCopier.create(Source.class, Target.class, useConverter=true) 第三個參數userConverter,是否開啟Convert,默認BeanCopier只會做同名,同類型屬性的copier,否則就會報錯. copier = BeanCopier.create(source.getClass(), target.getClass(), false); copier.copy(source, target, null); (4)修復beanCopier對set方法強限制的約束 改寫net.sf.cglib.beans.BeanCopier.Generator.generateClass(ClassVisitor)方法 將133行的 MethodInfo write = ReflectUtils.getMethodInfo(setter.getWriteMethod()); 預先存一個names2放入 109 Map names2 = new HashMap(); 110 for (int i = 0; i < getters.length; ++i) { 111 names2.put(setters[i].getName(), getters[i]); } 調用這行代碼前判斷查詢下,如果沒有改writeMethod則忽略掉該字段的操作,這樣就可以避免異常的發生。*/ }
參考文章:
https://blog.csdn.net/liangrui1988/article/details/41802275
https://ysj5125094.iteye.com/blog/2260885
http://cglib.sourceforge.net/apidocs/net/sf/cglib/beans/BeanCopier.html