java bean copy 探索

前言

作为一个JAVA后端开发,日常工作中不免会经常用到对象拷贝,本篇就从实际案例出发,进行各种方案的实践对比。

场景重现

一日,糖哥接到需求,需要新写一个学生信息列表获取的接口,数据库的获取的方法底层已有封装,但是考虑到信息保密需要隐藏一部分敏感字段。现在我们来看下已有的StudentDO和新添加的StudentTO类。

@Data
Class StudentDO {
    private Long id;
    private String name;
    private String idCard;
    private String tel;
    private Integer age;
}

@Data
Class StudentTO {
    private Long id;
    private String name;
    private Integer age;
}

根据已有的方法可以获取到StudentDO的List,但是在实际输出时需要将其转换成StudentTO的List。

方案和思路

1.遍历然后get/set

这是最容易想到的办法。具体实现如下:

public List<StudentTO> copyList(List<StudentDO> doList) {
    List<StudentTO> toList = new ArrayList<>();
    for (StudentDO item : doList) {
        StudentTO to = new StudentTO();
        to.setId(item.getId());
        to.setName(item.getName());
        to.setAge(item.getAge());
        toList.add(to);
    }
    return toList;
}

从代码性能来说,这种方式是最高效的,但是缺点是每次都要去基于不同的类实现不同的转换方法,在编码效率上是极低的。

2.反射实现通用性对象拷贝(Spring BeanUtils)

反射是java的一种特性,一般我们都会用它去解决一些通用性的问题。针对当前的问题,通用的解决思路就是将源对象与目标对象的相同属性值设置到目标对象中

基于反射去实现对象拷贝有很多种,我们拿其中使用较为普遍的Spring BeanUtils举例。

我们先来看看基于Spring BeanUtils怎么解决上述问题。

    public void studentCopyList(List<StudentDO> dolist) {
        // spring BeanUtils实现
        List<StudentTO> studentTOList1 = springBeanUtilsCopyList(dolist, StudentTO.class);
    }
    
    public static <T> List<T> springBeanUtilsCopyList(List<?> objects, Class<T> class1) {
        try {
            if (objects == null || objects.isEmpty()) {
                return Collections.emptyList();
            }
            List<T> res = new ArrayList<>();
            for (Object s : objects) {
                T t = class1.newInstance();
                BeanUtils.copyProperties(s, t);
                res.add(t);
            }
            return res;
        } catch (InstantiationException | IllegalAccessException e) {
            return Collections.emptyList();
        }
    }

再来看看Spring BeanUtils的部分核心源码。

private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties)
            throws BeansException {

        Assert.notNull(source, "Source must not be null");
        Assert.notNull(target, "Target must not be null");

        Class<?> actualEditable = target.getClass();
        if (editable != null) {
            if (!editable.isInstance(target)) {
                throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
                        "] not assignable to Editable class [" + editable.getName() + "]");
            }
            actualEditable = editable;
        }
        PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
        List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);

        for (PropertyDescriptor targetPd : targetPds) {
            Method writeMethod = targetPd.getWriteMethod();
            if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
                PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                if (sourcePd != null) {
                    Method readMethod = sourcePd.getReadMethod();
                    if (readMethod != null &&
                            ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                        try {
                            if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                readMethod.setAccessible(true);
                            }
                            Object value = readMethod.invoke(source);
                            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                writeMethod.setAccessible(true);
                            }
                            writeMethod.invoke(target, value);
                        }
                        catch (Throwable ex) {
                            throw new FatalBeanException(
                                    "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                        }
                    }
                }
            }
        }
    }

可以看到其主要就是利用了反射机制,先遍历目标对象的属性值,当发现源对象中有相同属性值时进行设置。

这种做法的好处就是通用性很强,但是缺点是反射会降低性能,尤其在调用量大的时候越发明显。

3.即时编译实现对象拷贝(cglib BeanCopier)

我们知道java不仅仅是一门静态编译语言,还带有即时编译的特性。思路是我们可以根据入参来动态生成相应的get/set代码处理逻辑,并即时编译运行。

这里我们举例基于cglib实现的BeanCopier,该工具类目前也引入在spring的core包中。先来看看如何使用。

    public void studentCopyList(List<StudentDO> dolist) {
        // cglib BeanCopier实现
        List<StudentTO> studentTOList2 = cglibBeanCopierCopyList(dolist, StudentTO.class);
    }
    
    public static <T> List<T> cglibBeanCopierCopyList(List<?> objects, Class<T> targetClass) {
        try {
            if (objects == null || objects.isEmpty()) {
                return Collections.emptyList();
            }
            List<T> res = new ArrayList<>();
            for (Object s : objects) {
                T t = targetClass.newInstance();
                BeanCopier copier = BeanCopier.create(s.getClass(), t.getClass(), false);
                copier.copy(s, t, null);
                res.add(t);
            }
            return res;
        } catch (InstantiationException | IllegalAccessException e) {
            return Collections.emptyList();
        }
    }

再来看看其源码实现,其主要逻辑可以参考生成class部分的代码:

