封面来源:碧蓝航线 破晓冰华 活动 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
/**
* @author mofan
* @date 2021/3/18
*/
@Getter
@Setter
public class User {
private String name;
private int age;
}
1
2
3
4
5
6
7
8
9
10
11
/**
* @author mofan
* @date 2021/3/18
*/
@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
/**
* @author mofan
* @date 2021/3/18
*/
@Getter
@Setter
public class UserWithDiffTypeDto {
private String name;
// 注意此处的类型是 int 的包装类
private Integer age;
}

属性名称、类型都相同

编写一个测试类,利用 BeanCopier 实现 UserUserDto 之间的转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author mofan
* @date 2021/3/18
*/
public class BeanCopierTest {
@Test
public void testSimpleCopy() {
// create(Class source, Class target, boolean useConverter)
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 实现 UserUserWithDiffTypeDto 之间的转换:

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

注意: 一旦使用 ConverterBeanCopier 使用 Converter 定义的规则去拷贝属性,所以在 convert() 方法中要考虑所有的属性。

首先编写一个用于 User 对象转换的转换器 UserConverter,这个转换器需要实现 Converter 接口,重写 convert() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author mofan
* @date 2021/3/18
*/
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
/**
* @author mofan
* @date 2021/3/18
*/
@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
/**
* @author mofan
* @date 2021/3/18
*/
@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
/**
* @author mofan
* @date 2021/3/18
*/
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
/**
* @author mofan
* @date 2021/3/18
*/
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 支持集合类型之间的拷贝。如果想让 BeanMap 之间进行相互,BeanCopier 是做不到的。在 CGLib 里还有一个 BeanMap,它可以完成这样的需求。BeanMapMap 的一种实现,因此可以把它当做一个 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
/**
* @author mofan
* @date 2021/3/18
*/
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
/**
* @author mofan
* @date 2021/3/18
*/
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;
}

}