java bean copy 探索

前言

作為一個JAVA后端開發(fā),日常工作中不免會經(jīng)常用到對象拷貝,本篇就從實際案例出發(fā),進行各種方案的實踐對比。

場景重現(xiàn)

一日,糖哥接到需求,需要新寫一個學(xué)生信息列表獲取的接口,數(shù)據(jù)庫的獲取的方法底層已有封裝,但是考慮到信息保密需要隱藏一部分敏感字段?,F(xiàn)在我們來看下已有的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;
}

根據(jù)已有的方法可以獲取到StudentDO的List,但是在實際輸出時需要將其轉(zhuǎn)換成StudentTO的List。

方案和思路

1.遍歷然后get/set

這是最容易想到的辦法。具體實現(xiàn)如下:

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;
}

從代碼性能來說,這種方式是最高效的,但是缺點是每次都要去基于不同的類實現(xiàn)不同的轉(zhuǎn)換方法,在編碼效率上是極低的。

2.反射實現(xiàn)通用性對象拷貝(Spring BeanUtils)

反射是java的一種特性,一般我們都會用它去解決一些通用性的問題。針對當(dāng)前的問題,通用的解決思路就是將源對象與目標(biāo)對象的相同屬性值設(shè)置到目標(biāo)對象中。

基于反射去實現(xiàn)對象拷貝有很多種,我們拿其中使用較為普遍的Spring BeanUtils舉例。

我們先來看看基于Spring BeanUtils怎么解決上述問題。

    public void studentCopyList(List<StudentDO> dolist) {
        // spring BeanUtils實現(xiàn)
        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);
                        }
                    }
                }
            }
        }
    }

可以看到其主要就是利用了反射機制,先遍歷目標(biāo)對象的屬性值,當(dāng)發(fā)現(xiàn)源對象中有相同屬性值時進行設(shè)置。

這種做法的好處就是通用性很強,但是缺點是反射會降低性能,尤其在調(diào)用量大的時候越發(fā)明顯。

3.即時編譯實現(xiàn)對象拷貝(cglib BeanCopier)

我們知道java不僅僅是一門靜態(tài)編譯語言,還帶有即時編譯的特性。思路是我們可以根據(jù)入?yún)韯討B(tài)生成相應(yīng)的get/set代碼處理邏輯,并即時編譯運行。

這里我們舉例基于cglib實現(xiàn)的BeanCopier,該工具類目前也引入在spring的core包中。先來看看如何使用。

    public void studentCopyList(List<StudentDO> dolist) {
        // cglib BeanCopier實現(xiàn)
        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();
        }
    }

再來看看其源碼實現(xiàn),其主要邏輯可以參考生成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是一致的,只是實現(xiàn)方式不同。(cglib主要是利用了 Asm 字節(jié)碼技術(shù)

該種方式即解決了日常使用的編碼效率問題,又優(yōu)化了整個執(zhí)行過程中的性能損耗。

4.注解處理器實現(xiàn)對象拷貝(mapstruct)

java源碼編譯由以下3個過程組成

  • 分析和輸入到符號表
  • 注解處理
  • 語義分析和生成class文件

很多工具其實都會基于注解處理器來實現(xiàn)相應(yīng)的功能,例如常用的lombok等。
本次介紹的mapstruct也是同樣的原理。

使用mapstruct會比之前的兩種方法多一個步驟就是需要創(chuàng)建一個interface類,具體實現(xiàn)如下:

    @Resource
    private StudentMapper studentMapper;

    public void studentCopyList(List<StudentDO> dolist) {
        // 基于mapstruct實現(xiàn)
        List<StudentTO> studentTOList3 = studentMapper.toTOList(dolist);
    }

對應(yīng)需要創(chuàng)建的接口類:

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

在源碼編譯階段,注解處理器根據(jù)@Mapper注解會自動生成StudentMapper對應(yīng)的實現(xiàn)類。

@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每次實現(xiàn)調(diào)用的復(fù)雜度上會高一點,但是從性能上看是最優(yōu)的,最接近原生的get/set調(diào)用實現(xiàn)。

性能對比

參考上面的案例,按list中元素個數(shù),單次拷貝的耗時(單位:ms)橫向?qū)Ρ热缦拢?/p>

方案 10個 100個 10000個 1000000個
Spring BeanUtils(反射) 650 723 770 950
cglib BeanCopier(asm字節(jié)碼技術(shù)) 48 60 65 300
mapstruct(注解處理器) 3 4 5 40

可以看到mapstruct確實是性能最好的。但是另外還發(fā)現(xiàn)基于反射實現(xiàn)的Spring BeanUtils并沒有隨著調(diào)用次數(shù)的增大而大大提升耗時,與預(yù)期不符。這個其實不難想象是spring做了一層緩存,對于同一個類會緩存住反射獲取的信息,參考CachedIntrospectionResults中的代碼。

總結(jié)

從綜合角度來看,mapstruct是最優(yōu)解,但是日常使用中如果對于性能要求并沒有那么高,其實其他的方案也是可以選擇的,畢竟可以實現(xiàn)更好的封裝復(fù)用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容