public void generateClass(ClassVisitor v) {
            Type sourceType = Type.getType(this.source);
            Type targetType = Type.getType(this.target);
            ClassEmitter ce = new ClassEmitter(v);
            ce.begin_class(46, 1, this.getClassName(), BeanCopier.BEAN_COPIER, (Type[])null, "<generated>");
            EmitUtils.null_constructor(ce);
            CodeEmitter e = ce.begin_method(1, BeanCopier.COPY, (Type[])null);
            PropertyDescriptor[] getters = ReflectUtils.getBeanGetters(this.source);
            PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(this.target);
            Map names = new HashMap();

            for(int i = 0; i < getters.length; ++i) {
                names.put(getters[i].getName(), getters[i]);
            }

            Local targetLocal = e.make_local();
            Local sourceLocal = e.make_local();
            if (this.useConverter) {
                e.load_arg(1);
                e.checkcast(targetType);
                e.store_local(targetLocal);
                e.load_arg(0);
                e.checkcast(sourceType);
                e.store_local(sourceLocal);
            } else {
                e.load_arg(1);
                e.checkcast(targetType);
                e.load_arg(0);
                e.checkcast(sourceType);
            }

            for(int i = 0; i < setters.length; ++i) {
                PropertyDescriptor setter = setters[i];
                PropertyDescriptor getter = (PropertyDescriptor)names.get(setter.getName());
                if (getter != null) {
                    MethodInfo read = ReflectUtils.getMethodInfo(getter.getReadMethod());
                    MethodInfo write = ReflectUtils.getMethodInfo(setter.getWriteMethod());
                    if (this.useConverter) {
                        Type setterType = write.getSignature().getArgumentTypes()[0];
                        e.load_local(targetLocal);
                        e.load_arg(2);
                        e.load_local(sourceLocal);
                        e.invoke(read);
                        e.box(read.getSignature().getReturnType());
                        EmitUtils.load_class(e, setterType);
                        e.push(write.getSignature().getName());
                        e.invoke_interface(BeanCopier.CONVERTER, BeanCopier.CONVERT);
                        e.unbox_or_zero(setterType);
                        e.invoke(write);
                    } else if (compatible(getter, setter)) {
                        e.dup2();
                        e.invoke(read);
                        e.invoke(write);
                    }
                }
            }

            e.return_value();
            e.end_method();
            ce.end_class();
        }

逻辑可以看到和基于反射的spring BeanUtils是一致的,只是实现方式不同。(cglib主要是利用了 Asm 字节码技术

该种方式即解决了日常使用的编码效率问题,又优化了整个执行过程中的性能损耗。

4.注解处理器实现对象拷贝(mapstruct)

java源码编译由以下3个过程组成

  • 分析和输入到符号表
  • 注解处理
  • 语义分析和生成class文件

很多工具其实都会基于注解处理器来实现相应的功能,例如常用的lombok等。
本次介绍的mapstruct也是同样的原理。

使用mapstruct会比之前的两种方法多一个步骤就是需要创建一个interface类,具体实现如下:

    @Resource
    private StudentMapper studentMapper;

    public void studentCopyList(List<StudentDO> dolist) {
        // 基于mapstruct实现
        List<StudentTO> studentTOList3 = studentMapper.toTOList(dolist);
    }

对应需要创建的接口类:

@Mapper(componentModel = "spring")
public interface StudentMapper {
    List<StudentTO> toTOList(List<StudentDO> doList);
}

在源码编译阶段,注解处理器根据@Mapper注解会自动生成StudentMapper对应的实现类。

@Component
public class StudentMapperImpl implements StudentMapper {
    public StudentMapperImpl() {
    }

    public List<StudentTO> toTOList(List<StudentDO> doList) {
        if (doList == null) {
            return null;
        } else {
            List<StudentTO> list = new ArrayList(doList.size());
            Iterator var3 = doList.iterator();

            while(var3.hasNext()) {
                StudentDO studentDO = (StudentDO)var3.next();
                list.add(this.studentDOToStudentTO(studentDO));
            }

            return list;
        }
    }

    protected StudentTO studentDOToStudentTO(StudentDO studentDO) {
        if (studentDO == null) {
            return null;
        } else {
            StudentTO studentTO = new StudentTO();
            studentTO.setId(studentDO.getId());
            studentTO.setName(studentDO.getName());
            studentTO.setAge(studentDO.getAge());
            return studentTO;
        }
    }
}

相较之下,mapstruct每次实现调用的复杂度上会高一点,但是从性能上看是最优的,最接近原生的get/set调用实现。

性能对比

参考上面的案例,按list中元素个数,单次拷贝的耗时(单位:ms)横向对比如下:

方案 10个 100个 10000个 1000000个
Spring BeanUtils(反射) 650 723 770 950
cglib BeanCopier(asm字节码技术) 48 60 65 300
mapstruct(注解处理器) 3 4 5 40

可以看到mapstruct确实是性能最好的。但是另外还发现基于反射实现的Spring BeanUtils并没有随着调用次数的增大而大大提升耗时,与预期不符。这个其实不难想象是spring做了一层缓存,对于同一个类会缓存住反射获取的信息,参考CachedIntrospectionResults中的代码。

总结

从综合角度来看,mapstruct是最优解,但是日常使用中如果对于性能要求并没有那么高,其实其他的方案也是可以选择的,毕竟可以实现更好的封装复用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353