最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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)景先做一下簡單描述和分析

背景

DO

DO是Data Object的簡寫,叫做數(shù)據(jù)實(shí)體,既然是數(shù)據(jù)實(shí)體,那么也就是和存儲(chǔ)層打交道的實(shí)體類,應(yīng)用從存儲(chǔ)層拿到的數(shù)據(jù)是以行為單位的數(shù)據(jù),不具備java特性,那么如果要和java屬性結(jié)合起來或者說在業(yè)務(wù)中流轉(zhuǎn),那么一定要轉(zhuǎn)換成java對(duì)象(反過來java要和持久層打交道也要把java對(duì)象轉(zhuǎn)換成行數(shù)據(jù)),那么就需要DO作為行數(shù)據(jù)的一個(gè)載體,把行的每一個(gè)列屬性映射到j(luò)ava對(duì)象的每一個(gè)字段。

BO

BO是Business Object的簡寫,是業(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)持久層交互來說,BO層有時(shí)候可以省略(大部分場(chǎng)景字段屬性基本一致),而對(duì)于和領(lǐng)域外二方或三方服務(wù)交互來說,增加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)也就一覽無余了,在公司內(nèi)部還好,對(duì)于也利益關(guān)系的團(tuán)隊(duì)之間有服務(wù)交互采取這種方式,那么就可能產(chǎn)生安全問題和不必要的糾紛。

VO

值對(duì)象(Value Object),其存在的意思主要是數(shù)據(jù)展示,其直接包含具有業(yè)務(wù)含義的數(shù)據(jù),和前端打交道,由業(yè)務(wù)層將DO或者BO轉(zhuǎn)換為VO供前端使用。

前邊介紹了幾種常用的數(shù)據(jù)實(shí)體,那么一個(gè)關(guān)鍵的問題就出現(xiàn)了,既然應(yīng)用分了那么多層,每個(gè)層使用的數(shù)據(jù)實(shí)體可能不一樣,也必然會(huì)存在實(shí)體之間的轉(zhuǎn)換問題,也是本篇文章需要重點(diǎ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的反射來實(shí)現(xiàn)。

就目前比較可行的以及可行的方案中,比較常用的有逐個(gè)set,和利用工具類賦值。

在數(shù)據(jù)實(shí)體字段比較少或者字段類型比較復(fù)雜的情況下,可以考慮使用逐個(gè)字段賦值的方式,但是如果字段相對(duì)較多,那么就會(huì)出現(xiàn)一個(gè)實(shí)體類轉(zhuǎn)換就寫了幾十行甚至上百行的代碼,這是完全不能接受的,那么我們就需要自己實(shí)現(xiàn)反射或者使用線程的工具類來實(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中沒有的屬性將被忽略。

上述方法直接調(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ù)往下走,否則跳過繼續(xù)遍歷
    if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
            //4.獲取源Class的與目標(biāo)屬性同名的屬性描述
      PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
      //5.如果源屬性描述不存在直接跳過,否則繼續(xù)往下走
            if (sourcePd != null) {
                //獲取源屬性描述的讀方法
        Method readMethod = sourcePd.getReadMethod();
                //6.如果源屬性描述的讀防范存在且返回?cái)?shù)據(jù)類型和目標(biāo)屬性的寫方法入?yún)㈩愋拖嗤蛘吲缮?
                //繼續(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);
                        //如果目標(biāo)屬性的寫方法修飾符不是public,則修改為可訪問
            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
              writeMethod.setAccessible(true);
            }
                        //8.通過反射將源屬性值賦值給目標(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ì)的注釋,基本上能夠看出來其實(shí)現(xiàn)原理是通過反射,但是里邊有兩個(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)省等,簡單介紹一下概念:

  • 強(qiáng)引用: 常見的用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是沒有問題的,但是在有些場(chǎng)景下會(huì)出現(xià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);
    }
}

很簡單,就是利用BeanUtils將demo1的屬性值復(fù)制到demo2,看上去沒什么問題,并且代碼也是編譯通過的,但是運(yùn)行后發(fā)現(xiàn):

類型轉(zhuǎn)換失敗,為什么?這里提一下泛型擦除的概念,說白了就是所有的泛型類型(除extends和super)編譯后都換變成Object類型,也就是說上邊的例子中代碼編譯后兩個(gè)類的list屬性的類型都會(huì)變成List<Object>,主要是兼容1.5之前的無泛型類型,那么在使用BeanUtils工具類進(jìn)行復(fù)制的時(shí)候發(fā)現(xiàn)連個(gè)beanClass的類型名稱和類型都是匹配的,直接將原來的值賦值給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處理完全沒有問題。

總結(jié)

