楊春發,潘 鶴,李 鑫
(1.中國電建集團華東勘測設計研究院有限公司,浙江 杭州 311122;2.中電建華東勘測設計研究院(鄭州)有限公司)
“灃西新城”為區域內建設項目,對于其數據平臺的建設,需要融入CIM(City Information Modeling)理念,即建立CIM 基礎數據平臺。該平臺服務于城市規劃、建設和治理等多個場景,為城市挖掘數據價值,有效避免重復投資與建設助力。
平臺開發中,需要將不同二維數據對象的相同或相似屬性進行賦值。傳統的方法是通過手動編寫Getter/Setter 為屬性或字段賦值,當拷貝對象字段較多時,會導致大量代碼堆積,可讀性降低。為解決這一痛點,平臺引入拷貝工具類,分別為Apache和Spring的BeanUtils,以優化兩個任意多字段對象的屬性拷貝,提高開發效率,為相似二維數據對象復制繁瑣問題,提供解決方案。
JavaBeans 的靜態便捷方法,對Bean 進行實例化、檢查屬性類型、復制屬性等操作。應用比較廣泛的一個是Apache Commons 的BeanUtils,另一個是Spring Framework的BeanUtils。
JavaBean 作為工具類的作用對象,本質上是一種命名規則,具體如下。
⑴對于一個名稱為xxx的屬性,通常要寫兩個方法:getXxx()和setXxx()。任何瀏覽這些方法的工具,都會把set 或get 后的第一個字母轉為小寫,以此產生屬性名。get方法返回的類型需要與set方法里的參數相同,屬性名稱與get和set所依據的類型毫無關系。
⑵對于布爾類型屬性,使用以上的get 和set 方式,可把get替換成is。
⑶ Bean 中的普通方法不必遵循以上的命名規則,不過其訪問權限控制符必須是public的[1]。
工具類本質上是對象的拷貝,而對象拷貝又分為深拷貝和淺拷貝,BeanUtils 的拷貝方法就是淺拷貝,拷貝原理見圖1,定義如下:

圖1 拷貝原理圖
⑴淺拷貝:對基本類型采用值傳遞(傳遞該變量的副本)。對引用類型進行引用傳遞即復制指向源對象的地址,不復制對象本身,修改新對象會影響原對象。
⑵深拷貝:對基本類型采用值傳遞,對引用類型來說,深拷貝會復制一個完全與源對象一樣的對象,且不共享內存。
工具類的核心拷貝原理就是通過反射和自省實現的。
Java反射機制是在運行中,對任意一個類,能夠獲取得到這個類的所有屬性和方法,對于任意一個對象,都能夠調用類的任意一個方法,這種動態獲取類信息以及動態調用類對象方法的功能叫做Java語言的反射機制[2]。利用反射機制編寫與執行程序代碼時,使程序代碼能夠接入裝載到JVM中的類的內部信息[3]。
內省是操作Java 對象屬性的API,內省依賴于反射,利用BeanInfo來獲取屬性描述器,就可獲取某個屬性對應的Getter和Setter方法,最后通過反射機制來調用Getter和Setter方法[4]。
BeanUtils 本質是通過內省對JavaBean 的屬性進行淺拷貝,通過Class 引用獲取BeanInfo 信息,調用BeanInfo 的getPropertyDescriptors 方法,返回類型為PropertyDescriptor 的屬性描述器數組。針對每一個PropertyDescriptor,調用getPropertyType 方法得到類型,getName 方法得到屬性名,getReadMethod 方法得到讀方法,getWriteMethod 方法得到寫方法,后兩個方法返回Method 對象,在對象上調用相應方法(invoke),進行屬性的賦值。
實驗以平臺的三維可視化系統返回單條場景及相關信息為例進行測試,使用Spring-Beans5.1.10 和Commons-BeanUtils1.9.3 作為實驗版本,持久層方法對源對象屬性賦值后,依次用兩個工具類對源、目標對象的同名屬性值進行拷貝,打印場景聯合體對象的編號、名字、相關第一條圖層編號、相關第一條標繪編號,核心調用方法如下:

3.2.1 鏈式編程的支持性
⑴實驗結果及分析
圖2 是工具類對啟用鏈式編程的對象拷貝后的打印結果,使用Spring 的BeanUtils 能正常打印新聞信息,而使用Apache 的BeanUtils 拷貝為空,具體原因如下。

圖2 啟用鏈式編程打印結果
①Apache 中,使用默認的getWriteMethod()方法,通過查找set前綴的屬性名方法并判斷返回類型是否為void類型,如下:

②而SpringBean 中在獲取BeanInfo 對象的過程中,提供候選方法,候選可寫方法將返回類型不是void的類型納入可寫方法中,而鏈式編程的返回類型是本類,這就導致Apache的BeanUtils不支持鏈式編程。


③關閉鏈式編程后再次測試,兩個工具類會打印同樣的結果,如圖3所示。

圖3 關閉鏈式編程打印結果
⑵結論
Spring 的BeanUtils 支持鏈式編程,而Apache 的BeanUtils不支持。
3.2.2 安全性
⑴實驗結果及分析
圖4、圖5 是Spring BeanUtils、Apache BeanUtils對于null的Long 寫入long 類型的打印結果,前者提示非法參數異常,后者拷貝正常,原因如下:

圖4 SpringBeanUtils打印結果

圖5 ApacheBeanUtils打印結果
①Spring的BeanUtils的部分代碼:

②Apache的BeanUtils的部分代碼:


Apache 的工具類采用多種數據驗證方式來保障安全性,包括類型的轉換,甚至還會檢驗對象所屬類的可訪問性。
⑵結論Spring的BeanUtils校驗更少,而Apache的BeanUtils會進行多種數據驗證和類型轉換,是更安全的。
3.2.3 性能
⑴實驗結果及分析
針對兩種拷貝工具類一次進行100,10000,100000,1000000 的對象數量多次拷貝,具體平均消耗時間如圖6所示。

圖6 BeanUtils性能對比圖
在大數量對象拷貝時,SpringBeanUtils 的拷貝性能是ApacheBeanUtils 的近十倍,其主要原因在于Apache BeanUtils 對屬性的各類條件判斷,包括但不限于類型、可讀性等。Apache BeanUtils 還對類加載器進行單例限制,獲取實例時會加鎖,造成資源持有,影響性能。
⑵結論
Spring 的BeanUtils 內部邏輯簡單,效率比Apache的BeanUtils更高。
針對平臺二維數據的需求場景選擇不同的拷貝工具類,實現數據的轉換。將灃西新城CIM 基礎平臺構建為一個不僅是數據展示,更是數據管理的載體。對于平臺性能要求較高的場景,可采用Spring 的BeanUtils,如有更多的格式驗證、訪問控制的需求,可采用Apache的BeanUtils。使用過程中應注意以下幾點:
⑴涉及數值類型的封裝類像Integer、Short、Long、Double的數據轉換時,Spring BeanUtils需要手動初始化,否則提示數據轉換異常。而Apache BeanUtils 內置轉換器,無需擔心初始化問題。
⑵兩個拷貝工具類方法的傳參順序是相反的,SpringBeanUtils 的參數順序是(source,dest),而ApacheBeanUtils順序為(dest,source)。
⑶ Spring 的BeanUtils 可指定忽略字段,實現Bean的部分屬性拷貝。
⑷涉及Date 對象的數據轉換時,需注意Apache的BeanUtils 不支持until 包下的Date 類,應使用sql 下的Date類,避免數據異常。
此外,BeanUtils 并不是任何場景都適用的。BeanUtils 的不足之處在于其本質是淺拷貝,涉及對象只能是單一屬性和或者幾乎不改動的子對象。一但涉及深拷貝的場景,BeanUtils 是無法滿足的。另外,雖然使用BeanUtils 進行屬性拷貝十分方便,極大的減少了代碼冗長,但由于各種驗證、獲取及調用方法、序列化等對象操作,BeanUtils 的消耗時間實際上比手動調用Getter/Setter的時間長,因此要合理使用。