對於項目而言, 我們一般會有DAO->Service->Controller分層設計, 這些層次體現了每層的作用, 而層次之間的數據傳遞對象設計很少被提及, 下面是一個相對完整的數據轉換過程:
Table層--(DO對象)-->DAO層--(DO對象)-->Service層--(DTO對象)-->Controller層--(VO對象)-->Web Template層
DO(domain object) 領域對象, 也有一種叫法是 entity object, 個人不推薦使用 entity object這個叫法, 因為Relationship 表也可以有DO 類.
1. 一般DO類和數據表是一一對應, 屬性和字段也是一一對應的.
2. 對於 relationship 表, 一般也需要有一個對應的 DO 類.
3. DO 類中不應該包含復雜的邏輯, 僅僅是一些簡單的 setter() 和 getter() 方法.
4. 比如 user 表有一個 deptId 字段, user entity 類必須有一個 deptId 屬性, 不推薦有一個對應的 dept Entity 對象, 當然更不應該直接包含 dept entity 對象的屬性, 比如 deptName 等.
5. 多個 DO 類之間的關系和數據模型一樣, 是滿足第三范式.
6. DO類名: 以DO作為后綴, 或者不加DO這樣的后綴.
DTO(Data Transfer Object) 數據傳輸對象:
1. 用於"跨進程或遠程"傳輸, 對象的序列化/反序列化的網絡開銷較大, 這時就需要加入 DTO 對象, 典型的使用場景是用來封裝Rest API接口. 如果是一個單體應用, 專門維護一套DTO類, 成本和收益相比, 引入DTO意義就不大了.
2. DTO 是一個貧血對象, 它不應包含"業務"處理邏輯, 主要是一些屬性和getter和setter訪問器, 也可包含一些屬性重組邏輯.
3. 多個 DTO 類之間的關系一般不再滿足第三范式, 而是反范式.
4. DTO類名: 應該以DTO作為后綴.
5. 用於解耦實體對象的存儲層和上層, 這包含下面的好處:
(1): 隱藏部分底層表的屬性, 以減少網絡傳輸的代價.
(2): 在存儲層和上層增加一個隔離, 屏蔽相互之間的影響.
(3): 用來可封裝多個DTO對象, 比如user DTO對象可以包含一個Dept DTO對象; 或者將Dept DTO某些常用屬性直接flatten到User DTO對象上, 方便上層的使用. 所以, 一般情況下DTO類的數量要比DO類要少.
(4): 如果沒有DTO, 很多時候 Controller 層不得不直接最低層的 Entity 類, 跨層數據對象依賴將使得分層設計大打折扣.
VO(View Object/Value object)視圖對象:
專門用於展現層(比如頁面展現等).
對於一般的項目, VO 和 DTO 屬性基本一致, 沒有必要再維護一套VO類.
在前后端分離的大背景下, 即使是大型項目, 也沒有必要再維護一套VO類.
Pojo(plain old java object) 普通java對象:
上述的DO/DTO/VO對象都屬於Pojo對象. 阿里巴巴規范中有如下要求:
1. Pojo 類屬性必須使用包裝數據類型, 而不是基本數據類型, 理由是: 數據庫中由可能是null值, 如果使用基本類型, 由可能導致自動拆箱異常.
2. 不能為任何屬性設定默認值. 理由是: 強制使用者在使用時顯式賦值, 任何NPE問題都應由使用者來保證.
3. Pojo 類都必須實現 toString() 方法, 可以使用 IDE 的 source/generate toString() 功能
4. Pojo 類都必須實現 Serializable 接口.
下圖能形象展現 DO 類和 DTO 類的區別:
下圖以基於角色的權限管理模塊為例, 展現了 table/DO對象/DAO層/Service層 對應關系:
(1) table 是采用范式建模, 共三個實體表, 兩個關系表.
(2) pojo和table直接對應,
(3) 在Dao層重點關注實體的操作, 所以就只有三個dao對象, 關系表的維護退化到實體操作中.
(4) 在service層, 是面向頂層使用, 重點考慮的是使用如何方便, 一般也都和Dao的類數量一致.
=====================================
object-object mapping framework 清單
=====================================
正如ORM過程, 我們可以手寫代碼, 也可以使用MyBatis這樣的ORM 工具, 對於DO/DTO的轉換也是一樣, 可以手寫代碼轉換, 也可以使用object-object mapping framework完成自動轉換, 當然也能用在對象之間需要深度拷貝的時候.
當前活躍的框架清單:
https://github.com/mapstruct/mapstruct
https://github.com/DozerMapper/dozer
https://github.com/orika-mapper/orika
https://github.com/modelmapper/modelmapper
性能對比:
https://github.com/arey/java-object-mapper-benchmark
https://www.baeldung.com/java-performance-mapping-frameworks
綜合考慮性能/流行度/項目活躍程度, mapstruct 明顯領先, 不過我這里更推薦使用 orika-mapper, 原因是:
1. 使用 mapstruct的話, IDE中需要引入 mapstruct 編譯插件, 我們自己負責編寫mapper interface, mapstruct 負責生成mapper實現類的class文件, 工程化成本較高, 可控性較差.
2. orika-mapper 相對更容易推廣, mapping 邏輯可控性較強, 性能也算不錯.
=====================================
orika-mapper 使用
=====================================
pom.xml 引入依賴
<dependency> <groupId>ma.glasnost.orika</groupId> <artifactId>orika-core</artifactId> <version>${orika.version}</version> </dependency>
--------------------------------
簡單示例:
--------------------------------
Orika 要求 Pojo 類必須是public級別,否則會在運行時報錯 java.lang.IllegalAccessError.
只要src/target屬性名一致, Orika 能自動完成所有的屬性的深拷貝, 甚至是 List<T> 這樣的屬性. 需要說明的是, 默認情況下定義的映射規則是雙向的, 也就是說, 如果從 target object 也能得到 src object.
//准備 src 對象 PersonSource source = new PersonSource(); //生成 MapperFactory 對象, 然后使用 MapperFactory 對象注冊 src/target 映射關系, 本例 src/target 的屬性名完全一致, 所以不需要再設置映射關系. MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build(); // MapperFacade 對象負責對象之間的映射 MapperFacade mapper = mapperFactory.getMapperFacade(); //完成DO->DTO的映射 PersonDest destination = mapper.map(source, PersonDest.class);
--------------------------------
更復雜的示例:
--------------------------------
如果src/target屬性名不一致, 或者要重組DO屬性, 這時需要自定義src/target的映射規則.
//自定義 src/target class的映射規則 mapperFactory.classMap( Source.class, Destination.class) .field(......) //src/target 雙向映射 .fieldAToB(......) //src->target的單向映射 .fieldBToA(......) //target->src的單向映射 .exclude(......) //不考慮指定的屬性 .byDefault() .register();
mapperFactory 除了負責注冊src/target 映射規則, 還可以 mapperFactory.getConverterFactory().registerConverter()來注冊自定義類型轉換器, 比如要將 Date 類型轉換為 String類型, 實際項目中, 一般不需要自定義類型轉換器, 因為 json 框架也有這樣的功能.
//定義一個類型轉換器 MyConverter public class MyConverter extends CustomConverter<Date,MyDate>{} //注冊類型轉換器 mapperFactory.getConverterFactory().registerConverter(new MyConverter()); // 然后仍然通過 mapper 完成src/target對象的深度拷貝. MapperFacade mapper = mapperFactory.getMapperFacade();
=====================================
參考
=====================================
https://cloud.tencent.com/developer/article/1110666
http://tech.dianwoda.com/2017/11/04/gao-xing-neng-te-xing-feng-fu-de-beanying-she-gong-ju-orika/?utmsource=tuicool&utmmedium=referral