通過本篇的描述我們對(duì)常見的數(shù)據(jù)實(shí)體轉(zhuǎn)換方式的使用和原來有了大致的了解,雖然看起來實(shí)現(xiàn)并不復(fù)雜,但是整個(gè)流程下來里邊涉及了很多java體系典型的知識(shí),有反射、引用類型、類加載、內(nèi)省、緩存安全和緩存等眾多內(nèi)容,從一個(gè)簡單的對(duì)象屬性拷貝就能看出spring源碼編寫人員對(duì)于java深刻的理解和深厚的功底,當(dāng)然我們更直觀的看到的是spring架構(gòu)設(shè)計(jì)的優(yōu)秀和源碼編寫的優(yōu)雅,希望通過本篇文章能夠加深對(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)文章

  • Java中的模板模式說明與實(shí)現(xiàn)

    Java中的模板模式說明與實(shí)現(xiàn)

    這篇文章主要介紹了Java中的模板模式說明與實(shí)現(xiàn),模板方法模式,又叫模板模式,在一個(gè)抽象類公開定義了執(zhí)行它的方法的模板,它的子類可以更需要重寫方法實(shí)現(xiàn),但可以成為典型類中定義的方式進(jìn)行,需要的朋友可以參考下
    2023-10-10
  • 解讀Java中char類型相加的問題

    解讀Java中char類型相加的問題

    這篇文章主要介紹了解讀Java中char類型相加的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-12-12
  • HTTPClient如何在Springboot中封裝工具類

    HTTPClient如何在Springboot中封裝工具類

    這篇文章主要介紹了HTTPClient如何在Springboot中封裝工具類問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-09-09
  • springboot swagger2注解使用的教程

    springboot swagger2注解使用的教程

    這篇文章主要介紹了springboot swagger2注解使用,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-05-05
  • idea新建Springboot項(xiàng)目,設(shè)置默認(rèn)maven和jdk版本方式

    idea新建Springboot項(xiàng)目,設(shè)置默認(rèn)maven和jdk版本方式

    這篇文章主要介紹了idea新建Springboot項(xiàng)目,設(shè)置默認(rèn)maven和jdk版本方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-12-12
  • Jackson忽略字段實(shí)現(xiàn)對(duì)字段進(jìn)行序列化和反序列化

    Jackson忽略字段實(shí)現(xiàn)對(duì)字段進(jìn)行序列化和反序列化

    在使用?Jackson?進(jìn)行序列化和反序列化時(shí),有時(shí)候需要對(duì)某些字段進(jìn)行過濾,以便在?JSON?數(shù)據(jù)中不包含某些敏感信息,下面就一起來了解一下Jackson忽略字段實(shí)現(xiàn)對(duì)字段進(jìn)行序列化和反序
    2023-10-10
  • HashMap vs TreeMap vs Hashtable vs LinkedHashMap

    HashMap vs TreeMap vs Hashtable vs LinkedHashMap

    這篇文章主要介紹了HashMap vs TreeMap vs Hashtable vs LinkedHashMap的相關(guān)知識(shí),非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2019-07-07
  • Mybatis?大數(shù)據(jù)量批量寫優(yōu)化的案例詳解

    Mybatis?大數(shù)據(jù)量批量寫優(yōu)化的案例詳解

    這篇文章主要介紹了Mybatis?大數(shù)據(jù)量批量寫優(yōu)化的示例代碼,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-05-05
  • SpringBoot整合ShedLock解決定時(shí)任務(wù)防止重復(fù)執(zhí)行的問題

    SpringBoot整合ShedLock解決定時(shí)任務(wù)防止重復(fù)執(zhí)行的問題

    ShedLock是一個(gè)用于分布式系統(tǒng)中防止定時(shí)任務(wù)重復(fù)執(zhí)行的庫,本文主要介紹了SpringBoot整合ShedLock解決定時(shí)任務(wù)防止重復(fù)執(zhí)行的問題,具有一定的參考價(jià)值,感興趣的可以了解一下
    2025-01-01
  • Springboot項(xiàng)目編寫測(cè)試單元完整步驟記錄

    Springboot項(xiàng)目編寫測(cè)試單元完整步驟記錄

    這篇文章主要介紹了如何使用JUnit編寫Spring?Boot項(xiàng)目中的測(cè)試單元,包括引入依賴、配置文件設(shè)置、啟動(dòng)文件創(chuàng)建以及編寫測(cè)試類的步驟,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2025-03-03

最新評(píng)論

修文县| 宾川县| 清远市| 安达市| 新宾| 灵山县| 孝昌县| 洪洞县| 连江县| 陵水| 滦南县| 黄梅县| 北安市| 进贤县| 四子王旗| 尼木县| 平泉县| 牙克石市| 天镇县| 安丘市| 紫阳县| 鹿泉市| 平乐县| 湖南省| 永新县| 三亚市| 交城县| 定兴县| 甘谷县| 湖北省| 荆州市| 忻州市| 大港区| 吉首市| 津市市| 张掖市| 侯马市| 北海市| 西藏| 吉首市| 县级市|