SpringBoot 如何進行物件複製,老鳥們都這麼玩的!

語言: CN / TW / HK

大家好,我是飄渺。

今天帶來SpringBoot老鳥系列的第四篇,來聊聊在日常開發中如何優雅的實現物件複製。

首先我們看看為什麼需要物件複製?

為什麼需要物件複製

image-20210906145134173

如上,是我們平時開發中最常見的三層MVC架構模型,編輯操作時Controller層接收到前端傳來的DTO物件,在Service層需要將DTO轉換成DO,然後在資料庫中儲存。查詢操作時Service層查詢到DO物件後需要將DO物件轉換成VO物件,然後通過Controller層返回給前端進行渲染。

這中間會涉及到大量的物件轉換,很明顯我們不能直接使用getter/setter複製物件屬性,這看上去太low了。想象一下你業務邏輯中充斥著大量的getter&setter,程式碼評審時老鳥們會如何笑話你?

image-20210716084136689

所以我們必須要找一個第三方工具來幫我們實現物件轉換。

看到這裡有同學可能會問,為什麼不能前後端都統一使用DO物件呢?這樣就不存在物件轉換呀?

設想一下如果我們不想定義 DTO 和 VO,直接將 DO 用到資料訪問層、服務層、控制層和外部訪問介面上。此時該表刪除或則修改一個欄位,DO 必須同步修改,這種修改將會影響到各層,這並不符合高內聚低耦合的原則。通過定義不同的 DTO 可以控制對不同系統暴露不同的屬性,通過屬性對映還可以實現具體的欄位名稱的隱藏。不同業務使用不同的模型,當一個業務發生變更需要修改欄位時,不需要考慮對其它業務的影響,如果使用同一個物件則可能因為 “不敢亂改” 而產生很多不優雅的相容性行為。

物件複製工具類推薦

物件複製的類庫工具有很多,除了常見的Apache的BeanUtils,Spring的BeanUtilsCglib BeanCopier,還有重量級元件MapStructOrikaDozerModelMapper等。

如果沒有特殊要求,這些工具類都可以直接使用,除了Apache的BeanUtils。原因在於Apache BeanUtils底層原始碼為了追求完美,加了過多的包裝,使用了很多反射,做了很多校驗,所以導致效能較差,並在阿里巴巴開發手冊上強制規定避免使用 Apache BeanUtils

強制規定避免使用 Apache BeanUtils

至於剩下的重量級元件,綜合考慮其效能還有使用的易用性,我這裡更推薦使用Orika。Orika底層採用了javassist類庫生成Bean對映的位元組碼,之後直接載入執行生成的位元組碼檔案,在速度上比使用反射進行賦值會快很多。

國外大神 baeldung 已經對常見的元件效能進行過詳細測試,大家可以通過 https://www.baeldung.com/java-performance-mapping-frameworks 檢視。

Orika基本使用

要使用Orika很簡單,只需要簡單四步:

  1. 引入依賴

xml <dependency> <groupId>ma.glasnost.orika</groupId> <artifactId>orika-core</artifactId> <version>1.5.4</version> </dependency>

  1. 構造一個MapperFactory

java MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();

  1. 註冊欄位對映

java mapperFactory.classMap(SourceClass.class, TargetClass.class) .field("firstName", "givenName") .field("lastName", "sirName") .byDefault() .register();

當欄位名在兩個實體不一致時可以通過.field()方法進行對映,如果欄位名都一樣則可省略,byDefault()方法用於註冊名稱相同的屬性,如果不希望某個欄位參與對映,可以使用exclude方法。

  1. 進行對映

```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倉庫獲取。

它提供了五個公共方法:

image-20210903151829872

分別對應:

  1. 欄位一致實體轉換
  2. 欄位不一致實體轉換(需要欄位對映)
  3. 欄位一致集合轉換
  4. 欄位不一致集合轉換(需要欄位對映)
  5. 欄位屬性轉換註冊

接下來我們通過單元測試案例重點介紹此工具類的使用。

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 refMap = new HashMap<>(1); //map key 放置 源屬性,value 放置 目標屬性 refMap.put("email","emailAddress"); Teacher teacher = OrikaUtils.convert(student, Teacher.class, refMap); System.out.println(teacher); } ```

輸出結果:

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 studentList = Lists.newArrayList(student1,student2);

List teacherList = OrikaUtils.convertList(studentList, Teacher.class);

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 studentList = Lists.newArrayList(student1,student2);

Map refMap = new HashMap<>(2); //map key 放置 源屬性,value 放置 目標屬性 refMap.put("email","emailAddress");

List teacherList = OrikaUtils.convertList(studentList, Teacher.class,refMap);

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 studentList; }

@Data public class TeacherGrade { private String teacherGradeName; private List teacherList; } ```

這種場景稍微複雜,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 studentList = Lists.newArrayList(student1,student2);

StudentGrade studentGrade = new StudentGrade(); studentGrade.setStudentGradeName("碩士"); studentGrade.setStudentList(studentList);

Map refMap1 = new HashMap<>(1); //map key 放置 源屬性,value 放置 目標屬性 refMap1.put("email","emailAddress"); OrikaUtils.register(Student.class,Teacher.class,refMap1);

Map refMap2 = new HashMap<>(2); //map key 放置 源屬性,value 放置 目標屬性 refMap2.put("studentGradeName", "teacherGradeName"); refMap2.put("studentList", "teacherList");

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 獲取