【轉(zhuǎn)載】SpringBoot 如何進(jìn)行對象復(fù)制

原文
SpringBoot 如何進(jìn)行對象復(fù)制,老鳥們都這么玩的! (qq.com)

為什么需要對象復(fù)制

圖片

如上,是我們平時開發(fā)中最常見的三層MVC架構(gòu)模型盛末,編輯操作時Controller層接收到前端傳來的DTO對象,在Service層需要將DTO轉(zhuǎn)換成DO涯穷,然后在數(shù)據(jù)庫中保存噪径。查詢操作時Service層查詢到DO對象后需要將DO對象轉(zhuǎn)換成VO對象柱恤,然后通過Controller層返回給前端進(jìn)行渲染。這中間會涉及到大量的對象轉(zhuǎn)換找爱,很明顯我們不能直接使用getter/setter復(fù)制對象屬性梗顺,這看上去太low了。

看到這里有同學(xué)可能會問车摄,為什么不能前后端都統(tǒng)一使用DO對象呢寺谤?這樣就不存在對象轉(zhuǎn)換呀?
設(shè)想一下如果我們不想定義 DTO 和 VO吮播,直接將 DO 用到數(shù)據(jù)訪問層变屁、服務(wù)層、控制層和外部訪問接口上意狠。此時該表刪除或則修改一個字段粟关,DO 必須同步修改,這種修改將會影響到各層环戈,這并不符合高內(nèi)聚低耦合的原則闷板。通過定義不同的 DTO 可以控制對不同系統(tǒng)暴露不同的屬性,通過屬性映射還可以實現(xiàn)具體的字段名稱的隱藏院塞。不同業(yè)務(wù)使用不同的模型遮晚,當(dāng)一個業(yè)務(wù)發(fā)生變更需要修改字段時,不需要考慮對其它業(yè)務(wù)的影響拦止,如果使用同一個對象則可能因為 “不敢亂改” 而產(chǎn)生很多不優(yōu)雅的兼容性行為县遣。

對象復(fù)制工具類推薦

對象復(fù)制的類庫工具有很多,除了常見的Apache的BeanUtils汹族,Spring的BeanUtils萧求,Cglib BeanCopier,還有重量級組件MapStruct顶瞒,Orika饭聚,DozerModelMapper等搁拙。如果沒有特殊要求秒梳,這些工具類都可以直接使用,除了Apache的BeanUtils箕速。原因在于Apache BeanUtils底層源碼為了追求完美酪碘,加了過多的包裝,使用了很多反射盐茎,做了很多校驗兴垦,所以導(dǎo)致性能較差,并在阿里巴巴開發(fā)手冊上強(qiáng)制規(guī)定避免使用 Apache BeanUtils。

圖片

<figcaption style="margin: 5px 0px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 12px;">強(qiáng)制規(guī)定避免使用 Apache BeanUtils</figcaption>

至于剩下的重量級組件探越,綜合考慮其性能還有使用的易用性狡赐,我這里更推薦使用Orika。Orika底層采用了javassist類庫生成Bean映射的字節(jié)碼钦幔,之后直接加載執(zhí)行生成的字節(jié)碼文件枕屉,在速度上比使用反射進(jìn)行賦值會快很多。

“國外大神 baeldung 已經(jīng)對常見的組件性能進(jìn)行過詳細(xì)測試鲤氢,大家可以通過 https://www.baeldung.com/java-performance-mapping-frameworks 查看瘫筐〉凳澹”

Orika基本使用

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

  1. 引入依賴
<dependency>
  <groupId>ma.glasnost.orika</groupId>
  <artifactId>orika-core</artifactId>
  <version>1.5.4</version>
</dependency> 
  1. 構(gòu)造一個MapperFactory
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build(); 
  1. 注冊字段映射
mapperFactory.classMap(SourceClass.class, TargetClass.class)  
   .field("firstName", "givenName")
   .field("lastName", "sirName")
   .byDefault()
   .register();

