封面来源:碧蓝航线 破晓冰华 活动 CG
参考文章:
使用CGlib实现Bean拷贝(BeanCopier)
使用cglib进行bean拷贝
0. 前言
前面介绍了利用 MapStruct 实现 Bean 拷贝(进行 Java Bean 之间的转换),使用 MapStruct 需要引入相关的依赖,引入了依赖就意味着 BUG 出现率会增加,那有没有什么轻量级的方式呢?
Spring 利用了 CGLib 实现动态代理,在 CGLib 中有一个名为 BeanCopier
的类,利用这个类就可以进行 Bean 拷贝,并且由于 Spring 依赖 CGLib,因此无需再导入其他包。
1. 快速开始
1.1 BeanCopier 的特点
1、通过修改字节码来生成调用 Setter / Getter 的方法,所以效率极高(注意: 会判断使用的 Setter 方法的返回值是否为 void
,在使用了 Lombok 的 @Accessors(chain = true)
注解的情况下导致 Bean 拷贝失效)
2、针对属性名一致但类型不一致的情况,支持自定义转换器,但这个时候所有属性的转换都会走转换器
3、仅 支持符合 JavaBean
规范的属性
4、值拷贝,只有最外层的对象是新的,属性都是用的原来那个对象的值(不是深拷贝,属于浅拷贝)
1.2 简单的测试
创建一个 Maven 项目,引入以下依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.12</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-core</artifactId > <version > 5.3.3</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency >
再编写三个实体,用于测试 BeanCopier
。
1 2 3 4 5 6 7 8 9 10 @Getter @Setter public class User { private String name; private int age; }
1 2 3 4 5 6 7 8 9 10 11 @Getter @Setter public class UserDto { private String name; private int age; private String gender = "男" ; }
1 2 3 4 5 6 7 8 9 10 11 @Getter @Setter public class UserWithDiffTypeDto { private String name; private Integer age; }
属性名称、类型都相同
编写一个测试类,利用 BeanCopier
实现 User
与 UserDto
之间的转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class BeanCopierTest { @Test public void testSimpleCopy () { final BeanCopier copier = BeanCopier.create(User.class, UserDto.class, false ); User user = new User (); user.setName("mofan" ); user.setAge(19 ); UserDto userDto = new UserDto (); copier.copy(user, userDto, null ); Assert.assertEquals("mofan" , userDto.getName()); Assert.assertEquals(19 , userDto.getAge()); Assert.assertEquals("男" , userDto.getGender()); } }
运行该测试类,测试通过!
拷贝前后,目标对象中相比于源对象中多出的属性的默认值不会改变。
结论: 利用 BeanCopier
可以实现属性名相同、类型也相同的实体之间的拷贝。
属性名称相同、类型 不同
利用 BeanCopier
实现 User
与 UserWithDiffTypeDto
之间的转换:
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void testDiffTypeCopy () { final BeanCopier copier = BeanCopier.create(User.class, UserWithDiffTypeDto.class, false ); User user = new User (); user.setName("mofan" ); user.setAge(19 ); UserWithDiffTypeDto userWithDiffTypeDto = new UserWithDiffTypeDto (); copier.copy(user, userWithDiffTypeDto, null ); Assert.assertEquals("mofan" , userWithDiffTypeDto.getName()); Assert.assertNull(userWithDiffTypeDto.getAge()); }
结论: 利用 BeanCopier
不会拷贝属性名称相同而类型不同的属性。
注意: 即使 source 的属性类型是基本数据类型(int,short 和 char 等),目标类型是其包装类型(Integer,Short 和 Character 等),或相反,利用 BeanCopier
都不会进行拷贝。
总结: BeanCopier
只拷贝 名称和类型都相同的属性 。
1.3 拷贝细节
在 MapStruct 中,JavaBean 之间的转换依赖于实体中的无参构造方法、Getter 和 Setter,那利用 BeanCopier
呢?
其实也需要依赖于这些方法。
如果目标类和源类中的属性名称相同、类型也相同,它们中都有各个属性的 Getter 方法,但是在目标类中没有 Setter 方法,将无法拷贝成功。
简单来说:BeanCopier
是执行原理就是从源对象中 get 到数据,然后 set 到目标对象中。
1.4 自定义转换器
简单的自定义转换器
User
对象想转换成 UserWithDiffTypeDto
对象,就需要使用转换器 Converter
。
注意: 一旦使用 Converter
,BeanCopier
只 使用 Converter
定义的规则去拷贝属性,所以在 convert()
方法中要考虑所有的属性。
首先编写一个用于 User
对象转换的转换器 UserConverter
,这个转换器需要实现 Converter
接口,重写 convert()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class UserConverter implements Converter { @Override public Object convert (Object o, Class aClass, Object o1) { if (o instanceof Integer) { return (int ) o; } else if (o instanceof String) { return o; } return null ; } }
再编写一个测试方法,测试自定义转换器的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testUseConverter () { final BeanCopier copier = BeanCopier.create(User.class, UserWithDiffTypeDto.class, true ); User user = new User (); user.setName("mofan" ); user.setAge(19 ); UserWithDiffTypeDto userWithDiffTypeDto = new UserWithDiffTypeDto (); copier.copy(user, userWithDiffTypeDto, new UserConverter ()); Assert.assertEquals("mofan" , userWithDiffTypeDto.getName()); Assert.assertEquals(19 , userWithDiffTypeDto.getAge().intValue()); }
运行测试方法后,通过测试。
较为复杂的自定义转换器
前面自定义的转换器比较简单,为加深理解,用一个比较复杂的转换器。
提供两个实体用于转换:
1 2 3 4 5 6 7 8 9 10 11 @Getter @Setter public class Student { private String name; private int age; private Date createTime; }
1 2 3 4 5 6 7 8 9 10 11 @Getter @Setter public class StudentDto { private String name; private String age; private String createTime; }
从这两个实体可以看到:属性 age
的类型不一样,一个是 int
,一个是 String
;属性 createTime
的类型也不一样,一个是 Date
,一个是 String
。
如果不使用转换器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testStudentNoConverter () { final BeanCopier copier = BeanCopier.create(Student.class, StudentDto.class, false ); Student student = new Student (); student.setName("mofan" ); student.setAge(19 ); student.setCreateTime(new Date ()); StudentDto studentDto = new StudentDto (); copier.copy(student, studentDto, null ); Assert.assertEquals("mofan" , studentDto.getName()); Assert.assertNull(studentDto.getAge()); Assert.assertNull(studentDto.getCreateTime()); }
运行上述测试方法后,测试通过,age 和 createTime 属性都没有成功映射。
编写一个自定义转换器,用于它们之间的转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class StudentConverter implements Converter { SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd" ); @Override public Object convert (Object o, Class aClass, Object o1) { if (o instanceof Integer) { return String.valueOf(o); } else if (o instanceof Date) { Date date = (Date) o; return sdf.format(date); } else if (o instanceof String) { return o; } return null ; } }
再编写一个测试方法,使用自定义转换器进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testStudentUseConverter () { final BeanCopier copier = BeanCopier.create(Student.class, StudentDto.class, true ); Student student = new Student (); student.setName("mofan" ); student.setAge(19 ); student.setCreateTime(new Date (1616041733658L )); StudentDto studentDto = new StudentDto (); copier.copy(student, studentDto, new StudentConverter ()); Assert.assertEquals("mofan" , studentDto.getName()); Assert.assertEquals("19" , studentDto.getAge()); Assert.assertEquals("2021-03-18" , studentDto.getCreateTime()); }
运行上述测试代码后,测试通过,也表明自定义的转换器已经被成功使用。
2. 封装 BeanCopier
BeanCopier
拷贝的速度快,其性能瓶颈主要出现在创建 BeanCopier
实例的过程中,即调用 BeanCopier.create()
方法的时候。如果可以把创建过的 BeanCopier
实例放到缓存中(比如 HashMap 中),在下次使用的时候直接获取,这样就能提升性能。
要达成这个目的很简单,可以在类中声明一个静态的 HashMap,然后将两个对象的类名进行组合作为 Map 的 key,使用两个对象创建的 BeanCopier 实例作为 value,每次使用时先判断 Map 中是否有对应的 key,如果没有就添加一个,如果有直接利用组合得到 key 获取 BeanCopier 实例即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class CachedBeanCopier { static final Map<String, BeanCopier> BEAN_COPIERS = new HashMap <>(); public static void copy (Object srcObj, Object destObj) { String key = genKey(srcObj.getClass(), destObj.getClass()); BeanCopier copier = null ; if (!BEAN_COPIERS.containsKey(key)) { copier = BeanCopier.create(srcObj.getClass(), destObj.getClass(), false ); BEAN_COPIERS.put(key, copier); } else { copier = BEAN_COPIERS.get(key); } copier.copy(srcObj, destObj, null ); } private static String genKey (Class<?> srcClazz, Class<?> destClazz) { return srcClazz.getName() + destClazz.getName(); } }
但这只是最简单的封装,有没有高级一点的?🤔
诶 ~ 还真有,还可以让 BeanCopier 支持集合类型之间的拷贝。如果想让 Bean
和 Map
之间进行相互,BeanCopier
是做不到的。在 CGLib 里还有一个 BeanMap
,它可以完成这样的需求。BeanMap
是 Map
的一种实现,因此可以把它当做一个 Map
使用。
最终可以得到以下这样的封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public final class BeanCopyUtil { private BeanCopyUtil () { } private static ThreadLocal<ObjenesisStd> objenesisStdThreadLocal = ThreadLocal.withInitial(ObjenesisStd::new ); private static ConcurrentHashMap<Class<?>, ConcurrentHashMap<Class<?>, BeanCopier>> cache = new ConcurrentHashMap <>(); public static <T> T copy (Object source, Class<T> target) { return copy(source, objenesisStdThreadLocal.get().newInstance(target)); } public static <T> T copy (Object source, T target) { BeanCopier beanCopier = getCacheBeanCopier(source.getClass(), target.getClass()); beanCopier.copy(source, target, null ); return target; } public static <T> List<T> copyList (List<?> sources, Class<T> target) { if (sources.isEmpty()) { return Collections.emptyList(); } ArrayList<T> list = new ArrayList <>(sources.size()); ObjenesisStd objenesisStd = objenesisStdThreadLocal.get(); for (Object source : sources) { if (source == null ) { throw new RuntimeException (); } T newInstance = objenesisStd.newInstance(target); BeanCopier beanCopier = getCacheBeanCopier(source.getClass(), target); beanCopier.copy(source, newInstance, null ); list.add(newInstance); } return list; } public static <T> T mapToBean (Map<?, ?> source, Class<T> target) { T bean = objenesisStdThreadLocal.get().newInstance(target); BeanMap beanMap = BeanMap.create(bean); beanMap.putAll(source); return bean; } public static <T> Map<?, ?> beanToMap(T source) { return BeanMap.create(source); } private static <S, T> BeanCopier getCacheBeanCopier (Class<S> source, Class<T> target) { ConcurrentHashMap<Class<?>, BeanCopier> copierConcurrentHashMap = cache.computeIfAbsent(source, aClass -> new ConcurrentHashMap <>(16 )); return copierConcurrentHashMap.computeIfAbsent(target, aClass -> BeanCopier.create(source, target, false )); } }
除此之外,还可以针对 Java 自带的反射效率低下进行封装,在此需要引入一个第三方类库:
1 2 3 4 5 <dependency > <groupId > com.esotericsoftware</groupId > <artifactId > reflectasm</artifactId > <version > 1.11.9</version > </dependency >
最终得到的工具类如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 public class WrapperBeanCopier { 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; } 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; } }