SpringBoot如何進(jìn)行對象復(fù)制的實(shí)踐
首先我們看看為什么需要對象復(fù)制?
為什么需要對象復(fù)制
如上,是我們平時(shí)開發(fā)中最常見的三層MVC架構(gòu)模型,編輯操作時(shí)Controller層接收到前端傳來的DTO對象,在Service層需要將DTO
轉(zhuǎn)換成DO
,然后在數(shù)據(jù)庫中保存。查詢操作時(shí)Service層查詢到DO對象后需要將DO
對象轉(zhuǎn)換成VO
對象,然后通過Controller層返回給前端進(jìn)行渲染。
這中間會(huì)涉及到大量的對象轉(zhuǎn)換,很明顯我們不能直接使用getter/setter
復(fù)制對象屬性,這看上去太low了。想象一下你業(yè)務(wù)邏輯中充斥著大量的getter&setter
,代碼評審時(shí)老鳥們會(huì)如何笑話你?
所以我們必須要找一個(gè)第三方工具來幫我們實(shí)現(xiàn)對象轉(zhuǎn)換。
看到這里有同學(xué)可能會(huì)問,為什么不能前后端都統(tǒng)一使用DO對象呢?這樣就不存在對象轉(zhuǎn)換呀?
設(shè)想一下如果我們不想定義 DTO 和 VO,直接將 DO 用到數(shù)據(jù)訪問層、服務(wù)層、控制層和外部訪問接口上。此時(shí)該表刪除或則修改一個(gè)字段,DO 必須同步修改,這種修改將會(huì)影響到各層,這并不符合高內(nèi)聚低耦合的原則。通過定義不同的 DTO 可以控制對不同系統(tǒng)暴露不同的屬性,通過屬性映射還可以實(shí)現(xiàn)具體的字段名稱的隱藏。不同業(yè)務(wù)使用不同的模型,當(dāng)一個(gè)業(yè)務(wù)發(fā)生變更需要修改字段時(shí),不需要考慮對其它業(yè)務(wù)的影響,如果使用同一個(gè)對象則可能因?yàn)?“不敢亂改” 而產(chǎn)生很多不優(yōu)雅的兼容性行為。
對象復(fù)制工具類推薦
對象復(fù)制的類庫工具有很多,除了常見的Apache的BeanUtils
,Spring的BeanUtils
,Cglib BeanCopier
,還有重量級組件MapStruct
,Orika
,Dozer
,ModelMapper
等。
如果沒有特殊要求,這些工具類都可以直接使用,除了Apache的BeanUtils
。原因在于Apache BeanUtils
底層源碼為了追求完美,加了過多的包裝,使用了很多反射,做了很多校驗(yàn),所以導(dǎo)致性能較差,并在阿里巴巴開發(fā)手冊上強(qiáng)制規(guī)定避免使用 Apache BeanUtils。
至于剩下的重量級組件,綜合考慮其性能還有使用的易用性,我這里更推薦使用Orika
。Orika底層采用了javassist類庫生成Bean映射的字節(jié)碼,之后直接加載執(zhí)行生成的字節(jié)碼文件,在速度上比使用反射進(jìn)行賦值會(huì)快很多。
國外大神 baeldung 已經(jīng)對常見的組件性能進(jìn)行過詳細(xì)測試,大家可以通過 https://www.baeldung.com/java-performance-mapping-frameworks 查看。
Orika基本使用
要使用Orika很簡單,只需要簡單四步:
引入依賴
<dependency> <groupId>ma.glasnost.orika</groupId> <artifactId>orika-core</artifactId> <version>1.5.4</version> </dependency>
構(gòu)造一個(gè)MapperFactory
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
注冊字段映射
mapperFactory.classMap(SourceClass.class, TargetClass.class) .field("firstName", "givenName") .field("lastName", "sirName") .byDefault() .register();
當(dāng)字段名在兩個(gè)實(shí)體不一致時(shí)可以通過.field()
方法進(jìn)行映射,如果字段名都一樣則可省略,byDefault()
方法用于注冊名稱相同的屬性,如果不希望某個(gè)字段參與映射,可以使用exclude
方法。
進(jìn)行映射
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);
經(jīng)過上面四步我們就完成了SourceClass到TargetClass的轉(zhuǎn)換。至于Orika的其他使用方法大家可以參考 http://orika-mapper.github.io/orika-docs/index.html
看到這里,肯定有粉絲會(huì)說:你這推薦的啥玩意呀,這個(gè)Orika使用也不簡單呀,每次都要這先創(chuàng)建MapperFactory
,建立字段映射關(guān)系,才能進(jìn)行映射轉(zhuǎn)換。
別急,我這里給你準(zhǔn)備了一個(gè)工具類OrikaUtils
,你可以通過文末github倉庫獲取。
它提供了五個(gè)公共方法:
分別對應(yīng):
- 字段一致實(shí)體轉(zhuǎn)換
- 字段不一致實(shí)體轉(zhuǎn)換(需要字段映射)
- 字段一致集合轉(zhuǎn)換
- 字段不一致集合轉(zhuǎn)換(需要字段映射)
- 字段屬性轉(zhuǎn)換注冊
接下來我們通過單元測試案例重點(diǎn)介紹此工具類的使用。
Orika工具類使用文檔
先準(zhǔn)備兩個(gè)基礎(chǔ)實(shí)體類,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,基礎(chǔ)實(shí)體映射
/** * 只拷貝相同的屬性 */ @Test public void convertObject(){ Student student = new Student("1","javadaily","jianzh5@163.com"); Teacher teacher = OrikaUtils.convert(student, Teacher.class); System.out.println(teacher); }
輸出結(jié)果:
Teacher(id=1, name=javadaily, emailAddress=null)
此時(shí)由于屬性名不一致,無法映射字段email。
TC2,實(shí)體映射 - 字段轉(zhuǎn)換
/** * 拷貝不同屬性 */ @Test public void convertRefObject(){ Student student = new Student("1","javadaily","jianzh5@163.com"); Map<String,String> refMap = new HashMap<>(1); //map key 放置 源屬性,value 放置 目標(biāo)屬性 refMap.put("email","emailAddress"); Teacher teacher = OrikaUtils.convert(student, Teacher.class, refMap); System.out.println(teacher); }
輸出結(jié)果:
Teacher(id=1, name=javadaily, emailAddress=jianzh5@163.com)
此時(shí)由于對字段做了映射,可以將email映射到emailAddress。注意這里的refMap中key放置的是源實(shí)體的屬性,而value放置的是目標(biāo)實(shí)體的屬性,不要弄反了。
TC3,基礎(chǔ)集合映射
/** * 只拷貝相同的屬性集合 */ @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); }
輸出結(jié)果:
[Teacher(id=1, name=javadaily, emailAddress=null), Teacher(id=2, name=JAVA日知錄, emailAddress=null)]
此時(shí)由于屬性名不一致,集合中無法映射字段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 放置 目標(biāo)屬性 refMap.put("email","emailAddress"); List<Teacher> teacherList = OrikaUtils.convertList(studentList, Teacher.class,refMap); System.out.println(teacherList); }
輸出結(jié)果:
[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,集合與實(shí)體映射
有時(shí)候我們需要將集合數(shù)據(jù)映射到實(shí)體中,如Person類
@Data public class Person { private List<String> nameParts; }
現(xiàn)在需要將Person類nameParts的值映射到Student中,可以這樣做
/** * 數(shù)組和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 放置 目標(biāo)屬性 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); }
輸出結(jié)果:
Student(id=1, name=javadaily, email=jianzh5@163.com)
TC6,類類型映射
有時(shí)候我們需要類類型對象映射,如BasicPerson類
@Data public class BasicPerson { private Student student; }
現(xiàn)在需要將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 放置 目標(biāo)屬性 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); }
輸出結(jié)果:
Teacher(id=1, name=javadaily, emailAddress=jianzh5@163.com)
TC7,多重映射
有時(shí)候我們會(huì)遇到多重映射,如將StudentGrade
映射到TeacherGrade
@Data public class StudentGrade { private String studentGradeName; private List<Student> studentList; } @Data public class TeacherGrade { private String teacherGradeName; private List<Teacher> teacherList; }
這種場景稍微復(fù)雜,Student與Teacher的屬性有email字段不相同,需要做轉(zhuǎn)換映射;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 放置 目標(biāo)屬性 refMap1.put("email","emailAddress"); OrikaUtils.register(Student.class,Teacher.class,refMap1); Map<String,String> refMap2 = new HashMap<>(2); //map key 放置 源屬性,value 放置 目標(biāo)屬性 refMap2.put("studentGradeName", "teacherGradeName"); refMap2.put("studentList", "teacherList"); TeacherGrade teacherGrade = OrikaUtils.convert(studentGrade,TeacherGrade.class,refMap2); System.out.println(teacherGrade); }
多重映射的場景需要根據(jù)情況調(diào)用OrikaUtils.register()
注冊字段映射。
輸出結(jié)果:
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的分頁組件,可以這樣轉(zhuǎn)換
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); // 實(shí)體轉(zhuǎn)換 SysKind轉(zhuǎn)化為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)); }
小結(jié)
在MVC架構(gòu)中肯定少不了需要用到對象復(fù)制,屬性轉(zhuǎn)換的功能,借用Orika組件,可以很簡單實(shí)現(xiàn)這些功能。本文在Orika的基礎(chǔ)上封裝了工具類,進(jìn)一步簡化了Orika的操作,希望對各位有所幫助。
到此這篇關(guān)于SpringBoot如何進(jìn)行對象復(fù)制的實(shí)踐的文章就介紹到這了,更多相關(guān)SpringBoot 對象復(fù)制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Jmeter連接Mysql數(shù)據(jù)庫實(shí)現(xiàn)過程詳解
這篇文章主要介紹了Jmeter連接Mysql數(shù)據(jù)庫實(shí)現(xiàn)過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08java數(shù)據(jù)結(jié)構(gòu)與算法之簡單選擇排序詳解
這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)與算法之簡單選擇排序,結(jié)合實(shí)例形式分析了選擇排序的原理、實(shí)現(xiàn)方法與相關(guān)操作技巧,需要的朋友可以參考下2017-05-05SpringBoot使用@ControllerAdvice全局異常處理
這篇文章主要介紹了SpringBoot使用@ControllerAdvice全局異常處理,異常處理是非常重要的一部分,它可以幫助我們捕獲并處理應(yīng)用程序中出現(xiàn)的異常情況,提高應(yīng)用程序的健壯性和可靠性,需要的朋友可以參考下2023-07-07SpringBoot項(xiàng)目中日志管理與調(diào)優(yōu)指南
在 Spring Boot 開發(fā)過程中,日志管理是開發(fā)者必須掌握的重要技能之一,合理的日志配置不僅能幫助開發(fā)者追蹤應(yīng)用程序的執(zhí)行流程、定位問題,還能提升應(yīng)用程序的可維護(hù)性,本文將詳細(xì)探討 Spring Boot 項(xiàng)目中日志管理的常見問題、解決方案以及最佳實(shí)踐2024-10-10SpringBoot中的@ApiModelProperty注解作用
這篇文章主要介紹了SpringBoot中的@ApiModelProperty注解作用,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教。2022-01-01Spring,hibernate,struts經(jīng)典面試筆試題(含答案)
這篇文章主要介紹了Spring,hibernate,struts經(jīng)典面試筆試題極其參考含答案,涉及SSH基本概念,原理與使用技巧,需要的朋友可以參考下2016-03-03