您好,登錄后才能下訂單哦!
[TOC]
我們在軟件開發設計及開發過程中,習慣將軟件橫向拆分為幾個層。比如常見的三層架構:表現層(VIEW/UI)、業務邏輯層(SERVICE/BAL)、數據訪問層(DAO/DAL)。如下圖:
那應用系統為什么要分層呢?其實主要是解決以下幾個問題:
第一是解耦:
有一句計算機名言:軟件的所有問題都可以通過增加一層來解決。當系統越大,團隊越多,需求變化越快時,越需要保證程序之間的依賴關系越少。而分層/面向接口編程,會使我們在應對變化時越容易。
第二是簡化問題:
當我們想不明白從用戶操作一直到數據落盤整個過程的交互情況時,我們應該換種方式思考。想想各層應該提供哪些支持,通過對各層分工的明確定義,復雜問題就變成了如何將各層功能組合起來的“積木搭建”。
第三是降低系統維護與升級成本:
這里體現了面向接口編程的優勢。我們抽象出數據訪問層后,只需要保證對外提供的接口不變,底層數據庫使用Oracle還是MySql,上層結構是感知不到的。
第四是邏輯復用/代碼復用:
通過分層,明確定義各層職責,再也不會出現系統中多個地方查詢同一個數據庫表的代碼。因為查詢某個數據庫表的工作只會由一個數據訪問層類來統一提供。
如果開發團隊很多,通過分層和接口定義。各團隊只需要遵循接口標準/開發規范,就可以并行開發。有一個形容比較貼切:分層化相當于把軟件橫向切幾刀,模塊化相當于把軟件縱向切幾刀。
在《阿里巴巴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實現吧。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。