系統變的複雜,系統的層次劃分越來越細,邊界也越來越明确。 然後每一層之間一般都有自己要處理的領域對象,統稱為pojo一般在model或者domain包下(類的字尾不能為pojo)。
常見的一些模型類型:
- PO、DO:持久層對象,一般和資料庫直接打交道。
- DTO:資料傳輸對象,系統之間的互動,再服務層提供服務的時候輸出到其它系統。
- VO:視圖對象,用于前端模型展示。 當然有時候前端也可以看做另外一個系統,使用DTO模型;
- BO:業務邏輯對象,比較少用...
為什麼模型要分這麼多層?
在複雜一點的業務中,業務模組化是非常有必要的,一定要抽象出業務上常用的領域模型,統一技術和非技術同學的語言。
建完模型之後,在技術的系統中,為了友善維護代碼,分離關注點,也會進行再次分層,讓每一層解決特定的問題。模型的分層是随着系統的分層而來的;試想所有的模型屬性在一個對象中,這個對象你看的懂嗎?
舉個實際的案例:
- 資料層一般用DO
- 現在要透出資料給其他系統,DO中一般都會有建立人,建立時間,修改人,修改時間,目前對象所處的環境等資訊; 但是外部的系統需要環境、建立人資訊嗎? 很多時候不需要,站在資料安全的角度,一般隻透出必要的字段就可以; 這些要輸出要外部系統的必要字段,一般定義在DTO中。
- 到前端系統,前端系統展示上所需的邏輯和輸出到外部系統的又有點不太一樣,前端系統可能要建立人,建立時間,但是不要另外一些東西,或者一些敏感的配置不能透出給前端,這個時候一般也會再定義一個新的對象。
簡單說就是當我們的系統要輸出能力到外部系統的時候,不同系統要的資料不一樣,資料安全要求我們不能透出這麼多的資料,一定要做一層處理。 另外給另外一個系統關注的資料,而不是一股腦的全部都給對方,對方處理起來也友善。
模型之間的轉換
建議不要用的方式
- 手寫get\set; 雖然性能高,但是費勁并且眼花缭亂,一不小心就寫錯了,難以維護,複用度不高
- BeanUtils,apacha和spring包下都有對應的類,但是底層用到的都是反射,性能比較差,大流量的情況下一般不用
- 直接fastjson,gc會很頻繁,而且性能比較差
常用的方式
- cglib的beanCopier,開銷在建立BeanCopier,一般在建立類的時候提前建立好一個,在代碼運作的時候直接進行copy,性能接近原生。
- mapstruct 性能和原生代碼一樣,支援複雜的轉化場景,實作原理同lombok編譯的時候生成對應的代碼。
以上從技術分類的角度來看:
- 反射:fastjson,beanutil 都不建議用
- get\set: beancoper通過位元組碼進行getset,mapstruct編譯的時候生成getset。 性能相對較好。
使用方式
個人覺得,如果說對象比較簡單的時候,使用BeanCopier就可以了,因為spring的aop依賴cglib,預設情況下就已經引入了對應的包了,不需要額外的依賴直接就可以用。
如果很複雜的模型之間的轉換,并且對性能有更極緻的要求,考慮使用下MapStruct。
定義對象
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
最簡單的使用方式
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());
}
複制代碼
建立可複用的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
* 使用場景:
* 功能描述:
*/
public class BeanCopyUtils {
/**
* beanCopier緩存
* 由sourceClass和targetClass可以确定一個唯一的BeanCoper,是以使用二級Map;
*/
private static Map<Class<?>, Map<Class<?>, BeanCopier>> beanCopierMap = new ConcurrentHashMap<>();
/**
* 直接指定Bean對象進行拷貝
* @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);
}
/**
* 轉換方法
* @param sourceBean 原對象
* @param targetClass 目标類
* @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
案例集:github.com/mapstruct/m…
引入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>
複制代碼
簡單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());
}
複制代碼
常見用法
- 屬性類型相同,名稱不同的時候,使用@Mapping注解指定source和target字段名稱對應關系, 如果有多個這種屬性,那就指定多個@Mapping注解。
- 忽略某個字段,在@Mapping的時候,加上ignore = true
- 轉化日期格式,字元串到數字的格式,可以使用dateFormat,numberFormat
- 如果有自定義轉換的需求,寫一個簡單的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 );
複制代碼
性能測試
測試代碼
import java.util.Date;
import com.alibaba.fastjson.JSON;
import org.junit.Before;
import org.junit.Test;
/**
* @author : aihe [email protected]
* @date : 2022/9/12 9:47 AM
* 使用場景:
* 功能描述:
*/
public class BenchDemoTest{
/**
* 轉化對象
*/
private UserDO userDO;
/**
* 轉化次數
*/
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));
}
}
複制代碼
測試結果
- 可以看出BeanCopier和MapStruct是遠遠超過其他轉換方式的...
- BeanCopier雖然快,但是比mapstruct還是有20倍的性能差距...
最後
總結下本文的内容:
- 軟體系統一般都會進行分層,領域模型也會随之進行分層,即每層都有自己關注的模型對象; 分層的主要原因是便于維護。
- 模型之間的對象經常要互相轉換,常用的轉換實作有反射和get/set,反射的性能很差不建議使用
- 然後寫了基于get/set實作的beancopier和mapstruct使用方式,簡單測試了下性能,mapstrcut優于其它各種對象轉換方式。并且MapStrcut支援功能更加複雜的對象轉換。 性能又好,功能又強大,是以可以考慮優先使用.
參考
- MapStrcut官網:mapstruct.org/documentati…
- MapStrcut案例集:github.com/mapstruct/m…
- 告别BeanUtils,Mapstruct從入門到精通 :mp.weixin.qq.com/s/8yDzCzLB-…
來源:https://juejin.cn/post/7142640678680395813