Spring深入分析講解BeanUtils的實現(xiàn)
背景
DO
DO是Data Object的簡寫,叫做數(shù)據(jù)實體,既然是數(shù)據(jù)實體,那么也就是和存儲層打交道的實體類,應(yīng)用從存儲層拿到的數(shù)據(jù)是以行為單位的數(shù)據(jù),不具備java特性,那么如果要和java屬性結(jié)合起來或者說在業(yè)務(wù)中流轉(zhuǎn),那么一定要轉(zhuǎn)換成java對象(反過來java要和持久層打交道也要把java對象轉(zhuǎn)換成行數(shù)據(jù)),那么就需要DO作為行數(shù)據(jù)的一個載體,把行的每一個列屬性映射到j(luò)ava對象的每一個字段。
BO
BO是Business Object的簡寫,是業(yè)務(wù)對象,區(qū)別于DO的純數(shù)據(jù)描述,BO用于在應(yīng)用各個模塊之間流轉(zhuǎn),具備一定的業(yè)務(wù)含義,一般情況像BO是應(yīng)用自己定義的業(yè)務(wù)實體,對持久層和二方或三方接口接口響應(yīng)結(jié)果的封裝,這里插一句,為什么有了DO和外部依賴的實體類,為什么還需要BO?對于領(lǐng)域內(nèi)持久層交互來說,BO層有時候可以省略(大部分場景字段屬性基本一致),而對于和領(lǐng)域外二方或三方服務(wù)交互來說,增加BO實體的目的主要是降低外部實體對領(lǐng)域內(nèi)其它層的侵入,以及降低外部實體簽名變更對領(lǐng)域內(nèi)其它層的影響,舉個例子將調(diào)用訂單服務(wù)的響應(yīng)結(jié)果在代理層封裝成BO供上層使用,那么如果訂單實體內(nèi)部屬性簽名發(fā)生變更或者升級,那么只需要改BO即可,只影響應(yīng)用的代理層,中間業(yè)務(wù)流轉(zhuǎn)層完全不受影響。
DTO
DTO是Data Transfer Object的縮寫,叫做數(shù)據(jù)傳輸對象,主要用于跨服務(wù)之間的數(shù)據(jù)傳輸,如公司內(nèi)部做了微服務(wù)拆封,那么微服務(wù)之間的數(shù)據(jù)交互就是以DTO作為數(shù)據(jù)結(jié)果響應(yīng)載體,另外DTO的存在也是對外部依賴屏蔽了領(lǐng)域內(nèi)底層數(shù)據(jù)的結(jié)構(gòu),假如直接返回DO給依賴方,那么我們的表結(jié)構(gòu)也就一覽無余了,在公司內(nèi)部還好,對于也利益關(guān)系的團隊之間有服務(wù)交互采取這種方式,那么就可能產(chǎn)生安全問題和不必要的糾紛。
VO
值對象(Value Object),其存在的意思主要是數(shù)據(jù)展示,其直接包含具有業(yè)務(wù)含義的數(shù)據(jù),和前端打交道,由業(yè)務(wù)層將DO或者BO轉(zhuǎn)換為VO供前端使用。
前邊介紹了幾種常用的數(shù)據(jù)實體,那么一個關(guān)鍵的問題就出現(xiàn)了,既然應(yīng)用分了那么多層,每個層使用的數(shù)據(jù)實體可能不一樣,也必然會存在實體之間的轉(zhuǎn)換問題,也是本篇文章需要重點講述的問題。
數(shù)據(jù)實體轉(zhuǎn)換
所謂數(shù)據(jù)實體轉(zhuǎn)換,就是將源數(shù)據(jù)實體存儲的數(shù)據(jù)轉(zhuǎn)換到目標實體的實例對象存儲,比如把BO轉(zhuǎn)換成VO數(shù)據(jù)響應(yīng)給前端,那么就需要將源數(shù)據(jù)實體的屬性值逐個映射到目標數(shù)據(jù)實體并賦值,也就是VO.setXxx(BO.getXxx()),當然我們可以選擇最原始最笨重的方式,逐個遍歷源數(shù)據(jù)實體的屬性然后賦值給新數(shù)據(jù)實體,也可以利用java的反射來實現(xiàn)。
就目前比較可行的以及可行的方案中,比較常用的有逐個set,和利用工具類賦值。
在數(shù)據(jù)實體字段比較少或者字段類型比較復(fù)雜的情況下,可以考慮使用逐個字段賦值的方式,但是如果字段相對較多,那么就會出現(xiàn)一個實體類轉(zhuǎn)換就寫了幾十行甚至上百行的代碼,這是完全不能接受的,那么我們就需要自己實現(xiàn)反射或者使用線程的工具類來實現(xiàn)了,當然工具類有很多,比如apache的common包有BeanUtils實現(xiàn),spring-beans有BeanUtils實現(xiàn)以及Guava也有相關(guān)實現(xiàn),其他的暫且不論,這里我們就從源碼維度分析一下使用spring-beans的BeanUtils做數(shù)據(jù)實體轉(zhuǎn)換的實現(xiàn)原理和可能會存在的坑。
使用方式
在數(shù)據(jù)實體轉(zhuǎn)換時,用的最多的就是BeanUtils#copyProperties方法,基本用法就是:
//DO是源數(shù)據(jù)對象,DTO是目標對象,把源類的數(shù)據(jù)拷貝到目標對象 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ù)制到目標bean中,源類和目標類不必匹配,甚至不必派生
彼此,只要屬性匹配即可,源bean中有但目標bean中沒有的屬性將被忽略。
上述方法直接調(diào)用了重載方法,多了兩個入?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");
//目標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.如果源屬性描述的讀防范存在且返回數(shù)據(jù)類型和目標屬性的寫方法入?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);
//如果目標屬性的寫方法修飾符不是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);
}
}
}
}
}
}方法的具體實現(xiàn)中增加了詳細的注釋,基本上能夠看出來其實現(xiàn)原理是通過反射,但是里邊有兩個地方我們需要關(guān)注一下:
//獲取目標bean屬性描述 PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable); //獲取源bean指定名稱的屬性描述 PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
其實兩個調(diào)用底層實現(xiàn)一樣,那么我們就對其中一個做一下分析即可,繼續(xù)跟進看getPropertyDescriptors(actualEditable)實現(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方法,從名稱中可以知道改方法返回一個緩存的自省結(jié)果,然后返回結(jié)果中的屬性描述,繼續(xù)看實現(xiàn):
@SuppressWarnings("unchecked")
static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
//1.從強緩存獲取beanClass的內(nèi)省結(jié)果,如果有數(shù)據(jù)直接返回
CachedIntrospectionResults results = strongClassCache.get(beanClass);
if (results != null) {
return results;
}
//2.如果強緩存中不存在beanClass的內(nèi)省結(jié)果,則從軟緩存中獲取beanClass的內(nèi)省結(jié)果,如果存在直接返回
results = softClassCache.get(beanClass);
if (results != null) {
return results;
}
//3.如果強緩存和軟緩存都不存在beanClass的自省結(jié)果,則創(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內(nèi)省結(jié)果放入緩存
CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results);
//7.返回內(nèi)省結(jié)果
return (existing != null ? existing : results);
}該方法中有幾個比較重要的概念,強引用、軟引用、緩存、緩存安全、類加載和內(nèi)省等,簡單介紹一下概念:
- 強引用: 常見的用new方式創(chuàng)建的引用,只要有引用存在,就算出現(xiàn)OOM也不會回收這部分內(nèi)存空間
- 軟引用: 引用強度低于強引用,在出現(xiàn)OOM之前垃圾回收器會嘗試回收這部分存儲空間,如果仍不夠用則報OOM
- 緩存安全:檢查beanClass是否是CachedIntrospectionResults的類加載器或者其父類加載器加載的
- 類加載:雙親委派
- 內(nèi)省:是java提供的一種獲取對bean的屬性、事件描述的方式
方法的作用是先嘗試從強引用緩存中獲取beanClass的自省結(jié)果,如果存在則直接返回,如果不存在則嘗試從軟引用緩存中獲取自省結(jié)果,如果存在直接返回,否則利用java自省特性生成beanClass屬性描述,如果緩存安全或者beanClass的類加載器是可接受的,將結(jié)果放入強引用緩存,否則放入軟引用緩存,最后返回結(jié)果。
屬性賦值類型擦除
我們在正常使用BeanUtils的copyProperties是沒有問題的,但是在有些場景下會出現(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,看上去沒什么問題,并且代碼也是編譯通過的,但是運行后發(fā)現(xiàn):

類型轉(zhuǎn)換失敗,為什么?這里提一下泛型擦除的概念,說白了就是所有的泛型類型(除extends和super)編譯后都換變成Object類型,也就是說上邊的例子中代碼編譯后兩個類的list屬性的類型都會變成List<Object>,主要是兼容1.5之前的無泛型類型,那么在使用BeanUtils工具類進行復(fù)制的時候發(fā)現(xiàn)連個beanClass的類型名稱和類型都是匹配的,直接將原來的值賦值給demo2的list,但是程序運行的時候由于泛型定義,會嘗試自動將demo2中l(wèi)ist中的元素當成Integer類型處理,所以就出現(xiàn)了類型轉(zhuǎn)換異常。
把上面的代碼稍微做下調(diào)整:
for (Object obj : demo2.getList()) {
System.out.println(obj);
}
運行結(jié)果正常打印,因為demo2的list實際存儲的是String,這里把String當成Object處理完全沒有問題。
總結(jié)
通過本篇的描述我們對常見的數(shù)據(jù)實體轉(zhuǎn)換方式的使用和原來有了大致的了解,雖然看起來實現(xiàn)并不復(fù)雜,但是整個流程下來里邊涉及了很多java體系典型的知識,有反射、引用類型、類加載、內(nèi)省、緩存安全和緩存等眾多內(nèi)容,從一個簡單的對象屬性拷貝就能看出spring源碼編寫人員對于java深刻的理解和深厚的功底,當然我們更直觀的看到的是spring架構(gòu)設(shè)計的優(yōu)秀和源碼編寫的優(yōu)雅,希望通過本篇文章能夠加深對spring框架對象賦值工具類使用方式和實現(xiàn)原理的理解,以及如何避免由于使用不當容易踩到的坑。
到此這篇關(guān)于Spring深入分析講解BeanUtils的實現(xiàn)的文章就介紹到這了,更多相關(guān)Spring BeanUtils內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot中登錄后關(guān)于cookie和session攔截問題的案例分析
這篇文章主要介紹了Springboot中登錄后關(guān)于cookie和session攔截案例,本文通過實例圖文相結(jié)合給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08
Java BufferedWriter BufferedReader 源碼分析
本文是關(guān)于Java BufferedWriter ,BufferedReader 簡介、分析源碼 對Java IO 流深入了解,希望看到的同學對你有所幫助2016-07-07
解決Tomcat啟動報異常java.lang.ClassNotFoundException問題
這篇文章主要介紹了解決Tomcat啟動報異常java.lang.ClassNotFoundException問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01
詳解Spring中Bean的生命周期和作用域及實現(xiàn)方式
這篇文章主要給大家介紹了Spring中Bean的生命周期和作用域及實現(xiàn)方式的相關(guān)資料,文中介紹的非常詳細,對大家具有一定的參考價值,需要的朋友們下面來一起看看吧。2017-03-03

