SpringBoot 如何進行對象複製,老鳥們都這麼玩的!
大家好,我是飄渺。
今天帶來SpringBoot老鳥系列的第四篇,來聊聊在日常開發中如何優雅的實現對象複製。
首先我們看看為什麼需要對象複製?
為什麼需要對象複製
如上,是我們平時開發中最常見的三層MVC架構模型,編輯操作時Controller層接收到前端傳來的DTO對象,在Service層需要將DTO
轉換成DO
,然後在數據庫中保存。查詢操作時Service層查詢到DO對象後需要將DO
對象轉換成VO
對象,然後通過Controller層返回給前端進行渲染。
這中間會涉及到大量的對象轉換,很明顯我們不能直接使用getter/setter
複製對象屬性,這看上去太low了。想象一下你業務邏輯中充斥着大量的getter&setter
,代碼評審時老鳥們會如何笑話你?
所以我們必須要找一個第三方工具來幫我們實現對象轉換。
看到這裏有同學可能會問,為什麼不能前後端都統一使用DO對象呢?這樣就不存在對象轉換呀?
設想一下如果我們不想定義 DTO 和 VO,直接將 DO 用到數據訪問層、服務層、控制層和外部訪問接口上。此時該表刪除或則修改一個字段,DO 必須同步修改,這種修改將會影響到各層,這並不符合高內聚低耦合的原則。通過定義不同的 DTO 可以控制對不同系統暴露不同的屬性,通過屬性映射還可以實現具體的字段名稱的隱藏。不同業務使用不同的模型,當一個業務發生變更需要修改字段時,不需要考慮對其它業務的影響,如果使用同一個對象則可能因為 “不敢亂改” 而產生很多不優雅的兼容性行為。
對象複製工具類推薦
對象複製的類庫工具有很多,除了常見的Apache的BeanUtils
,Spring的BeanUtils
,Cglib BeanCopier
,還有重量級組件MapStruct
,Orika
,Dozer
,ModelMapper
等。
如果沒有特殊要求,這些工具類都可以直接使用,除了Apache的BeanUtils
。原因在於Apache BeanUtils
底層源碼為了追求完美,加了過多的包裝,使用了很多反射,做了很多校驗,所以導致性能較差,並在阿里巴巴開發手冊上強制規定避免使用 Apache BeanUtils。
至於剩下的重量級組件,綜合考慮其性能還有使用的易用性,我這裏更推薦使用Orika
。Orika底層採用了javassist類庫生成Bean映射的字節碼,之後直接加載執行生成的字節碼文件,在速度上比使用反射進行賦值會快很多。
國外大神 baeldung 已經對常見的組件性能進行過詳細測試,大家可以通過 https://www.baeldung.com/java-performance-mapping-frameworks 查看。
Orika基本使用
要使用Orika很簡單,只需要簡單四步:
- 引入依賴
xml
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.4</version>
</dependency>
- 構造一個MapperFactory
java
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
- 註冊字段映射
java
mapperFactory.classMap(SourceClass.class, TargetClass.class)
.field("firstName", "givenName")
.field("lastName", "sirName")
.byDefault()
.register();
當字段名在兩個實體不一致時可以通過.field()
方法進行映射,如果字段名都一樣則可省略,byDefault()
方法用於註冊名稱相同的屬性,如果不希望某個字段參與映射,可以使用exclude
方法。
- 進行映射
```java MapperFacade mapper = mapperFactory.getMapperFacade();
SourceClass source = new SourceClass();
// set some field values
...
// map the fields of 'source' onto a new instance of PersonDest
TargetClass target = mapper.map(source, TargetClass.class);
```
經過上面四步我們就完成了SourceClass到TargetClass的轉換。至於Orika的其他使用方法大家可以參考 http://orika-mapper.github.io/orika-docs/index.html
看到這裏,肯定有粉絲會説:你這推薦的啥玩意呀,這個Orika使用也不簡單呀,每次都要這先創建MapperFactory
,建立字段映射關係,才能進行映射轉換。
別急,我這裏給你準備了一個工具類OrikaUtils
,你可以通過文末github倉庫獲取。
它提供了五個公共方法:
分別對應:
- 字段一致實體轉換
- 字段不一致實體轉換(需要字段映射)
- 字段一致集合轉換
- 字段不一致集合轉換(需要字段映射)
- 字段屬性轉換註冊
接下來我們通過單元測試案例重點介紹此工具類的使用。
Orika工具類使用文檔
先準備兩個基礎實體類,Student,Teacher。
```java @Data @AllArgsConstructor @NoArgsConstructor public class Student { private String id; private String name; private String email; }
```
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
private String id;
private String name;
private String emailAddress;
}
TC1,基礎實體映射
java
/**
* 只拷貝相同的屬性
*/
@Test
public void convertObject(){
Student student = new Student("1","javadaily","[email protected]");
Teacher teacher = OrikaUtils.convert(student, Teacher.class);
System.out.println(teacher);
}
輸出結果:
java
Teacher(id=1, name=javadaily, emailAddress=null)
此時由於屬性名不一致,無法映射字段email。
TC2,實體映射 - 字段轉換
```java /* * 拷貝不同屬性 / @Test public void convertRefObject(){ Student student = new Student("1","javadaily","[email protected]");
Map
輸出結果:
java
Teacher(id=1, name=javadaily, [email protected])
此時由於對字段做了映射,可以將email映射到emailAddress。注意這裏的refMap中key放置的是源實體的屬性,而value放置的是目標實體的屬性,不要弄反了。
TC3,基礎集合映射
```java
/*
* 只拷貝相同的屬性集合
/
@Test
public void convertList(){
Student student1 = new Student("1","javadaily","[email protected]");
Student student2 = new Student("2","JAVA日知錄","[email protected]");
List
List
System.out.println(teacherList); } ```
輸出結果:
java
[Teacher(id=1, name=javadaily, emailAddress=null), Teacher(id=2, name=JAVA日知錄, emailAddress=null)]
此時由於屬性名不一致,集合中無法映射字段email。
TC4,集合映射 - 字段映射
```java
/*
* 映射不同屬性的集合
/
@Test
public void convertRefList(){
Student student1 = new Student("1","javadaily","[email protected]");
Student student2 = new Student("2","JAVA日知錄","[email protected]");
List
Map
List
System.out.println(teacherList); } ```
輸出結果:
java
[Teacher(id=1, name=javadaily, [email protected]), Teacher(id=2, name=JAVA日知錄, [email protected])]
也可以通過這樣映射:
Map<String,String> refMap = new HashMap<>(2);
refMap.put("email","emailAddress");
List<Teacher> teacherList = OrikaUtils.classMap(Student.class,Teacher.class,refMap)
.mapAsList(studentList,Teacher.class);
TC5,集合與實體映射
有時候我們需要將集合數據映射到實體中,如Person類
@Data
public class Person {
private List<String> nameParts;
}
現在需要將Person類nameParts的值映射到Student中,可以這樣做
```java /* * 數組和List的映射 / @Test public void convertListObject(){ Person person = new Person(); person.setNameParts(Lists.newArrayList("1","javadaily","[email protected]"));
Map<String,String> refMap = new HashMap<>(2);
//map key 放置 源屬性,value 放置 目標屬性
refMap.put("nameParts[0]","id");
refMap.put("nameParts[1]","name");
refMap.put("nameParts[2]","email");
Student student = OrikaUtils.convert(person, Student.class,refMap);
System.out.println(student);
} ```
輸出結果:
java
Student(id=1, name=javadaily, [email protected])
TC6,類類型映射
有時候我們需要類類型對象映射,如BasicPerson類
@Data
public class BasicPerson {
private Student student;
}
現在需要將BasicPerson映射到Teacher
```java /* * 類類型映射 / @Test public void convertClassObject(){ BasicPerson basicPerson = new BasicPerson(); Student student = new Student("1","javadaily","[email protected]"); basicPerson.setStudent(student);
Map<String,String> refMap = new HashMap<>(2);
//map key 放置 源屬性,value 放置 目標屬性
refMap.put("student.id","id");
refMap.put("student.name","name");
refMap.put("student.email","emailAddress");
Teacher teacher = OrikaUtils.convert(basicPerson, Teacher.class,refMap);
System.out.println(teacher);
} ```
輸出結果:
java
Teacher(id=1, name=javadaily, [email protected])
TC7,多重映射
有時候我們會遇到多重映射,如將StudentGrade
映射到TeacherGrade
```java
@Data
public class StudentGrade {
private String studentGradeName;
private List
@Data
public class TeacherGrade {
private String teacherGradeName;
private List
這種場景稍微複雜,Student與Teacher的屬性有email字段不相同,需要做轉換映射;StudentGrade與TeacherGrade中的屬性也需要映射。
```java
/*
* 一對多映射
/
@Test
public void convertComplexObject(){
Student student1 = new Student("1","javadaily","[email protected]");
Student student2 = new Student("2","JAVA日知錄","[email protected]");
List
StudentGrade studentGrade = new StudentGrade(); studentGrade.setStudentGradeName("碩士"); studentGrade.setStudentList(studentList);
Map
Map
TeacherGrade teacherGrade = OrikaUtils.convert(studentGrade,TeacherGrade.class,refMap2); System.out.println(teacherGrade); } ```
多重映射的場景需要根據情況調用OrikaUtils.register()
註冊字段映射。
輸出結果:
java
TeacherGrade(teacherGradeName=碩士, teacherList=[Teacher(id=1, name=javadaily, [email protected]), Teacher(id=2, name=JAVA日知錄, [email protected])])
TC8,MyBaits plus分頁映射
如果你使用的是mybatis的分頁組件,可以這樣轉換
java
public IPage<UserDTO> selectPage(UserDTO userDTO, Integer pageNo, Integer pageSize) {
Page page = new Page<>(pageNo, pageSize);
LambdaQueryWrapper<User> query = new LambdaQueryWrapper();
if (StringUtils.isNotBlank(userDTO.getName())) {
query.like(User::getKindName,userDTO.getName());
}
IPage<User> pageList = page(page,query);
// 實體轉換 SysKind轉化為SysKindDto
Map<String,String> refMap = new HashMap<>(3);
refMap.put("kindName","name");
refMap.put("createBy","createUserName");
refMap.put("createTime","createDate");
return pageList.convert(item -> OrikaUtils.convert(item, UserDTO.class, refMap));
}
小結
在MVC架構中肯定少不了需要用到對象複製,屬性轉換的功能,借用Orika組件,可以很簡單實現這些功能。本文在Orika的基礎上封裝了工具類,進一步簡化了Orika的操作,希望對各位有所幫助。
最後,我是飄渺Jam,一名寫代碼的架構師,做架構的程序員,期待您的轉發與關注,當然也可以添加我的個人微信 jianzh5,咱們一起聊技術!
老鳥系列源碼已經上傳至GitHub,需要的在公號【JAVA日知錄】回覆關鍵字 0923 獲取
- 數據權限就該這麼實現(設計篇)
- 數據權限就該這麼實現(實現篇)
- 給你一段SQL,你會如何優化?
- 當我把ChatGPT機器人拉到微信羣裏,羣友都玩瘋了!!
- SpringBoot 如何保證接口安全?老鳥們都是這麼玩的!
- 掌握系統思維,你就可以既勤奮努力又輕鬆愉快。
- SpringBoot自定義註解 AOP 防止重複提交(建議收藏)
- 面試官:應用上線後Cpu使用率飆升如何排查?
- SpringBoot中實現業務校驗,這種方式才叫優雅!
- SpringCloud Gateway 收集輸入輸出日誌
- 震驚,Spring官方推薦的@Transational還能導致生產事故?
- 為什麼要在MVC三層架構上再加一層Manager層?
- SpringBoot 如何生成接口文檔,老鳥們都這麼玩的!
- SpringBoot 如何進行限流?老鳥們都這麼玩的!
- SpringBoot 生成接口文檔,我用smart-doc,一款比Swagger更nice的工具!
- SpringBoot 如何進行對象複製,老鳥們都這麼玩的!
- 3天,我把MySQL索引、鎖、事務、分庫分表擼乾淨了!
- 字節全面對外開放中台能力!中台,又靈了?
- 基於 Kubernetes 的微服務項目設計與實現
- 老闆要我開發一個簡單的工作流引擎