[TOC]java
咱們在軟件開發設計及開發過程當中,習慣將軟件橫向拆分爲幾個層。好比常見的三層架構:表現層(VIEW/UI)、業務邏輯層(SERVICE/BAL)、數據訪問層(DAO/DAL)。以下圖:spring
那應用系統爲何要分層呢?其實主要是解決如下幾個問題:數據庫
第一是解耦:apache
有一句計算機名言:軟件的全部問題均可以經過增長一層來解決。當系統越大,團隊越多,需求變化越快時,越須要保證程序之間的依賴關係越少。而分層/面向接口編程,會使咱們在應對變化時越容易。編程
第二是簡化問題:json
當咱們想不明白從用戶操做一直到數據落盤整個過程的交互狀況時,咱們應該換種方式思考。想一想各層應該提供哪些支持,經過對各層分工的明肯定義,複雜問題就變成了如何將各層功能組合起來的「積木搭建」。緩存
第三是下降系統維護與升級成本:安全
這裏體現了面向接口編程的優點。咱們抽象出數據訪問層後,只須要保證對外提供的接口不變,底層數據庫使用Oracle仍是MySql,上層結構是感知不到的。架構
第四是邏輯複用/代碼複用:app
經過分層,明肯定義各層職責,不再會出現系統中多個地方查詢同一個數據庫表的代碼。由於查詢某個數據庫表的工做只會由一個數據訪問層類來統一提供。
若是開發團隊不少,經過分層和接口定義。各團隊只須要遵循接口標準/開發規範,就能夠並行開發。有一個形容比較貼切:分層化至關於把軟件橫向切幾刀,模塊化至關於把軟件縱向切幾刀。
在《阿里巴巴Java開發手冊》中,對應用分層的建議是這樣的:
以上的層級只是在原來三層架構的基礎上進行了細分,而這些細分的層級僅僅是爲了知足業務的須要。千萬不要爲了分層而分層。
過多的層會增長系統的複雜度和開發難度。由於應用被細分爲多個層次,每一個層關注的點不一樣。因此在這基礎上,抽象出不一樣的領域模型。也就是咱們常見的DTO,DO等等。其本質的目的仍是爲了達到分層解耦的效果。
以上咱們簡單瞭解了分層的重要性,那麼隨着分層引入的典型領域模型都有哪些?咱們仍是來看看《阿里開發手冊》提供的分層領域模型規約參考:
各個領域模型在分層上的傳輸關係大概是這樣:
在給出的參考中並無對模型對象進行很是明確的劃分,特別是對BO、AO、DTO的界限不是很是明確。這也是由於系統處理的業務不一樣、複雜度不一樣致使的。因此在設計系統分層和建模的時候,須要綜合考慮實際應用場景。
數據在上傳下達的過程當中就會出現轉換的工做,可能有些小夥伴會以爲麻煩,爲何要弄出這麼多O?轉來轉去的多累!
在這裏我舉個例子,好比你查詢本身網上購物的訂單,可能會在網頁上看到這樣的信息:
其中包含:訂單編號,下單日期,店鋪名稱,用戶信息,總金額,支付方式,訂單狀態還有一個訂單商品明細的集合。
對終端顯示層來講,這些信息是能夠封裝成一個VO對象的。由於顯示層的關注點就是這些信息。爲了方便顯示層展現,咱們能夠將全部屬性都弄成字符串類型。以下示例,能夠看到,除了訂單id外,都是String類型:
public class OrderVO { /** * 訂單id */ Long orderId; /** * 下單日期 */ String orderDate; /** * 總金額 */ String totalMoney; /** * 支付方式 */ String paymentType; /** * 訂單狀態 */ String orderStatus; /** * 商鋪名稱 */ String shopName; /** * 用戶名稱 */ String userName; /** * 訂單商品明細集合 */ List<ProductVO> orderedProducts; }
再來看看對於業務邏輯層來講,它關心的是什麼呢?顯然跟顯示層關注的不同,它更加關注的是內部的邏輯關係。以下示例:
public class OrderVO { /** * 訂單id */ Long orderId; /** * 下單日期 */ Date orderDate; /** * 總金額 */ BigDecimal totalMoney; /** * 支付方式 */ PaymentType paymentType; /** * 訂單狀態 */ OrderStatus orderStatus; /** * 商鋪信息 */ ShopDTO shopInfo; /** * 用戶信息 */ UserDTO userInfo; /** * 訂單商品明細集合 */ List<ProductDTO> orderedProducts; }
從如上代碼能夠看到,下單日期使用的Date類型,金額使用BigDecimal,支付方式和訂單狀態使用枚舉值表示,商鋪名稱和用戶名稱變成了商鋪信息/用戶信息對象,明細集合中的商品也變成了DTO類型的對象。
在業務邏輯層面,更多的是關注由多種信息組合而成的關係。由於它在系統中起到信息傳遞的做用,因此它攜帶的信息也是最多的。
那咱們再來看看數據持久層,上面也提到了,數據持久層與數據庫是一一對應的關係,而上一層的訂單信息其實能夠拆解爲多個持久層對象,其中包含:訂單持久層對象(OrderDO),商鋪持久層對象(ShopDO),用戶持久層對象(UserDO)還有一堆的商品持久層對象(ProductDO)。相信經過描述你們也能夠理解具體的拆分方法了。
回過頭來想一想,若是咱們一路拿着最開始的OrderVO對象來操做,當咱們想要將它持久化時,會遇到多少坑就可想而知了。因此分層/拆分的本質仍是簡化咱們思考問題的方式,各層只關注本身感興趣的內容。
可這樣的拆分確實增長了許多工做量,不一樣模型之間轉來轉去的確實頭疼。那就讓咱們來梳理一下,在模型轉換時都須要注意哪些問題。在進行不一樣領域對象轉換時,有些問題是須要咱們考慮的。
例如,上面這兩個不一樣的模型在轉換時,咱們就須要考慮一些問題:
這麼多須要考慮的地方,我們要怎麼處理,才能優雅的進行模型轉換呢?
這裏我調研了大概有10種方法,有些使用起來比較複雜就沒有下大力氣去深刻研究,若是有感興趣的小夥伴,能夠自行深刻研究下。
作爲測試和講解的案例,我們就以上面說到的OrderDTO轉OrderVO爲例,來講說下面的各類方法。源對象OrderDTO大致結構是這樣的:
{ "orderDate":1570558718699, "orderId":201909090001, "orderStatus":"CREATED", "orderedProducts":[ { "price":799.990000000000009094947017729282379150390625, "productId":1, "productName":"吉他", "quantity":1 }, { "price":30, "productId":2, "productName":"變調夾", "quantity":1 } ], "paymentType":"CASH", "shopInfo":{ "shopId":20000101, "shopName":"樂韻商鋪" }, "totalMoney":829.990000000000009094947017729282379150390625, "userInfo":{ "userId":20100001, "userLevel":2147483647, "userName":"尼古拉斯趙四" } }
咱們期待轉換完的OrderVO對象是這樣的:
{ "orderDate":"2019-10-09 15:49:24.619", "orderStatus":"CREATED", "orderedProducts":[ { "productName":"吉他", "quantity":1 }, { "productName":"變調夾", "quantity":1 } ], "paymentType":"CASH", "shopName":"樂韻商鋪", "totalMoney":"829.99", "userName":"尼古拉斯趙四" }
先來看第一種方法:
也是最簡單粗暴的方法,直接經過Set/Get方式來進行人肉賦值。代碼我就不貼了,相信你們都會。
說一說它的優缺點:
優勢:直觀,簡單,執行速度快
缺點:屬性過多的時候,人容易崩潰,代碼顯得臃腫很差複用
第二種:FastJson:
利用序列化和反序列化,這裏咱們採用先使用FastJson的toJSONString的方法將原對象序列化爲字符串,再使用parseObject方法將字符串反序列化爲目標對象。
// JSON.toJSONString將對象序列化成字符串,JSON.parseObject將字符串反序列化爲OderVO對象 orderVO = JSON.parseObject(JSON.toJSONString(orderDTO), OrderVO.class);
轉換後的結果以下:
// 目標對象 { "orderDate":"1570558718699", "orderId":201909090001, "orderStatus":"CREATED", "orderedProducts":[ { "productName":"吉他", "quantity":1 }, { "productName":"變調夾", "quantity":1 } ], "paymentType":"CASH", "totalMoney":"829.990000000000009094947017729282379150390625" }
能夠看到轉換後的數據格式有幾個問題:
這就是第二種使用JSON處理,好像也不能知足咱們的要求
第三種,Apache工具包PropertyUtils工具類,代碼以下:
PropertyUtils.copyProperties(orderVO, orderDTO);
轉換代碼看着很簡單,可是轉換過程會報錯:
java.lang.IllegalArgumentException: Cannot invoke com.imooc.demo.OrderVO.setTotalMoney on bean class 'class com.imooc.demo.OrderVO' - argument type mismatch - had objects of type "java.math.BigDecimal" but expected signature "java.lang.String"
轉換結果:
// 目標對象 { "orderId":201909090001 }
缺點:
第四種,Apache工具包BeanUtils工具類,代碼以下:
BeanUtils.copyProperties(orderVO, orderDTO);
轉換後的結果是這樣:
// 目標對象 { "orderDate":"Wed Oct 09 02:36:25 CST 2019", "orderId":201909090001, "orderStatus":"CREATED", "orderedProducts":[ { "price":799.990000000000009094947017729282379150390625, "productId":1, "productName":"吉他", "quantity":1 }, { "price":30, "productId":2, "productName":"變調夾", "quantity":1 } ], "paymentType":"CASH", "totalMoney":"829.990000000000009094947017729282379150390625" }
缺點:
第五種,Spring封裝BeanUtils工具類,代碼以下:
// 對象屬性轉換,忽略orderedProducts字段 BeanUtils.copyProperties(orderDTO, orderVO, "orderedProducts");
在忽略了部分屬性後,轉換結果就只剩下:
// 目標對象 { "orderId":201909090001 }
apache的BeanUtils
和spring的BeanUtils
中拷貝方法的原理都是先用jdk中 java.beans.Introspector
類的getBeanInfo()
方法獲取對象的屬性信息及屬性get/set方法,接着使用反射(Method
的invoke(Object obj, Object... args)
)方法進行賦值。
前面五種都不能知足咱們的須要,其實想一想也挺簡單。對象轉換原本就很複雜,人工不介入很難作到完美轉換。
第六種,cglib工具包BeanCopier:
cglib的BeanCopier
採用了不一樣的方法:它不是利用反射對屬性進行賦值,而是直接使用ASM的MethodVisitor
直接編寫各屬性的get/set
方法生成class文件,而後進行執行。
使用方法以下,註釋寫的很清楚。咱們經過自定義的轉換器來處理Date轉String的操做:
// 構造轉換器對象,最後的參數表示是否須要自定義轉換器 BeanCopier beanCopier = BeanCopier.create(orderDTO.getClass(), orderVO.getClass(), true); // 轉換對象,自定義轉換器處理特殊字段 beanCopier.copy(orderDTO, orderVO, (value, target, context) -> { // 原始數據value是Date類型,目標類型target是String if (value instanceof Date) { if ("String".equals(target.getSimpleName())) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); return sdf.format(value); } } // 未匹配上的字段,原值返回 return value; });
轉換結果以下,對於咱們自定義處理的屬性能夠完美支持,其餘未處理的屬性就不行了:
// 目標對象 { "orderDate":"2019-10-09 03:07:13.768", "orderId":201909090001 }
優缺點:
第七種,Dozer框架:
注意,這已經不是一個工具類了,而是框架。使用以上類庫雖然能夠不用手動編寫get/set
方法,可是他們都不能對不一樣名稱的對象屬性進行映射。在定製化的屬性映射方面作得比較好的就是Dozer了。
Dozer支持簡單屬性映射、複雜類型映射、雙向映射、隱式映射以及遞歸映射。可以使用xml或者註解進行映射的配置,支持自動類型轉換,使用方便。但Dozer底層是使用reflect
包下Field
類的set(Object obj, Object value)
方法進行屬性賦值,執行速度上不是那麼理想。代碼示例:
// 建立轉換器對象,強烈建議建立全局惟一的,避免沒必要要的開銷 DozerBeanMapper mapper = new DozerBeanMapper(); // 加載映射文件 mapper.addMapping(TransferTest.class.getResourceAsStream("/mapping.xml")); // 轉換 orderVO = mapper.map(orderDTO, OrderVO.class);
使用方式很簡單,關鍵在於配置:
<mappings xmlns="http://dozer.sourceforge.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://dozer.sourceforge.net http://dozer.sourceforge.net/schema/beanmapping.xsd"> <!-- 一組類映射關係 --> <mapping> <!-- 類A和類B --> <class-a>com.imooc.demo.OrderDTO</class-a> <class-b>com.imooc.demo.OrderVO</class-b> <!-- 一組須要映射的特殊屬性 --> <field> <a>shopInfo.shopName</a> <b>shopName</b> </field> <!-- 將嵌套對象中的某個屬性值映射到目標對象的指定屬性上 --> <field> <a>userInfo.userName</a> <b>userName</b> </field> <!-- 將Date對象映射成指定格式的日期字符串 --> <field> <a>orderDate</a> <b date-format="yyyy-MM-dd HH:mm:ss.SSS">orderDate</b> </field> <!-- 自定義屬性轉化器 --> <field custom-converter="com.imooc.demo.DozerCustomConverter"> <a>totalMoney</a> <b>totalMoney</b> </field> <!-- 忽略指定屬性 --> <field-exclude> <a>orderId</a> <b>orderId</b> </field-exclude> </mapping> </mappings>
在配置文件中對特殊屬性進行了特殊定義,轉換結果符合咱們的要求:
// 目標對象 { "orderDate":"2019-10-09 15:49:24.619", "orderStatus":"CREATED", "orderedProducts":[ { "productName":"吉他", "quantity":1 }, { "productName":"變調夾", "quantity":1 } ], "paymentType":"CASH", "shopName":"樂韻商鋪", "totalMoney":"829.99", "userName":"尼古拉斯趙四" }
Dozer支持自定義轉換器,以下示例:
public class DozerCustomConverter implements CustomConverter { @Override public Object convert(Object destination, Object source, Class<?> destClass, Class<?> sourceClass) { // 若是原始屬性爲BigDecimal類型 if (source instanceof BigDecimal) { // 目標屬性爲String類型 if ("String".equals(destClass.getSimpleName())) { return String.valueOf(((BigDecimal) source).doubleValue()); } } return destination; } }
它的特色以下:
第八種,MapStruct框架:
基於JSR269的Java註解處理器,經過註解配置映射關係,在編譯時自動生成接口實現類。相似於Lombok的原理同樣,因此在執行速度上和Setter、Getter差很少。我目前我的使用較多的是MapStruct和BeanCopier,後期有空會單獨寫一篇文章介紹MapStruct的使用。
第九種,Orika框架:
支持在代碼中註冊字段映射,經過javassist類庫生成Bean映射的字節碼,以後直接加載執行生成的字節碼文件。
第十種,ModelMapper框架:
基於反射原理進行賦值或者直接對成員變量賦值。至關因而BeanUtils
的進階版
其餘幾種框架就沒有深刻研究了。但看使用狀況應該都能知足實際場景的要求。介紹的這些轉換方法中,在性能上基本遵循:手動賦值 > cglib > 反射 > Dozer > 序列化。
在實際項目中,須要綜合使用上述方法進行模型轉換。好比較低層的DO,由於涉及到的嵌套對象少,改動也少,因此可使用BeanUtils直接轉。若是是速度、穩定優先的系統,仍是乖乖使用Set、Get實現吧。