Spring深入分析講解BeanUtils的實現
背景
DO
DO是Data Object的簡寫,叫做數據實體,既然是數據實體,那么也就是和存儲層打交道的實體類,應用從存儲層拿到的數據是以行為單位的數據,不具備java特性,那么如果要和java屬性結合起來或者說在業(yè)務中流轉,那么一定要轉換成java對象(反過來java要和持久層打交道也要把java對象轉換成行數據),那么就需要DO作為行數據的一個載體,把行的每一個列屬性映射到java對象的每一個字段。
BO
BO是Business Object的簡寫,是業(yè)務對象,區(qū)別于DO的純數據描述,BO用于在應用各個模塊之間流轉,具備一定的業(yè)務含義,一般情況像BO是應用自己定義的業(yè)務實體,對持久層和二方或三方接口接口響應結果的封裝,這里插一句,為什么有了DO和外部依賴的實體類,為什么還需要BO?對于領域內持久層交互來說,BO層有時候可以省略(大部分場景字段屬性基本一致),而對于和領域外二方或三方服務交互來說,增加BO實體的目的主要是降低外部實體對領域內其它層的侵入,以及降低外部實體簽名變更對領域內其它層的影響,舉個例子將調用訂單服務的響應結果在代理層封裝成BO供上層使用,那么如果訂單實體內部屬性簽名發(fā)生變更或者升級,那么只需要改BO即可,只影響應用的代理層,中間業(yè)務流轉層完全不受影響。
DTO
DTO是Data Transfer Object的縮寫,叫做數據傳輸對象,主要用于跨服務之間的數據傳輸,如公司內部做了微服務拆封,那么微服務之間的數據交互就是以DTO作為數據結果響應載體,另外DTO的存在也是對外部依賴屏蔽了領域內底層數據的結構,假如直接返回DO給依賴方,那么我們的表結構也就一覽無余了,在公司內部還好,對于也利益關系的團隊之間有服務交互采取這種方式,那么就可能產生安全問題和不必要的糾紛。
VO
值對象(Value Object),其存在的意思主要是數據展示,其直接包含具有業(yè)務含義的數據,和前端打交道,由業(yè)務層將DO或者BO轉換為VO供前端使用。
前邊介紹了幾種常用的數據實體,那么一個關鍵的問題就出現了,既然應用分了那么多層,每個層使用的數據實體可能不一樣,也必然會存在實體之間的轉換問題,也是本篇文章需要重點講述的問題。
數據實體轉換
所謂數據實體轉換,就是將源數據實體存儲的數據轉換到目標實體的實例對象存儲,比如把BO轉換成VO數據響應給前端,那么就需要將源數據實體的屬性值逐個映射到目標數據實體并賦值,也就是VO.setXxx(BO.getXxx()),當然我們可以選擇最原始最笨重的方式,逐個遍歷源數據實體的屬性然后賦值給新數據實體,也可以利用java的反射來實現。
就目前比較可行的以及可行的方案中,比較常用的有逐個set,和利用工具類賦值。
在數據實體字段比較少或者字段類型比較復雜的情況下,可以考慮使用逐個字段賦值的方式,但是如果字段相對較多,那么就會出現一個實體類轉換就寫了幾十行甚至上百行的代碼,這是完全不能接受的,那么我們就需要自己實現反射或者使用線程的工具類來實現了,當然工具類有很多,比如apache的common包有BeanUtils實現,spring-beans有BeanUtils實現以及Guava也有相關實現,其他的暫且不論,這里我們就從源碼維度分析一下使用spring-beans的BeanUtils做數據實體轉換的實現原理和可能會存在的坑。
使用方式
在數據實體轉換時,用的最多的就是BeanUtils#copyProperties方法,基本用法就是:
//DO是源數據對象,DTO是目標對象,把源類的數據拷貝到目標對象 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的屬性值復制到目標bean中,源類和目標類不必匹配,甚至不必派生
彼此,只要屬性匹配即可,源bean中有但目標bean中沒有的屬性將被忽略。
上述方法直接調用了重載方法,多了兩個入參:
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"); //目標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.獲取目標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ù)往下走,否則跳過繼續(xù)遍歷 if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) { //4.獲取源Class的與目標屬性同名的屬性描述 PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName()); //5.如果源屬性描述不存在直接跳過,否則繼續(xù)往下走 if (sourcePd != null) { //獲取源屬性描述的讀方法 Method readMethod = sourcePd.getReadMethod(); //6.如果源屬性描述的讀防范存在且返回數據類型和目標屬性的寫方法入參類型相同或者派生 //繼續(xù)往下走,否則直接跳過繼續(xù)下次遍歷 if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) { try { //如果源屬性讀方法修飾符不是public,那么修改為可訪問 if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) { readMethod.setAccessible(true); } //7.讀取源屬性的值 Object value = readMethod.invoke(source); //如果目標屬性的寫方法修飾符不是public,則修改為可訪問 if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) { writeMethod.setAccessible(true); } //8.通過反射將源屬性值賦值給目標屬性 writeMethod.invoke(target, value); } catch (Throwable ex) { throw new FatalBeanException( "Could not copy property '" + targetPd.getName() + "' from source to target", ex); } } } } } }
方法的具體實現中增加了詳細的注釋,基本上能夠看出來其實現原理是通過反射,但是里邊有兩個地方我們需要關注一下:
//獲取目標bean屬性描述 PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable); //獲取源bean指定名稱的屬性描述 PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
其實兩個調用底層實現一樣,那么我們就對其中一個做一下分析即可,繼續(xù)跟進看getPropertyDescriptors(actualEditable)實現:
/** * 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的屬性描述,調用了CachedIntrospectionResults的forClass方法,從名稱中可以知道改方法返回一個緩存的自省結果,然后返回結果中的屬性描述,繼續(xù)看實現:
@SuppressWarnings("unchecked") static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException { //1.從強緩存獲取beanClass的內省結果,如果有數據直接返回 CachedIntrospectionResults results = strongClassCache.get(beanClass); if (results != null) { return results; } //2.如果強緩存中不存在beanClass的內省結果,則從軟緩存中獲取beanClass的內省結果,如果存在直接返回 results = softClassCache.get(beanClass); if (results != null) { return results; } //3.如果強緩存和軟緩存都不存在beanClass的自省結果,則創(chuàng)建一個 results = new CachedIntrospectionResults(beanClass); ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse; //4.如果beanClass是緩存安全的,或者beanClass的類加載器是配置可接受的,緩存引用指向強緩存 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內省結果放入緩存 CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results); //7.返回內省結果 return (existing != null ? existing : results); }
該方法中有幾個比較重要的概念,強引用、軟引用、緩存、緩存安全、類加載和內省等,簡單介紹一下概念:
- 強引用: 常見的用new方式創(chuàng)建的引用,只要有引用存在,就算出現OOM也不會回收這部分內存空間
- 軟引用: 引用強度低于強引用,在出現OOM之前垃圾回收器會嘗試回收這部分存儲空間,如果仍不夠用則報OOM
- 緩存安全:檢查beanClass是否是CachedIntrospectionResults的類加載器或者其父類加載器加載的
- 類加載:雙親委派
- 內省:是java提供的一種獲取對bean的屬性、事件描述的方式
方法的作用是先嘗試從強引用緩存中獲取beanClass的自省結果,如果存在則直接返回,如果不存在則嘗試從軟引用緩存中獲取自省結果,如果存在直接返回,否則利用java自省特性生成beanClass屬性描述,如果緩存安全或者beanClass的類加載器是可接受的,將結果放入強引用緩存,否則放入軟引用緩存,最后返回結果。
屬性賦值類型擦除
我們在正常使用BeanUtils的copyProperties是沒有問題的,但是在有些場景下會出現問題,我們看下面的代碼:
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); } }
很簡單,就是利用BeanUtils將demo1的屬性值復制到demo2,看上去沒什么問題,并且代碼也是編譯通過的,但是運行后發(fā)現:
類型轉換失敗,為什么?這里提一下泛型擦除的概念,說白了就是所有的泛型類型(除extends和super)編譯后都換變成Object類型,也就是說上邊的例子中代碼編譯后兩個類的list屬性的類型都會變成List<Object>,主要是兼容1.5之前的無泛型類型,那么在使用BeanUtils工具類進行復制的時候發(fā)現連個beanClass的類型名稱和類型都是匹配的,直接將原來的值賦值給demo2的list,但是程序運行的時候由于泛型定義,會嘗試自動將demo2中l(wèi)ist中的元素當成Integer類型處理,所以就出現了類型轉換異常。
把上面的代碼稍微做下調整:
for (Object obj : demo2.getList()) { System.out.println(obj); }
運行結果正常打印,因為demo2的list實際存儲的是String,這里把String當成Object處理完全沒有問題。
總結
通過本篇的描述我們對常見的數據實體轉換方式的使用和原來有了大致的了解,雖然看起來實現并不復雜,但是整個流程下來里邊涉及了很多java體系典型的知識,有反射、引用類型、類加載、內省、緩存安全和緩存等眾多內容,從一個簡單的對象屬性拷貝就能看出spring源碼編寫人員對于java深刻的理解和深厚的功底,當然我們更直觀的看到的是spring架構設計的優(yōu)秀和源碼編寫的優(yōu)雅,希望通過本篇文章能夠加深對spring框架對象賦值工具類使用方式和實現原理的理解,以及如何避免由于使用不當容易踩到的坑。
到此這篇關于Spring深入分析講解BeanUtils的實現的文章就介紹到這了,更多相關Spring BeanUtils內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Springboot中登錄后關于cookie和session攔截問題的案例分析
這篇文章主要介紹了Springboot中登錄后關于cookie和session攔截案例,本文通過實例圖文相結合給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08Java BufferedWriter BufferedReader 源碼分析
本文是關于Java BufferedWriter ,BufferedReader 簡介、分析源碼 對Java IO 流深入了解,希望看到的同學對你有所幫助2016-07-07解決Tomcat啟動報異常java.lang.ClassNotFoundException問題
這篇文章主要介紹了解決Tomcat啟動報異常java.lang.ClassNotFoundException問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01