注:本文在讲
BeanUtils
时,主要指的是BeanUtils.copyProperties
方法。
前几日在工作时需要处理 VO 到 DO、DO 到 VO 的转换问题,写了一堆 setter 和 getter 来手动转换。于是前辈问我,为啥不用 BeanUtil.copyProperties
来转换呢?我脱口而出这方法性能有问题,而且我当时是在一个分页结果的循环里调用的,这个循环最高会跑 100 次。循环次数不高,但是用 copyProrerties 还是总有一种膈应的感觉。
虽然我知道性能有问题,但是问题有多大我还不清楚呢。于是悄咪咪写了个 demo,和我自己实现的反射转换方法对比了一下。
数据类
首先定义两个数据类:
public record ARecord(
Long id,
Integer num,
String type,
String type01,
String type02,
String msg
) {
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AClass {
Long id;
Integer num;
String type;
String type01;
String type02;
String msg;
}
测试方法
把一个 record 转换成一个有 setter 和 getter 的类,分别用 setter、我自己写的转换方法、BeanUtils.copyPorperties
转换 500w 次,记录时间。
public class BeanUtilsBench {
public static void main(String[] args) {
ARecord aRecord = new ARecord(1L, 123, "type", "type011", "type02","msg");
int loop = 5_000_000;
long start = System.currentTimeMillis();
for (int i = 0; i < loop; i++) {
AClass aClass = stpd(aRecord);
Long id = aClass.getId();
}
long end1 = System.currentTimeMillis();
System.out.printf("setter\t\t%5d ms.\n", end1 - start);
AClass aClass1 = new AClass(1L, 123, "type", "type011", "type02","msg");
for (int i = 0; i < loop; i++) {
ARecord aRecord1 = model2Vo(aClass1, ARecord.class);
}
long end2 = System.currentTimeMillis();
System.out.printf("model2vo\t%5d ms.\n", end2 - end1);
for (int i = 0; i < loop; i++) {
AClass aClass = new AClass();
BeanUtils.copyProperties(aRecord, aClass);
}
long end3 = System.currentTimeMillis();
System.out.printf("beanutils\t%5d ms.\n", end3 - end2);
}
public static AClass stpd(ARecord aRecord) {
AClass res = new AClass();
res.setId(aRecord.id());
res.setNum(aRecord.num());
res.setType(aRecord.type());
res.setType01(aRecord.type01());
res.setType02(aRecord.type02());
return res;
}
public static <T> T model2Vo(AClass obj, Class<T> clazz) {
if (!clazz.isRecord()) {
return null;
}
List<Field> fieldList = new ArrayList<>(List.of(obj.getClass().getDeclaredFields()));
fieldList.addAll(List.of(obj.getClass().getSuperclass().getDeclaredFields()));
Constructor<T> constructor = (Constructor<T>) clazz.getConstructors()[0];
Parameter[] parameters = constructor.getParameters();
List<Object> res = new ArrayList<>(parameters.length);
for (Parameter parameter : parameters) {
for (Field field : fieldList) {
field.setAccessible(true);
if (field.getName().equals(parameter.getName())) {
try {
res.add(field.get(obj));
} catch (IllegalAccessException ignore) {
}
break;
}
}
}
try {
return constructor.newInstance(res.toArray());
} catch (InstantiationException | InvocationTargetException | IllegalAccessException ignore) {
}
return null;
}
}
测试结果
setter 10 ms.
model2vo 8560 ms.
beanutils 20666 ms.
可以看到,setter 是最快的,碾压级的性能。我自己写的由于针对场景优化,比 beanutils 少处理很多东西,所以比它快 60% 左右。
不过这个测试也说明,除非有成千上万需要转换的数据,不然使用 BeanUtils
带来的性能下降是可以忽略的。经过进一步的思考,我觉得不用 BeanUtils
还有一个更强大的原因,那就是 BeanUtils
模糊了编译期 Java 类型的检查,让一些本来能在编译期检查出来的错误不容易被发现。比如说某个成员变量更改了类型或者变量名,使用 BeanUtils
转换会直接忽略这个变量。我目前的解决方案(也不算是解决方案吧,就打了个警告),是加了一个 count,如果转换的变量少于成员变量总数的一半,就打一个 WARNING 或者 DEBUG 日志,告诉调试方这个转换可能有问题。
MapStruct
其实这种警告啥的需要有详细的测试才能发现,如果项目赶的话这种错误可能要排查半天才能查出来。我最近才发现这种转换有一个更好的工具 MapStruct
它能像 mybatis 注入 mapper 一样,只需定义一个接口,就能注入用来类型转换的类。这个类使用的是原来的 getter 和 setter,没有反射的性能消耗。
2023-07-30