亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Spring深入分析講解BeanUtils的實(shí)現(xiàn)

 更新時(shí)間:2022年06月21日 11:25:48   作者:叔牙  
java知識(shí)體系統(tǒng)有很多數(shù)據(jù)實(shí)體,比較常用的DTO、BO、DO、VO等,其他類似POJO概念太老了現(xiàn)在基本廢棄掉了,本篇幅直接忽略,對(duì)于這幾種數(shù)據(jù)實(shí)體各自代表的含義和應(yīng)用場(chǎng)景先做一下簡(jiǎ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攔截問(wèn)題的案例分析

    這篇文章主要介紹了Springboot中登錄后關(guān)于cookie和session攔截案例,本文通過(guò)實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-08-08
  • 淺析java中的取整(/)和求余(%)

    淺析java中的取整(/)和求余(%)

    這篇文章主要介紹了淺析java中的取整(/)和求余(%),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-08-08
  • Java BufferedWriter BufferedReader 源碼分析

    Java BufferedWriter BufferedReader 源碼分析

    本文是關(guān)于Java BufferedWriter ,BufferedReader 簡(jiǎn)介、分析源碼 對(duì)Java IO 流深入了解,希望看到的同學(xué)對(duì)你有所幫助
    2016-07-07
  • java中全排列的生成算法匯總

    java中全排列的生成算法匯總

    本文給大家匯總介紹了常見(jiàn)的6種全排列的生成算法,包括字典序法、遞增進(jìn)位數(shù)制法、遞減進(jìn)位數(shù)制法、鄰位交換法、遞歸類算法、元素增值法,有需要的小伙伴可以參考下
    2015-07-07
  • 解析Java按值傳遞還是按引用傳遞

    解析Java按值傳遞還是按引用傳遞

    這篇文章主要介紹了解析Java按值傳遞還是按引用傳遞,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-01-01
  • 解決Tomcat啟動(dòng)報(bào)異常java.lang.ClassNotFoundException問(wèn)題

    解決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-01
  • SpringBoot定時(shí)任務(wù)的實(shí)現(xiàn)詳解

    SpringBoot定時(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
  • java實(shí)現(xiàn)歸并排序算法

    java實(shí)現(xiàn)歸并排序算法

    在學(xué)習(xí)算法的過(guò)程中,我們難免會(huì)接觸很多和排序相關(guān)的算法。總而言之,對(duì)于任何編程人員來(lái)說(shuō),基本的排序算法是必須要掌握的。那么現(xiàn)在我們將要進(jìn)行基本的歸并排序算法的講解
    2016-01-01
  • 一文讀懂Spring Cloud-Hystrix

    一文讀懂Spring Cloud-Hystrix

    這篇文章主要介紹了通過(guò)一文讀懂Spring Cloud-Hystrix的相關(guān)知識(shí),本文分步驟通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-03-03
  • 詳解Spring中Bean的生命周期和作用域及實(shí)現(xiàn)方式

    詳解Spring中Bean的生命周期和作用域及實(shí)現(xiàn)方式

    這篇文章主要給大家介紹了Spring中Bean的生命周期和作用域及實(shí)現(xiàn)方式的相關(guān)資料,文中介紹的非常詳細(xì),對(duì)大家具有一定的參考價(jià)值,需要的朋友們下面來(lái)一起看看吧。
    2017-03-03

最新評(píng)論