使用CGlib实现Bean拷贝(BeanCopier)
在做业务的时候,我们有时为了隔离变化,会将DAO查询出来的Entity,和对外提供的DTO隔离开来。大概90%的时候,它们的结构都是类似的,但是我们很不喜欢写很多冗长的b.setF1(a.getF1())这样的代码,于是我们需要BeanCopier来帮助我们。选择Cglib的BeanCopier进行Bean拷贝的理由是,其性能要比Spring的BeanUtils,Apache的BeanUtils和PropertyUtils要好很多,尤其是数据量比较大的情况下。
BeanCopier基本用法
public class User { private int age; private String name; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
public class UserDto { private int age; private String name; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
public class UserWithDiffType { private Integer age; private String name; public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
1. 属性名称、类型都相同:
@Test public void normalCopyTest() { // create(Class source, Class target, boolean useConverter) final BeanCopier beanCopier = BeanCopier.create(User.class, UserDto.class, false); User user = new User(); user.setAge(10); user.setName("zhangsan"); UserDto userDto = new UserDto(); beanCopier.copy(user, userDto, null); Assert.assertEquals(10, userDto.getAge()); Assert.assertEquals("zhangsan", userDto.getName()); }
结论:属性名称相同类型相同的属性拷贝OK。
2. 属性名称相同、类型不同:
@Test public void normalCopyTest() { // create(Class source, Class target, boolean useConverter) final BeanCopier beanCopier = BeanCopier.create(User.class, UserWithDiffType.class, false); User user = new User(); user.setAge(10); user.setName("zhangsan"); UserWithDiffType userDto = new UserWithDiffType(); beanCopier.copy(user, userDto, null); Assert.assertEquals(null, userDto.getAge()); Assert.assertEquals("zhangsan", userDto.getName()); }
结论:属性名称相同而类型不同的属性不会被拷贝。
注意:即使源类型是原始类型(int, short和char等),目标类型是其包装类型(Integer, Short和Character等),或反之:都 不会被拷贝。
总结:
BeanCopier只拷贝名称和类型都相同的属性。
自定义转换器
当源和目标类的属性类型不同时,不能拷贝该属性,此时我们可以通过实现Converter接口来自定义转换器
源类和目标类:
public class Account { private int id; private Date createTime; private BigDecimal balance; public int getId() { return id; } public void setId(int id) { this.id = id; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public BigDecimal getBalance() { return balance; } public void setBalance(BigDecimal balance) { this.balance = balance; } }
public class AccountDto { private int id; private String createTime; private String balance; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getCreateTime() { return createTime; } public void setCreateTime(String createTime) { this.createTime = createTime; } public String getBalance() { return balance; } public void setBalance(String balance) { this.balance = balance; } }
1. 不使用Converter
@Test public void noConverterTest() { Account po = new Account(); po.setId(1); po.setCreateTime(new Date()); po.setBalance(BigDecimal.valueOf(4000L)); BeanCopier copier = BeanCopier.create(Account.class, AccountDto.class, false); AccountDto dto = new AccountDto(); copier.copy(po, dto, null); // 类型不同,未拷贝 Assert.assertNull(dto.getBalance()); // 类型不同,未拷贝 Assert.assertNull(dto.getCreateTime()); }
2. 使用Converter
基于目标对象的属性出发,如果源对象有相同名称的属性,则调一次convert方法:
public class TestCase { @Test public void noConverterTest() { Account po = new Account(); po.setId(1); po.setCreateTime(new Date()); po.setBalance(BigDecimal.valueOf(4000L)); BeanCopier copier = BeanCopier.create(Account.class, AccountDto.class, true); AccountDto dto = new AccountDto(); AccountConverter converter = new AccountConverter(); copier.copy(po, dto, converter); // 类型不同,未拷贝 Assert.assertEquals("4000", dto.getBalance()); // 类型不同,未拷贝 Assert.assertEquals("2018-12-13", dto.getCreateTime()); } } class AccountConverter implements Converter { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); @SuppressWarnings("rawtypes") @Override public Object convert(Object source, Class target, Object context) { if (source instanceof Integer) { return (Integer) source; } else if (source instanceof Date) { Date date = (Date) source; return sdf.format(date); } else if (source instanceof BigDecimal) { BigDecimal bd = (BigDecimal) source; return bd.toPlainString(); } return null; } }
注:一旦使用Converter,BeanCopier只使用Converter定义的规则去拷贝属性,所以在convert方法中要考虑所有的属性。
封装BeanCopier
@Test public void costTest() { List<User> list1 = new ArrayList<>(100); for (int i = 0; i < 100; i++) { User po = new User(); po.setId(1); po.setCreateTime(new Date()); po.setBalance(BigDecimal.valueOf(4000L)); list1.add(po); } BeanCopier copier = BeanCopier.create(User.class, UserDto.class, false); long start = System.currentTimeMillis(); List<UserDto> list2 = new ArrayList<>(100); for (User user : list1) { UserDto dto = new UserDto(); //BeanUtils.beanCopy(user, dto); copier.copy(user, dto, null); list2.add(dto); } System.out.printf("took time: %d(ms)%n",System.currentTimeMillis() - start); }
经过测试,BeanCopier性能是BeanUtils10倍左右。
BeanCopier拷贝速度快,性能瓶颈出现在创建BeanCopier实例的过程中。 所以,把创建过的BeanCopier实例放到缓存中,下次可以直接获取,提升性能:
依赖:
<dependencies> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.10</version> </dependency> <dependency> <groupId>com.esotericsoftware</groupId> <artifactId>reflectasm</artifactId> <version>1.11.7</version> </dependency> </dependencies>
封装工具
public class WrapperBeanCopier { private WrapperBeanCopier() { //do nothing } private static final Map<String, BeanCopier> BEAN_COPIER_CACHE = new ConcurrentHashMap<>(); private static final Map<String, ConstructorAccess> CONSTRUCTOR_ACCESS_CACHE = new ConcurrentHashMap<>(); public static void copyProperties(Object source, Object target) { 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 (!BEAN_COPIER_CACHE.containsKey(beanKey)) { copier = BeanCopier.create(sourceClass, targetClass, false); BEAN_COPIER_CACHE.put(beanKey, copier); } else { copier = BEAN_COPIER_CACHE.get(beanKey); } return copier; } /** * 两个类的全限定名拼接起来构成Key * * @param sourceClass * @param targetClass * @return */ private static String generateKey(Class<?> sourceClass, Class<?> targetClass) { return sourceClass.getName() + targetClass.getName(); } public static <T> T copyProperties(Object source, Class<T> targetClass) { T t = null; try { t = targetClass.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(format("Create new instance of %s failed: %s", targetClass, e.getMessage())); } copyProperties(source, t); return t; } public 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 = CONSTRUCTOR_ACCESS_CACHE.get(targetClass.getName()); if (constructorAccess != null) { return constructorAccess; } try { constructorAccess = ConstructorAccess.get(targetClass); constructorAccess.newInstance(); CONSTRUCTOR_ACCESS_CACHE.put(targetClass.toString(), constructorAccess); } catch (Exception e) { throw new RuntimeException(format("Create new instance of %s failed: %s", targetClass, e.getMessage())); } return constructorAccess; } }