javascript
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。
強制規定避免使用 Apache BeanUtils至于剩下的重量級組件,綜合考慮其性能還有使用的易用性,我這里更推薦使用Orika。Orika底層采用了javassist類庫生成Bean映射的字節碼,之后直接加載執行生成的字節碼文件,在速度上比使用反射進行賦值會快很多。
“
國外大神 baeldung 已經對常見的組件性能進行過詳細測試,大家可以通過 https://www.baeldung.com/java-performance-mapping-frameworks 查看。
”
?
Orika基本使用
要使用Orika很簡單,只需要簡單四步:
引入依賴
構造一個MapperFactory
注冊字段映射
當字段名在兩個實體不一致時可以通過.field()方法進行映射,如果字段名都一樣則可省略,byDefault()方法用于注冊名稱相同的屬性,如果不希望某個字段參與映射,可以使用exclude方法。
進行映射
經過上面四步我們就完成了SourceClass到TargetClass的轉換。至于Orika的其他使用方法大家可以參考 http://orika-mapper.github.io/orika-docs/index.html
看到這里,肯定有粉絲會說:你這推薦的啥玩意呀,這個Orika使用也不簡單呀,每次都要這先創建MapperFactory,建立字段映射關系,才能進行映射轉換。
別急,我這里給你準備了一個工具類OrikaUtils,你可以通過文末github倉庫獲取。
它提供了五個公共方法:
分別對應:
字段一致實體轉換
字段不一致實體轉換(需要字段映射)
字段一致集合轉換
字段不一致集合轉換(需要字段映射)
字段屬性轉換注冊
接下來我們通過單元測試案例重點介紹此工具類的使用。
?
Orika工具類使用文檔
先準備兩個基礎實體類,Student,Teacher。
@Data @AllArgsConstructor @NoArgsConstructor public?class?Student?{private?String?id;private?String?name;private?String?email; }@Data @AllArgsConstructor @NoArgsConstructor public?class?Teacher?{private?String?id;private?String?name;private?String?emailAddress; }TC1,基礎實體映射
/***?只拷貝相同的屬性*/ @Test public?void?convertObject(){Student?student?=?new?Student("1","javadaily","jianzh5@163.com");Teacher?teacher?=?OrikaUtils.convert(student,?Teacher.class);System.out.println(teacher); }輸出結果:
Teacher(id=1,?name=javadaily,?emailAddress=null)此時由于屬性名不一致,無法映射字段email。
TC2,實體映射 - 字段轉換
/***?拷貝不同屬性*/ @Test public?void?convertRefObject(){Student?student?=?new?Student("1","javadaily","jianzh5@163.com");Map<String,String>?refMap?=?new?HashMap<>(1);//map?key?放置?源屬性,value?放置?目標屬性refMap.put("email","emailAddress");Teacher?teacher?=?OrikaUtils.convert(student,?Teacher.class,?refMap);System.out.println(teacher); }輸出結果:
Teacher(id=1,?name=javadaily,?emailAddress=jianzh5@163.com)此時由于對字段做了映射,可以將email映射到emailAddress。注意這里的refMap中key放置的是源實體的屬性,而value放置的是目標實體的屬性,不要弄反了。
TC3,基礎集合映射
/***?只拷貝相同的屬性集合*/ @Test public?void?convertList(){Student?student1?=?new?Student("1","javadaily","jianzh5@163.com");Student?student2?=?new?Student("2","JAVA日知錄","jianzh5@xxx.com");List<Student>?studentList?=?Lists.newArrayList(student1,student2);List<Teacher>?teacherList?=?OrikaUtils.convertList(studentList,?Teacher.class);System.out.println(teacherList); }輸出結果:
[Teacher(id=1,?name=javadaily,?emailAddress=null),?Teacher(id=2,?name=JAVA日知錄,?emailAddress=null)]此時由于屬性名不一致,集合中無法映射字段email。
TC4,集合映射 - 字段映射
/***?映射不同屬性的集合*/ @Test public?void?convertRefList(){Student?student1?=?new?Student("1","javadaily","jianzh5@163.com");Student?student2?=?new?Student("2","JAVA日知錄","jianzh5@xxx.com");List<Student>?studentList?=?Lists.newArrayList(student1,student2);Map<String,String>?refMap?=?new?HashMap<>(2);//map?key?放置?源屬性,value?放置?目標屬性refMap.put("email","emailAddress");List<Teacher>?teacherList?=?OrikaUtils.convertList(studentList,?Teacher.class,refMap);System.out.println(teacherList); }輸出結果:
[Teacher(id=1,?name=javadaily,?emailAddress=jianzh5@163.com),?Teacher(id=2,?name=JAVA日知錄,?emailAddress=jianzh5@xxx.com)]也可以通過這樣映射:
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中,可以這樣做
/***?數組和List的映射*/ @Test public?void?convertListObject(){Person?person?=?new?Person();person.setNameParts(Lists.newArrayList("1","javadaily","jianzh5@163.com"));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); }輸出結果:
Student(id=1,?name=javadaily,?email=jianzh5@163.com)TC6,類類型映射
有時候我們需要類類型對象映射,如BasicPerson類
@Data public?class?BasicPerson?{private?Student?student; }現在需要將BasicPerson映射到Teacher
/***?類類型映射*/ @Test public?void?convertClassObject(){BasicPerson?basicPerson?=?new?BasicPerson();Student?student?=?new?Student("1","javadaily","jianzh5@163.com");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); }輸出結果:
Teacher(id=1,?name=javadaily,?emailAddress=jianzh5@163.com)TC7,多重映射
有時候我們會遇到多重映射,如將StudentGrade映射到TeacherGrade
@Data public?class?StudentGrade?{private?String?studentGradeName;private?List<Student>?studentList; }@Data public?class?TeacherGrade?{private?String?teacherGradeName;private?List<Teacher>?teacherList; }這種場景稍微復雜,Student與Teacher的屬性有email字段不相同,需要做轉換映射;StudentGrade與TeacherGrade中的屬性也需要映射。
/***?一對多映射*/ @Test public?void?convertComplexObject(){Student?student1?=?new?Student("1","javadaily","jianzh5@163.com");Student?student2?=?new?Student("2","JAVA日知錄","jianzh5@xxx.com");List<Student>?studentList?=?Lists.newArrayList(student1,student2);StudentGrade?studentGrade?=?new?StudentGrade();studentGrade.setStudentGradeName("碩士");studentGrade.setStudentList(studentList);Map<String,String>?refMap1?=?new?HashMap<>(1);//map?key?放置?源屬性,value?放置?目標屬性refMap1.put("email","emailAddress");OrikaUtils.register(Student.class,Teacher.class,refMap1);Map<String,String>?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()注冊字段映射。
輸出結果:
TeacherGrade(teacherGradeName=碩士,?teacherList=[Teacher(id=1,?name=javadaily,?emailAddress=jianzh5@163.com),?Teacher(id=2,?name=JAVA日知錄,?emailAddress=jianzh5@xxx.com)])TC8,MyBaits plus分頁映射
如果你使用的是mybatis的分頁組件,可以這樣轉換
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轉化為SysKindDtoMap<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的操作,希望對各位有所幫助。
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的SpringBoot 如何进行对象复制,老鸟们都这么玩的!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 入职微软三个月了!
- 下一篇: 一文详解,RocketMQ事务消息