首頁 > 軟體

Spring深入分析講解BeanUtils的實現

2022-06-21 14:10:03

背景

DO

DO是Data Object的簡寫,叫做資料實體,既然是資料實體,那麼也就是和儲存層打交道的實體類,應用從儲存層拿到的資料是以行為單位的資料,不具備java特性,那麼如果要和java屬性結合起來或者說在業務中流轉,那麼一定要轉換成java物件(反過來java要和持久層打交道也要把java物件轉換成行資料),那麼就需要DO作為行資料的一個載體,把行的每一個列屬性對映到java物件的每一個欄位。

BO

BO是Business Object的簡寫,是業務物件,區別於DO的純資料描述,BO用於在應用各個模組之間流轉,具備一定的業務含義,一般情況像BO是應用自己定義的業務實體,對持久層和二方或三方介面介面響應結果的封裝,這裡插一句,為什麼有了DO和外部依賴的實體類,為什麼還需要BO?對於領域內持久層互動來說,BO層有時候可以省略(大部分場景欄位屬性基本一致),而對於和領域外二方或三方服務互動來說,增加BO實體的目的主要是降低外部實體對領域內其它層的侵入,以及降低外部實體簽名變更對領域內其它層的影響,舉個例子將呼叫訂單服務的響應結果在代理層封裝成BO供上層使用,那麼如果訂單實體內部屬性簽名發生變更或者升級,那麼只需要改BO即可,隻影響應用的代理層,中間業務流轉層完全不受影響。

DTO

DTO是Data Transfer Object的縮寫,叫做資料傳輸物件,主要用於跨服務之間的資料傳輸,如公司內部做了微服務拆封,那麼微服務之間的資料互動就是以DTO作為資料結果響應載體,另外DTO的存在也是對外部依賴遮蔽了領域內底層資料的結構,假如直接返回DO給依賴方,那麼我們的表結構也就一覽無餘了,在公司內部還好,對於也利益關係的團隊之間有服務互動採取這種方式,那麼就可能產生安全問題和不必要的糾紛。

VO

值物件(Value Object),其存在的意思主要是資料展示,其直接包含具有業務含義的資料,和前端打交道,由業務層將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.如果存在寫方法,並且該屬性不忽略,繼續往下走,否則跳過繼續遍歷
    if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
            //4.獲取源Class的與目標屬性同名的屬性描述
      PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
      //5.如果源屬性描述不存在直接跳過,否則繼續往下走
            if (sourcePd != null) {
                //獲取源屬性描述的讀方法
        Method readMethod = sourcePd.getReadMethod();
                //6.如果源屬性描述的讀防範存在且返回資料型別和目標屬性的寫方法入參型別相同或者派生
                //繼續往下走,否則直接跳過繼續下次遍歷
        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());

其實兩個呼叫底層實現一樣,那麼我們就對其中一個做一下分析即可,繼續跟進看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方法,從名稱中可以知道改方法返回一個快取的自省結果,然後返回結果中的屬性描述,繼續看實現:

@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的自省結果,則建立一個
  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方式建立的參照,只要有參照存在,就算出現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,看上去沒什麼問題,並且程式碼也是編譯通過的,但是執行後發現:

型別轉換失敗,為什麼?這裡提一下泛型擦除的概念,說白了就是所有的泛型型別(除extends和super)編譯後都換變成Object型別,也就是說上邊的例子中程式碼編譯後兩個類的list屬性的型別都會變成List<Object>,主要是相容1.5之前的無泛型型別,那麼在使用BeanUtils工具類進行復制的時候發現連個beanClass的型別名稱和型別都是匹配的,直接將原來的值賦值給demo2的list,但是程式執行的時候由於泛型定義,會嘗試自動將demo2中list中的元素當成Integer型別處理,所以就出現了型別轉換異常。

把上面的程式碼稍微做下調整:

for (Object obj : demo2.getList()) {
    System.out.println(obj);
}

執行結果正常列印,因為demo2的list實際儲存的是String,這裡把String當成Object處理完全沒有問題。

總結

通過本篇的描述我們對常見的資料實體轉換方式的使用和原來有了大致的瞭解,雖然看起來實現並不複雜,但是整個流程下來裡邊涉及了很多java體系典型的知識,有反射、參照型別、類載入、內省、快取安全和快取等眾多內容,從一個簡單的物件屬性拷貝就能看出spring原始碼編寫人員對於java深刻的理解和深厚的功底,當然我們更直觀的看到的是spring架構設計的優秀和原始碼編寫的優雅,希望通過本篇文章能夠加深對spring框架物件賦值工具類使用方式和實現原理的理解,以及如何避免由於使用不當容易踩到的坑。

到此這篇關於Spring深入分析講解BeanUtils的實現的文章就介紹到這了,更多相關Spring BeanUtils內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com