Spring深入分析講解BeanUtils的實(shí)現(xiàn)
背景
DO
DO是Data Object的簡(jiǎn)寫,叫做數(shù)據(jù)實(shí)體,既然是數(shù)據(jù)實(shí)體,那么也就是和存儲(chǔ)層打交道的實(shí)體類,應(yīng)用從存儲(chǔ)層拿到的數(shù)據(jù)是以行為單位的數(shù)據(jù),不具備java特性,那么如果要和java屬性結(jié)合起來(lái)或者說(shuō)在業(yè)務(wù)中流轉(zhuǎn),那么一定要轉(zhuǎn)換成java對(duì)象(反過(guò)來(lái)java要和持久層打交道也要把java對(duì)象轉(zhuǎn)換成行數(shù)據(jù)),那么就需要DO作為行數(shù)據(jù)的一個(gè)載體,把行的每一個(gè)列屬性映射到j(luò)ava對(duì)象的每一個(gè)字段。
BO
BO是Business Object的簡(jiǎn)寫,是業(yè)務(wù)對(duì)象,區(qū)別于DO的純數(shù)據(jù)描述,BO用于在應(yīng)用各個(gè)模塊之間流轉(zhuǎn),具備一定的業(yè)務(wù)含義,一般情況像BO是應(yīng)用自己定義的業(yè)務(wù)實(shí)體,對(duì)持久層和二方或三方接口接口響應(yīng)結(jié)果的封裝,這里插一句,為什么有了DO和外部依賴的實(shí)體類,為什么還需要BO?對(duì)于領(lǐng)域內(nèi)持久層交互來(lái)說(shuō),BO層有時(shí)候可以省略(大部分場(chǎng)景字段屬性基本一致),而對(duì)于和領(lǐng)域外二方或三方服務(wù)交互來(lái)說(shuō),增加BO實(shí)體的目的主要是降低外部實(shí)體對(duì)領(lǐng)域內(nèi)其它層的侵入,以及降低外部實(shí)體簽名變更對(duì)領(lǐng)域內(nèi)其它層的影響,舉個(gè)例子將調(diào)用訂單服務(wù)的響應(yīng)結(jié)果在代理層封裝成BO供上層使用,那么如果訂單實(shí)體內(nèi)部屬性簽名發(fā)生變更或者升級(jí),那么只需要改BO即可,只影響應(yīng)用的代理層,中間業(yè)務(wù)流轉(zhuǎn)層完全不受影響。
DTO
DTO是Data Transfer Object的縮寫,叫做數(shù)據(jù)傳輸對(duì)象,主要用于跨服務(wù)之間的數(shù)據(jù)傳輸,如公司內(nèi)部做了微服務(wù)拆封,那么微服務(wù)之間的數(shù)據(jù)交互就是以DTO作為數(shù)據(jù)結(jié)果響應(yīng)載體,另外DTO的存在也是對(duì)外部依賴屏蔽了領(lǐng)域內(nèi)底層數(shù)據(jù)的結(jié)構(gòu),假如直接返回DO給依賴方,那么我們的表結(jié)構(gòu)也就一覽無(wú)余了,在公司內(nèi)部還好,對(duì)于也利益關(guān)系的團(tuán)隊(duì)之間有服務(wù)交互采取這種方式,那么就可能產(chǎn)生安全問(wèn)題和不必要的糾紛。
VO
值對(duì)象(Value Object),其存在的意思主要是數(shù)據(jù)展示,其直接包含具有業(yè)務(wù)含義的數(shù)據(jù),和前端打交道,由業(yè)務(wù)層將DO或者BO轉(zhuǎn)換為VO供前端使用。
前邊介紹了幾種常用的數(shù)據(jù)實(shí)體,那么一個(gè)關(guān)鍵的問(wèn)題就出現(xiàn)了,既然應(yīng)用分了那么多層,每個(gè)層使用的數(shù)據(jù)實(shí)體可能不一樣,也必然會(huì)存在實(shí)體之間的轉(zhuǎn)換問(wèn)題,也是本篇文章需要重點(diǎn)講述的問(wèn)題。
數(shù)據(jù)實(shí)體轉(zhuǎn)換
所謂數(shù)據(jù)實(shí)體轉(zhuǎn)換,就是將源數(shù)據(jù)實(shí)體存儲(chǔ)的數(shù)據(jù)轉(zhuǎn)換到目標(biāo)實(shí)體的實(shí)例對(duì)象存儲(chǔ),比如把BO轉(zhuǎn)換成VO數(shù)據(jù)響應(yīng)給前端,那么就需要將源數(shù)據(jù)實(shí)體的屬性值逐個(gè)映射到目標(biāo)數(shù)據(jù)實(shí)體并賦值,也就是VO.setXxx(BO.getXxx()),當(dāng)然我們可以選擇最原始最笨重的方式,逐個(gè)遍歷源數(shù)據(jù)實(shí)體的屬性然后賦值給新數(shù)據(jù)實(shí)體,也可以利用java的反射來(lái)實(shí)現(xiàn)。
就目前比較可行的以及可行的方案中,比較常用的有逐個(gè)set,和利用工具類賦值。
在數(shù)據(jù)實(shí)體字段比較少或者字段類型比較復(fù)雜的情況下,可以考慮使用逐個(gè)字段賦值的方式,但是如果字段相對(duì)較多,那么就會(huì)出現(xiàn)一個(gè)實(shí)體類轉(zhuǎn)換就寫了幾十行甚至上百行的代碼,這是完全不能接受的,那么我們就需要自己實(shí)現(xiàn)反射或者使用線程的工具類來(lái)實(shí)現(xiàn)了,當(dāng)然工具類有很多,比如apache的common包有BeanUtils實(shí)現(xiàn),spring-beans有BeanUtils實(shí)現(xiàn)以及Guava也有相關(guān)實(shí)現(xiàn),其他的暫且不論,這里我們就從源碼維度分析一下使用spring-beans的BeanUtils做數(shù)據(jù)實(shí)體轉(zhuǎn)換的實(shí)現(xiàn)原理和可能會(huì)存在的坑。
使用方式
在數(shù)據(jù)實(shí)體轉(zhuǎn)換時(shí),用的最多的就是BeanUtils#copyProperties方法,基本用法就是:
//DO是源數(shù)據(jù)對(duì)象,DTO是目標(biāo)對(duì)象,把源類的數(shù)據(jù)拷貝到目標(biāo)對(duì)象 BeanUtils.copyProperties(DO,DTO);
原理&源碼分析
直接看方法簽名:
/** * Copy the property values of the given source bean into the target bean. * <p>Note: The source and target classes do not have to match or even be derived * from each other, as long as the properties match. Any bean properties that the * source bean exposes but the target bean does not will silently be ignored. * <p>This is just a convenience method. For more complex transfer needs, * consider using a full BeanWrapper. * @param source the source bean * @param target the target bean * @throws BeansException if the copying failed * @see BeanWrapper */ public static void copyProperties(Object source, Object target) throws BeansException { copyProperties(source, target, null, (String[]) null); }
方法注釋的大致意思是,將給定的源bean的屬性值復(fù)制到目標(biāo)bean中,源類和目標(biāo)類不必匹配,甚至不必派生
彼此,只要屬性匹配即可,源bean中有但目標(biāo)bean中沒(méi)有的屬性將被忽略。
上述方法直接調(diào)用了重載方法,多了兩個(gè)入?yún)?
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException { Assert.notNull(source, "Source must not be null"); Assert.notNull(target, "Target must not be null"); //目標(biāo)Class 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; } //1.獲取目標(biāo)Class的屬性描述 PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable); List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null); //2.遍歷源Class的屬性 for (PropertyDescriptor targetPd : targetPds) { //源Class屬性的寫方法,setXXX Method writeMethod = targetPd.getWriteMethod(); //3.如果存在寫方法,并且該屬性不忽略,繼續(xù)往下走,否則跳過(guò)繼續(xù)遍歷 if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) { //4.獲取源Class的與目標(biāo)屬性同名的屬性描述 PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName()); //5.如果源屬性描述不存在直接跳過(guò),否則繼續(xù)往下走 if (sourcePd != null) { //獲取源屬性描述的讀方法 Method readMethod = sourcePd.getReadMethod(); //6.如果源屬性描述的讀防范存在且返回?cái)?shù)據(jù)類型和目標(biāo)屬性的寫方法入?yún)㈩愋拖嗤蛘吲缮? //繼續(xù)往下走,否則直接跳過(guò)繼續(xù)下次遍歷 if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) { try { //如果源屬性讀方法修飾符不是public,那么修改為可訪問(wèn) if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) { readMethod.setAccessible(true); } //7.讀取源屬性的值 Object value = readMethod.invoke(source); //如果目標(biāo)屬性的寫方法修飾符不是public,則修改為可訪問(wèn) if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) { writeMethod.setAccessible(true); } //8.通過(guò)反射將源屬性值賦值給目標(biāo)屬性 writeMethod.invoke(target, value); } catch (Throwable ex) { throw new FatalBeanException( "Could not copy property '" + targetPd.getName() + "' from source to target", ex); } } } } } }
方法的具體實(shí)現(xiàn)中增加了詳細(xì)的注釋,基本上能夠看出來(lái)其實(shí)現(xiàn)原理是通過(guò)反射,但是里邊有兩個(gè)地方我們需要關(guān)注一下:
//獲取目標(biāo)bean屬性描述 PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable); //獲取源bean指定名稱的屬性描述 PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
其實(shí)兩個(gè)調(diào)用底層實(shí)現(xiàn)一樣,那么我們就對(duì)其中一個(gè)做一下分析即可,繼續(xù)跟進(jìn)看getPropertyDescriptors(actualEditable)實(shí)現(xiàn):
/** * Retrieve the JavaBeans {@code PropertyDescriptor}s of a given class. * @param clazz the Class to retrieve the PropertyDescriptors for * @return an array of {@code PropertyDescriptors} for the given class * @throws BeansException if PropertyDescriptor look fails */ public static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) throws BeansException { CachedIntrospectionResults cr = CachedIntrospectionResults.forClass(clazz); return cr.getPropertyDescriptors(); }
該方法是獲取指定Class的屬性描述,調(diào)用了CachedIntrospectionResults的forClass方法,從名稱中可以知道改方法返回一個(gè)緩存的自省結(jié)果,然后返回結(jié)果中的屬性描述,繼續(xù)看實(shí)現(xiàn):
@SuppressWarnings("unchecked") static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException { //1.從強(qiáng)緩存獲取beanClass的內(nèi)省結(jié)果,如果有數(shù)據(jù)直接返回 CachedIntrospectionResults results = strongClassCache.get(beanClass); if (results != null) { return results; } //2.如果強(qiáng)緩存中不存在beanClass的內(nèi)省結(jié)果,則從軟緩存中獲取beanClass的內(nèi)省結(jié)果,如果存在直接返回 results = softClassCache.get(beanClass); if (results != null) { return results; } //3.如果強(qiáng)緩存和軟緩存都不存在beanClass的自省結(jié)果,則創(chuàng)建一個(gè) results = new CachedIntrospectionResults(beanClass); ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse; //4.如果beanClass是緩存安全的,或者beanClass的類加載器是配置可接受的,緩存引用指向強(qiáng)緩存 if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) || isClassLoaderAccepted(beanClass.getClassLoader())) { classCacheToUse = strongClassCache; } else { //5.如果不是緩存安全,則將緩存引用指向軟緩存 if (logger.isDebugEnabled()) { logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe"); } classCacheToUse = softClassCache; } //6.將beanClass內(nèi)省結(jié)果放入緩存 CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results); //7.返回內(nèi)省結(jié)果 return (existing != null ? existing : results); }
該方法中有幾個(gè)比較重要的概念,強(qiáng)引用、軟引用、緩存、緩存安全、類加載和內(nèi)省等,簡(jiǎn)單介紹一下概念:
- 強(qiáng)引用: 常見(jiàn)的用new方式創(chuàng)建的引用,只要有引用存在,就算出現(xiàn)OOM也不會(huì)回收這部分內(nèi)存空間
- 軟引用: 引用強(qiáng)度低于強(qiáng)引用,在出現(xiàn)OOM之前垃圾回收器會(huì)嘗試回收這部分存儲(chǔ)空間,如果仍不夠用則報(bào)OOM
- 緩存安全:檢查beanClass是否是CachedIntrospectionResults的類加載器或者其父類加載器加載的
- 類加載:雙親委派
- 內(nèi)省:是java提供的一種獲取對(duì)bean的屬性、事件描述的方式
方法的作用是先嘗試從強(qiáng)引用緩存中獲取beanClass的自省結(jié)果,如果存在則直接返回,如果不存在則嘗試從軟引用緩存中獲取自省結(jié)果,如果存在直接返回,否則利用java自省特性生成beanClass屬性描述,如果緩存安全或者beanClass的類加載器是可接受的,將結(jié)果放入強(qiáng)引用緩存,否則放入軟引用緩存,最后返回結(jié)果。
屬性賦值類型擦除
我們?cè)谡J褂肂eanUtils的copyProperties是沒(méi)有問(wèn)題的,但是在有些場(chǎng)景下會(huì)出現(xiàn)問(wèn)題,我們看下面的代碼:
public static void main(String[] args) { Demo1 demo1 = new Demo1(Arrays.asList("1","2","3")); Demo2 demo2 = new Demo2(); BeanUtils.copyProperties(demo1,demo2); for (Integer integer : demo2.getList()) { System.out.println(integer); } for (String s : demo1.getList()) { demo2.addList(Integer.valueOf(s)); } } @Data static class Demo1 { private List<String> list; public Demo1(List<String> list) { this.list = list; } } @Data static class Demo2 { private List<Integer> list; public void addList(Integer target) { if(null == list) { list = new ArrayList<>(); } list.add(target); } }
很簡(jiǎn)單,就是利用BeanUtils將demo1的屬性值復(fù)制到demo2,看上去沒(méi)什么問(wèn)題,并且代碼也是編譯通過(guò)的,但是運(yùn)行后發(fā)現(xiàn):
類型轉(zhuǎn)換失敗,為什么?這里提一下泛型擦除的概念,說(shuō)白了就是所有的泛型類型(除extends和super)編譯后都換變成Object類型,也就是說(shuō)上邊的例子中代碼編譯后兩個(gè)類的list屬性的類型都會(huì)變成List<Object>,主要是兼容1.5之前的無(wú)泛型類型,那么在使用BeanUtils工具類進(jìn)行復(fù)制的時(shí)候發(fā)現(xiàn)連個(gè)beanClass的類型名稱和類型都是匹配的,直接將原來(lái)的值賦值給demo2的list,但是程序運(yùn)行的時(shí)候由于泛型定義,會(huì)嘗試自動(dòng)將demo2中l(wèi)ist中的元素當(dāng)成Integer類型處理,所以就出現(xiàn)了類型轉(zhuǎn)換異常。
把上面的代碼稍微做下調(diào)整:
for (Object obj : demo2.getList()) { System.out.println(obj); }
運(yùn)行結(jié)果正常打印,因?yàn)閐emo2的list實(shí)際存儲(chǔ)的是String,這里把String當(dāng)成Object處理完全沒(méi)有問(wèn)題。
總結(jié)
通過(guò)本篇的描述我們對(duì)常見(jiàn)的數(shù)據(jù)實(shí)體轉(zhuǎn)換方式的使用和原來(lái)有了大致的了解,雖然看起來(lái)實(shí)現(xiàn)并不復(fù)雜,但是整個(gè)流程下來(lái)里邊涉及了很多java體系典型的知識(shí),有反射、引用類型、類加載、內(nèi)省、緩存安全和緩存等眾多內(nèi)容,從一個(gè)簡(jiǎn)單的對(duì)象屬性拷貝就能看出spring源碼編寫人員對(duì)于java深刻的理解和深厚的功底,當(dāng)然我們更直觀的看到的是spring架構(gòu)設(shè)計(jì)的優(yōu)秀和源碼編寫的優(yōu)雅,希望通過(guò)本篇文章能夠加深對(duì)spring框架對(duì)象賦值工具類使用方式和實(shí)現(xiàn)原理的理解,以及如何避免由于使用不當(dāng)容易踩到的坑。
到此這篇關(guān)于Spring深入分析講解BeanUtils的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Spring BeanUtils內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot中登錄后關(guān)于cookie和session攔截問(wèn)題的案例分析
這篇文章主要介紹了Springboot中登錄后關(guān)于cookie和session攔截案例,本文通過(guò)實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08Java BufferedWriter BufferedReader 源碼分析
本文是關(guān)于Java BufferedWriter ,BufferedReader 簡(jiǎn)介、分析源碼 對(duì)Java IO 流深入了解,希望看到的同學(xué)對(duì)你有所幫助2016-07-07解決Tomcat啟動(dòng)報(bào)異常java.lang.ClassNotFoundException問(wèn)題
這篇文章主要介紹了解決Tomcat啟動(dòng)報(bào)異常java.lang.ClassNotFoundException問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01SpringBoot定時(shí)任務(wù)的實(shí)現(xiàn)詳解
這篇文章主要介紹了SpringBoot定時(shí)任務(wù)的實(shí)現(xiàn)詳解,定時(shí)任務(wù)是企業(yè)級(jí)開發(fā)中最常見(jiàn)的功能之一,如定時(shí)統(tǒng)計(jì)訂單數(shù)、數(shù)據(jù)庫(kù)備份、定時(shí)發(fā)送短信和郵件、定時(shí)統(tǒng)計(jì)博客訪客等,簡(jiǎn)單的定時(shí)任務(wù)可以直接通過(guò)Spring中的@Scheduled注解來(lái)實(shí)現(xiàn),需要的朋友可以參考下2024-01-01詳解Spring中Bean的生命周期和作用域及實(shí)現(xiàn)方式
這篇文章主要給大家介紹了Spring中Bean的生命周期和作用域及實(shí)現(xiàn)方式的相關(guān)資料,文中介紹的非常詳細(xì),對(duì)大家具有一定的參考價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-03-03