Java對(duì)象轉(zhuǎn)換的方案分享
前言
系統(tǒng)變的復(fù)雜,系統(tǒng)的層次劃分越來(lái)越細(xì),邊界也越來(lái)越明確。 然后每一層之間一般都有自己要處理的領(lǐng)域?qū)ο?,統(tǒng)稱為pojo一般在model或者domain包下(類的后綴不能為pojo)。
常見(jiàn)的一些模型類型:
- PO、DO:持久層對(duì)象,一般和數(shù)據(jù)庫(kù)直接打交道。
- DTO:數(shù)據(jù)傳輸對(duì)象,系統(tǒng)之間的交互,再服務(wù)層提供服務(wù)的時(shí)候輸出到其它系統(tǒng)。
- VO:視圖對(duì)象,用于前端模型展示。 當(dāng)然有時(shí)候前端也可以看做另外一個(gè)系統(tǒng),使用DTO模型;
- BO:業(yè)務(wù)邏輯對(duì)象,比較少用...
為什么模型要分這么多層?
在復(fù)雜一點(diǎn)的業(yè)務(wù)中,業(yè)務(wù)建模是非常有必要的,一定要抽象出業(yè)務(wù)上常用的領(lǐng)域模型,統(tǒng)一技術(shù)和非技術(shù)同學(xué)的語(yǔ)言。
建完模型之后,在技術(shù)的系統(tǒng)中,為了方便維護(hù)代碼,分離關(guān)注點(diǎn),也會(huì)進(jìn)行再次分層,讓每一層解決特定的問(wèn)題。模型的分層是隨著系統(tǒng)的分層而來(lái)的;試想所有的模型屬性在一個(gè)對(duì)象中,這個(gè)對(duì)象你看的懂嗎?
舉個(gè)實(shí)際的案例:
- 數(shù)據(jù)層一般用DO
- 現(xiàn)在要透出數(shù)據(jù)給其他系統(tǒng),DO中一般都會(huì)有創(chuàng)建人,創(chuàng)建時(shí)間,修改人,修改時(shí)間,當(dāng)前對(duì)象所處的環(huán)境等信息; 但是外部的系統(tǒng)需要環(huán)境、創(chuàng)建人信息嗎? 很多時(shí)候不需要,站在數(shù)據(jù)安全的角度,一般只透出必要的字段就可以; 這些要輸出要外部系統(tǒng)的必要字段,一般定義在DTO中。
- 到前端系統(tǒng),前端系統(tǒng)展示上所需的邏輯和輸出到外部系統(tǒng)的又有點(diǎn)不太一樣,前端系統(tǒng)可能要?jiǎng)?chuàng)建人,創(chuàng)建時(shí)間,但是不要另外一些東西,或者一些敏感的配置不能透出給前端,這個(gè)時(shí)候一般也會(huì)再定義一個(gè)新的對(duì)象。
簡(jiǎn)單說(shuō)就是當(dāng)我們的系統(tǒng)要輸出能力到外部系統(tǒng)的時(shí)候,不同系統(tǒng)要的數(shù)據(jù)不一樣,數(shù)據(jù)安全要求我們不能透出這么多的數(shù)據(jù),一定要做一層處理。 另外給另外一個(gè)系統(tǒng)關(guān)注的數(shù)據(jù),而不是一股腦的全部都給對(duì)方,對(duì)方處理起來(lái)也方便。
模型之間的轉(zhuǎn)換
建議不要用的方式
- 手寫(xiě)get\set; 雖然性能高,但是費(fèi)勁并且眼花繚亂,一不小心就寫(xiě)錯(cuò)了,難以維護(hù),復(fù)用度不高
- BeanUtils,apacha和spring包下都有對(duì)應(yīng)的類,但是底層用到的都是反射,性能比較差,大流量的情況下一般不用
- 直接fastjson,gc會(huì)很頻繁,而且性能比較差
常用的方式
- cglib的beanCopier,開(kāi)銷在創(chuàng)建BeanCopier,一般在創(chuàng)建類的時(shí)候提前創(chuàng)建好一個(gè),在代碼運(yùn)行的時(shí)候直接進(jìn)行copy,性能接近原生。
- mapstruct 性能和原生代碼一樣,支持復(fù)雜的轉(zhuǎn)化場(chǎng)景,實(shí)現(xiàn)原理同lombok編譯的時(shí)候生成對(duì)應(yīng)的代碼。
以上從技術(shù)分類的角度來(lái)看:
- 反射:fastjson,beanutil 都不建議用
- get\set: beancoper通過(guò)字節(jié)碼進(jìn)行g(shù)etset,mapstruct編譯的時(shí)候生成getset。 性能相對(duì)較好。
使用方式
個(gè)人覺(jué)得,如果說(shuō)對(duì)象比較簡(jiǎn)單的時(shí)候,使用BeanCopier就可以了,因?yàn)閟pring的aop依賴cglib,默認(rèn)情況下就已經(jīng)引入了對(duì)應(yīng)的包了,不需要額外的依賴直接就可以用。
如果很復(fù)雜的模型之間的轉(zhuǎn)換,并且對(duì)性能有更極致的要求,考慮使用下MapStruct。
定義對(duì)象
UserDO
@Data
public class UserDO {
private Long id;
private String name;
private Integer gender;
private String password;
private Date gmtCreate;
private Date gmtModified;
}UserDTO
@Data
public class UserDTO {
private Long id;
private String name;
private Integer gender;
}BeanCopier
最簡(jiǎn)單的使用方式
BeanCopier beanCopier = BeanCopier.create(UserDO.class, UserDTO.class, false); bean.copy即可;
private static void simpleBeanCopy() {
BeanCopier beanCopier = BeanCopier.create(UserDO.class, UserDTO.class, false);
UserDO userDO = new UserDO();
userDO.setId(1L);
userDO.setName("aihe");
userDO.setGmtCreate(new Date());
userDO.setGender(0);
userDO.setPassword("xxxxxx");
UserDTO userDTO = new UserDTO();
beanCopier.copy(userDO, userDTO,null);
Assert.assertEquals("名稱未成功拷貝",userDTO.getName(),"aihe");
Assert.assertEquals("Id未成功拷貝", 1L, (long)userDTO.getId());
Assert.assertEquals("性別未成功拷貝", Integer.valueOf(0),userDTO.getGender());
}創(chuàng)建可復(fù)用的BeanCopier工具類
package me.aihe.daka;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import net.sf.cglib.beans.BeanCopier;
/**
* @author : aihe
* @date : 2022/9/12 9:21 AM
* 使用場(chǎng)景:
* 功能描述:
*/
public class BeanCopyUtils {
/**
* beanCopier緩存
* 由sourceClass和targetClass可以確定一個(gè)唯一的BeanCoper,因此使用二級(jí)Map;
*/
private static Map<Class<?>, Map<Class<?>, BeanCopier>> beanCopierMap = new ConcurrentHashMap<>();
/**
* 直接指定Bean對(duì)象進(jìn)行拷貝
* @param sourceBean
* @param targetBean
* @param <S>
* @param <T>
*/
public static <S,T> void copy(S sourceBean,T targetBean){
@SuppressWarnings("unchecked")
Class<S> sourceClass = (Class<S>) sourceBean.getClass();
@SuppressWarnings("unchecked")
Class<T> targetClass = (Class<T>) targetBean.getClass();
BeanCopier beanCopier = getBeanCopier(sourceClass,targetClass);
beanCopier.copy(sourceBean,targetBean,null);
}
/**
* 轉(zhuǎn)換方法
* @param sourceBean 原對(duì)象
* @param targetClass 目標(biāo)類
* @param <S>
* @param <T>
* @return
*/
public static <S,T> T convert(S sourceBean,Class<T> targetClass){
try {
assert sourceBean!=null;
T targetBean = targetClass.newInstance();
copy(sourceBean,targetBean);
return targetBean;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static <S,T> BeanCopier getBeanCopier(Class<S> sourceClass, Class<T> targetClass ){
Map<Class<?>,BeanCopier> map = beanCopierMap.get(sourceClass);
if(map == null || map.isEmpty()){
BeanCopier newBeanCopier = BeanCopier.create(sourceClass, targetClass, false);
Map<Class<?>,BeanCopier> newMap = new ConcurrentHashMap<>();
newMap.put(targetClass,newBeanCopier);
beanCopierMap.put(sourceClass,newMap);
return newBeanCopier;
}
BeanCopier beanCopier = map.get(targetClass);
if(beanCopier == null){
BeanCopier newBeanCopier = BeanCopier.create(sourceClass, targetClass, false);
map.put(targetClass,newBeanCopier);
return newBeanCopier;
}
return beanCopier;
}
}同上:
UserDO userDO = new UserDO();
userDO.setId(1L);
userDO.setName("aihe");
userDO.setGmtCreate(new Date());
userDO.setGender(0);
userDO.setPassword("xxxxxx");
UserDTO userDTO = new UserDTO();
BeanCopyUtils.copy(userDO, userDTO);
Assert.assertEquals("名稱未成功拷貝",userDTO.getName(),"aihe");
Assert.assertEquals("Id未成功拷貝", 1L, (long)userDTO.getId());
Assert.assertEquals("性別未成功拷貝", Integer.valueOf(0),userDTO.getGender());MapStruct
引入mapstruct
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.20</org.projectlombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
<!-- IntelliJ does not pick up the processor if it is not in the dependencies.
There is already an open issue for IntelliJ see https://youtrack.jetbrains.com/issue/IDEA-150621
-->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>簡(jiǎn)單Demo
定義Mapper
@Mapper
public interface UserDTOMapper {
UserDTOMapper MAPPER = Mappers.getMapper( UserDTOMapper.class );
//@Mapping( source = "test", target = "testing" )
//@Mapping( source = "test1", target = "testing2" )
UserDTO toTarget( UserDO s );
}使用:
public static void main( String[] args ) {
//simpleDemo();
UserDO userDO = new UserDO();
userDO.setId(1L);
userDO.setName("aihe");
userDO.setGmtCreate(new Date());
userDO.setGender(0);
userDO.setPassword("xxxxxx");
UserDTO userDTO = UserDTOMapper.MAPPER.toTarget(userDO);
Assert.assertEquals("名稱未成功拷貝",userDTO.getName(),"aihe");
Assert.assertEquals("Id未成功拷貝", 1L, (long)userDTO.getId());
Assert.assertEquals("性別未成功拷貝", Integer.valueOf(0),userDTO.getGender());
}常見(jiàn)用法
- 屬性類型相同,名稱不同的時(shí)候,使用@Mapping注解指定source和target字段名稱對(duì)應(yīng)關(guān)系, 如果有多個(gè)這種屬性,那就指定多個(gè)@Mapping注解。
- 忽略某個(gè)字段,在@Mapping的時(shí)候,加上ignore = true
- 轉(zhuǎn)化日期格式,字符串到數(shù)字的格式,可以使用dateFormat,numberFormat
- 如果有自定義轉(zhuǎn)換的需求,寫(xiě)一個(gè)簡(jiǎn)單的Java類即可,然后在方法上打上Mapstruct的注解@Named,在在@Mapper(uses = 自定義的類),然后@Mapping中用上qualifiedByName。
@Mapping(target = "userNick1", source = "userNick") @Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd") @Mapping(target = "age", source = "age", numberFormat = "#0.00") @Mapping(target = "id", ignore = true) @Mapping(target = "userVerified", defaultValue = "defaultValue-2") UserDTO toTarget( UserDO s );
性能測(cè)試
測(cè)試代碼
import java.util.Date;
import com.alibaba.fastjson.JSON;
import org.junit.Before;
import org.junit.Test;
/**
* @author : aihe aihe.ah@alibaba-inc.com
* @date : 2022/9/12 9:47 AM
* 使用場(chǎng)景:
* 功能描述:
*/
public class BenchDemoTest{
/**
* 轉(zhuǎn)化對(duì)象
*/
private UserDO userDO;
/**
* 轉(zhuǎn)化次數(shù)
*/
private final static int count = 1000000;
@Before
public void before() {
userDO = new UserDO();
userDO.setId(1L);
userDO.setName("aihe");
userDO.setGmtCreate(new Date());
userDO.setGender(0);
userDO.setPassword("xxxxxx");
}
@Test
public void mapstruct() {
long startTime = System.currentTimeMillis();
for (int i = 1; i <=count; i++) {
UserDTO userDTO = UserDTOMapper.MAPPER.toTarget(userDO);
}
System.out.println("mapstruct time" + (System.currentTimeMillis() - startTime));
}
@Test
public void beanCopier() {
long startTime = System.currentTimeMillis();
for (int i = 1; i <= count; i++) {
UserDTO targetBean = new UserDTO();
BeanCopyUtils.copy(userDO, targetBean);
}
System.out.println("beanCopier time" + (System.currentTimeMillis() - startTime));
}
@Test
public void springBeanUtils(){
long startTime = System.currentTimeMillis();
for (int i = 1; i <=count; i++) {
UserDTO userDTO = new UserDTO();
org.springframework.beans.BeanUtils.copyProperties(userDO, userDTO);
}
System.out.println("springBeanUtils time" + (System.currentTimeMillis() - startTime));
}
@Test
public void fastjson() {
long startTime = System.currentTimeMillis();
for (int i = 1; i <= count; i++) {
JSON.parseObject(JSON.toJSONString(userDO), UserDTO.class);
}
System.out.println("fastjson time" + (System.currentTimeMillis() - startTime));
}
}測(cè)試結(jié)果

- 可以看出BeanCopier和MapStruct是遠(yuǎn)遠(yuǎn)超過(guò)其他轉(zhuǎn)換方式的...
- BeanCopier雖然快,但是比mapstruct還是有20倍的性能差距...
最后
總結(jié)下本文的內(nèi)容:
- 軟件系統(tǒng)一般都會(huì)進(jìn)行分層,領(lǐng)域模型也會(huì)隨之進(jìn)行分層,即每層都有自己關(guān)注的模型對(duì)象; 分層的主要原因是便于維護(hù)。
- 模型之間的對(duì)象經(jīng)常要互相轉(zhuǎn)換,常用的轉(zhuǎn)換實(shí)現(xiàn)有反射和get/set,反射的性能很差不建議使用
- 然后寫(xiě)了基于get/set實(shí)現(xiàn)的beancopier和mapstruct使用方式,簡(jiǎn)單測(cè)試了下性能,mapstrcut優(yōu)于其它各種對(duì)象轉(zhuǎn)換方式。并且MapStrcut支持功能更加復(fù)雜的對(duì)象轉(zhuǎn)換。 性能又好,功能又強(qiáng)大,所以可以考慮優(yōu)先使用.
到此這篇關(guān)于Java對(duì)象轉(zhuǎn)換的方案分享的文章就介紹到這了,更多相關(guān)Java對(duì)象轉(zhuǎn)換內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 如何將Java對(duì)象轉(zhuǎn)換為JSON實(shí)例詳解
- Java對(duì)象和Json文本轉(zhuǎn)換工具類的實(shí)現(xiàn)
- Java如何將String轉(zhuǎn)換成json對(duì)象或json數(shù)組
- sqlserver和java將resultSet中的記錄轉(zhuǎn)換為學(xué)生對(duì)象
- 復(fù)雜JSON字符串轉(zhuǎn)換為Java嵌套對(duì)象的實(shí)現(xiàn)
- Java將json對(duì)象轉(zhuǎn)換為map鍵值對(duì)案例詳解
- Java BeanMap實(shí)現(xiàn)Bean與Map的相互轉(zhuǎn)換
相關(guān)文章
SpringBoot3實(shí)現(xiàn)Gzip壓縮優(yōu)化的技術(shù)指南
隨著 Web 應(yīng)用的用戶量和數(shù)據(jù)量增加,網(wǎng)絡(luò)帶寬和頁(yè)面加載速度逐漸成為瓶頸,為了減少數(shù)據(jù)傳輸量,提高用戶體驗(yàn),我們可以使用 Gzip 壓縮 HTTP 響應(yīng),本文將介紹如何在 Spring Boot 3 中實(shí)現(xiàn) Gzip 壓縮優(yōu)化,需要的朋友可以參考下2025-04-04
Spring Boot 中該如何防御計(jì)時(shí)攻擊
這篇文章主要介紹了Spring Boot 中該如何防御計(jì)時(shí)攻擊,幫助大家更好的使用spring boot框架,感興趣的朋友可以了解下2020-09-09
利用Spring Data MongoDB持久化文檔數(shù)據(jù)的方法教程
這篇文章主要給大家介紹了關(guān)于利用Spring Data MongoDB持久化文檔數(shù)據(jù)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-08-08
SpringBoot + thymeleaf 實(shí)現(xiàn)讀取視頻列表并播放視頻功能
這篇文章主要介紹了SpringBoot + thymeleaf 實(shí)現(xiàn)讀取視頻列表并播放視頻功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04
elasticsearch索引index之engine讀寫(xiě)控制結(jié)構(gòu)實(shí)現(xiàn)
這篇文章主要為大家介紹了elasticsearch索引index之engine讀寫(xiě)控制結(jié)構(gòu)實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04
使用@DS輕松解決動(dòng)態(tài)數(shù)據(jù)源的問(wèn)題
這篇文章主要介紹了使用@DS輕松解決動(dòng)態(tài)數(shù)據(jù)源的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05
關(guān)于@Autowierd && @Resource 你真的了解嗎
這篇文章主要介紹了關(guān)于@Autowierd && @Resource的具體使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08