當(dāng)字段名在兩個實體不一致時可以通過.field()方法進(jìn)行映射宁昭,如果字段名都一樣則可省略,byDefault()方法用于注冊名稱相同的屬性相种,如果不希望某個字段參與映射威恼,可以使用exclude方法。

  1. 進(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看到這里箫措,肯定有粉絲會說:你這推薦的啥玩意呀,這個Orika使用也不簡單呀食茎,每次都要這先創(chuàng)建MapperFactory,建立字段映射關(guān)系馏谨,才能進(jìn)行映射轉(zhuǎn)換别渔。別急,我這里給你準(zhǔn)備了一個工具類OrikaUtils惧互,你可以通過文末github倉庫獲取哎媚。它提供了五個公共方法:

圖片

分別對應(yīng):

  1. 字段一致實體轉(zhuǎn)換
  2. 字段不一致實體轉(zhuǎn)換(需要字段映射)
  3. 字段一致集合轉(zhuǎn)換
  4. 字段不一致集合轉(zhuǎn)換(需要字段映射)
  5. 字段屬性轉(zhuǎn)換注冊

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

Orika工具類使用文檔

先準(zhǔn)備兩個基礎(chǔ)實體類喊儡,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ǔ)實體映射

/**
 * 只拷貝相同的屬性
 */
@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)

此時由于屬性名不一致买喧,無法映射字段email。

TC2匆赃,實體映射 - 字段轉(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) </pre>

此時由于對字段做了映射,可以將email映射到emailAddress算柳。注意這里的refMap中key放置的是源實體的屬性低淡,而value放置的是目標(biāo)實體的屬性,不要弄反了。

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)]

此時由于屬性名不一致何荚,集合中無法映射字段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ù)據(jù)映射到實體中胁孙,如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涮较,類類型映射

有時候我們需要類類型對象映射稠鼻,如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) </pre>

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

這種場景稍微復(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);
  // 實體轉(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組件禁漓,可以很簡單實現(xiàn)這些功能。本文在Orika的基礎(chǔ)上封裝了工具類孵睬,進(jìn)一步簡化了Orika的操作播歼,希望對各位有所幫助。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末掰读,一起剝皮案震驚了整個濱河市秘狞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蹈集,老刑警劉巖谒撼,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異雾狈,居然都是意外死亡廓潜,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辩蛋,“玉大人呻畸,你說我怎么就攤上這事〉吭海” “怎么了伤为?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長据途。 經(jīng)常有香客問我绞愚,道長,這世上最難降的妖魔是什么颖医? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任位衩,我火速辦了婚禮,結(jié)果婚禮上熔萧,老公的妹妹穿的比我還像新娘糖驴。我一直安慰自己,他們只是感情好佛致,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布贮缕。 她就那樣靜靜地躺著,像睡著了一般俺榆。 火紅的嫁衣襯著肌膚如雪感昼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天罐脊,我揣著相機(jī)與錄音定嗓,去河邊找鬼。 笑死爹殊,一個胖子當(dāng)著我的面吹牛蜕乡,可吹牛的內(nèi)容都是我干的奸绷。 我是一名探鬼主播梗夸,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼号醉!你這毒婦竟也來了反症?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤畔派,失蹤者是張志新(化名)和其女友劉穎铅碍,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體线椰,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡胞谈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片烦绳。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡卿捎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出径密,到底是詐尸還是另有隱情午阵,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布享扔,位于F島的核電站底桂,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏惧眠。R本人自食惡果不足惜籽懦,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望锉试。 院中可真熱鬧猫十,春花似錦、人聲如沸呆盖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽应又。三九已至宙项,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間株扛,已是汗流浹背尤筐。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留洞就,地道東北人盆繁。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像旬蟋,于是被迫代替她去往敵國和親油昂。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 1倾贰、MyBatis簡介 MyBatis 是一款優(yōu)秀的持久層框架 中文官網(wǎng):https://mybatis.org/...
    CHeng_c0e9閱讀 393評論 0 0
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法冕碟,類相關(guān)的語法,內(nèi)部類的語法匆浙,繼承相關(guān)的語法安寺,異常的語法,線程的語...
    子非魚_t_閱讀 31,581評論 18 399
  • 一. Java基礎(chǔ)部分.................................................
    wy_sure閱讀 3,790評論 0 11
  • hibernate 面試題小集 Hibernate有哪幾種查詢數(shù)據(jù)的方式 3種:hql首尼、條件查詢QBC(Query...
    dbc94a66f502閱讀 230評論 0 0
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月挑庶,有人笑有人哭言秸,有人歡樂有人憂愁,有人驚喜有人失落迎捺,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,523評論 28 53