注:本文在讲 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