java bean copy 探索

前言

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

場景重現(xiàn)

一日,糖哥接到需求焙格,需要新寫一個學(xué)生信息列表獲取的接口姻报,數(shù)據(jù)庫的獲取的方法底層已有封裝,但是考慮到信息保密需要隱藏一部分敏感字段〖涿現(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)系作者
  • 序言:七十年代末秉继,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子泽铛,更是在濱河造成了極大的恐慌尚辑,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盔腔,死亡現(xiàn)場離奇詭異杠茬,居然都是意外死亡月褥,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門瓢喉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宁赤,“玉大人,你說我怎么就攤上這事栓票【鲎螅” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵走贪,是天一觀的道長佛猛。 經(jīng)常有香客問我,道長坠狡,這世上最難降的妖魔是什么继找? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮逃沿,結(jié)果婚禮上婴渡,老公的妹妹穿的比我還像新娘。我一直安慰自己凯亮,他們只是感情好缩搅,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著触幼,像睡著了一般硼瓣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上置谦,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天堂鲤,我揣著相機與錄音,去河邊找鬼媒峡。 笑死瘟栖,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谅阿。 我是一名探鬼主播半哟,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼签餐!你這毒婦竟也來了寓涨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤氯檐,失蹤者是張志新(化名)和其女友劉穎戒良,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冠摄,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡糯崎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年几缭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沃呢。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡年栓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出薄霜,到底是詐尸還是另有隱情某抓,我是刑警寧澤邑雅,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布蚯舱,位于F島的核電站,受9級特大地震影響挚币,放射性物質(zhì)發(fā)生泄漏鸵熟。R本人自食惡果不足惜副编,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望流强。 院中可真熱鬧痹届,春花似錦、人聲如沸打月。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奏篙。三九已至柴淘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間秘通,已是汗流浹背为严。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肺稀,地道東北人第股。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像话原,于是被迫代替她去往敵國和親夕吻。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361