Java物件轉換與mapstruct實踐
1 前言
在日常開發中,我們經常需要給物件進行賦值,通常會呼叫其set/get方法,有些時候,如果我們要轉換的兩個物件之間屬性大致相同,會考慮使用屬性拷貝工具進行。
如我們經常在程式碼中會對一個數據結構封裝成 DO、PO、DTO、VO等,而這些Bean中的大部分屬性都是一樣的,所以使用屬性拷貝類工具可以幫助我們節省大量的set和get操作。
2 常用工具類
市面上有很多類似的工具類,比較常用的有如下圖:
那麼,我們到底應該選擇哪種工具類更加合適呢?為什麼阿里巴巴Java開發手冊中提到禁止使用Apache BeanUtils呢?
3 效能測試對比
在 Java 系統工程開發過程中,都會有各個層之間的物件轉換,比如 VO、DTO、PO、VO 等,而如果都是手動get、set
目前整理出用於物件屬性轉換有12種,包括:普通的getset、json2Json、Apache屬性拷貝、Spring屬性拷貝、bean-mapping、bean-mapping-asm、BeanCopier、Orika、Dozer、ModelMapper、JMapper、MapStruct 接下來我們分別測試這11種屬性轉換操作分別在一百次
、一千次
、一萬次
、十萬次
、一百萬次
時候的效能時間對比。
-
BeanUtils.copyProperties
是大家程式碼裡最常出現的工具類,但只要你不把它用錯成Apache
包下的,而是使用 Spring 提供的,就基本還不會對效能造成多大影響。 - 但如果說效能更好,可替代手動
get、set
的,還是MapStruct
更好用,因為它本身就是在編譯期生成get、set
程式碼,和我們寫get、set
一樣。 - 其他一些元件包主要基於
AOP
、ASM
、CGlib
,的技術手段實現的,所以也會有相應的效能損耗。
4 案例介紹
4.1 get\set
@Component public class GetSetAssembler implements IAssembler<UserVO, UserDTO> { @Override public UserDTO sourceToTarget(UserVO var) { UserDTO userDTO= new UserDTO(); userDTO.setUserId(var.getUserId()); userDTO.setUserNickName(var.getUserNickName()); userDTO.setCreateTime(var.getCreateTime()); return userDTO; } }
- 推薦:★★★☆☆
- 效能:★★★★★
- 手段:手寫
- 點評:其實這種方式也是日常使用的最多的,效能肯定是槓槓的,就是操作起來有點麻煩。尤其是一大堆屬性的 VO 物件轉換為 DTO 物件時候。但其實也有一些快捷的操作方式,比如你可以通過 Shift+Alt 選中所有屬性,Shift+Tab 歸併到一列,接下來在使用 Alt 選中這一列,批量操作貼上
userDTO.set
以及快捷鍵大寫屬性首字母,最後切換到結尾補充括號和分號,最終格式化一下就搞定了
4.2. fastjson
這種方案因為通過生成中間json格式字串,然後再轉化成目標物件,效能非常差,同時因為中間會生成json格式字串,如果轉化過多,gc會非常頻繁,同時針對複雜場景支援能力不足,基本很少用。
@Component public class Json2JsonAssembler implements IAssembler<UserVO, UserDTO> { @Override public UserDTO sourceToTarget(UserVO var) { String strJson = JSON.toJSONString(var); return JSON.parseObject(strJson, UserDTO.class); } }
- 推薦:☆☆☆☆☆
- 效能:★☆☆☆☆
- 手段:把物件轉JSON串,再把JSON轉另外一個物件
- 點評:這麼寫多半有點燒!
4.3 Apache copyProperties
BeanUtil.copyProperties()結合手寫get、set,對於簡單的轉換直接使用BeanUtil,複雜的轉換自己手工寫get、set。該方案的痛點就在於程式碼編寫效率低、冗餘繁雜還略顯醜陋,並且BeanUtil因為使用了反射invoke去賦值效能不高。只能適合bean數量較少、內容不多、轉換不頻繁的場景。
@Component public class ApacheCopyPropertiesAssembler implements IAssembler<UserVO, UserDTO> { @Override public UserDTO sourceToTarget(UserVO var) { UserDTO userDTO = new UserDTO(); try { BeanUtils.copyProperties(userDTO, var); } catch (IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } return userDTO; } }
- 推薦:☆☆☆☆☆
- 效能:★☆☆☆☆
- 手段:Introspector 機制獲取到類的屬性來進行賦值操作
- 點評:有坑,相容性交差,不建議使用
4.4 Spring copyProperties
這種方案針對apache的BeanUtils做了很多優化,整體效能提升不少,不過還是使用反射實現比不上原生程式碼處理,其次針對複雜場景支援能力不足。
@Component public class SpringCopyPropertiesAssembler implements IAssembler<UserVO, UserDTO> { @Override public UserDTO sourceToTarget(UserVO var) { UserDTO userDTO = new UserDTO(); BeanUtils.copyProperties(var, userDTO); return userDTO; } }
- 推薦:★★★☆☆
- 效能:★★★★☆
- 手段:Introspector機制獲取到類的屬性來進行賦值操作
- 點評:同樣是反射的屬性拷貝,Spring 提供的 copyProperties 要比 Apache 好用的多,只要你不用錯,基本不會有啥問題。
4.5 Bean Mapping
@Component public class BeanMappingAssembler implements IAssembler<UserVO, UserDTO> { @Override public UserDTO sourceToTarget(UserVO var) { UserDTO userDTO = new UserDTO(); BeanUtil.copyProperties(var, userDTO); return userDTO; } }
- 推薦:★★☆☆☆
- 效能:★★★☆☆
- 手段:屬性拷貝
- 點評:效能一般
4.6 Bean Mapping ASM
@Component public class BeanMappingAssembler implements IAssembler<UserVO, UserDTO> { @Override public UserDTO sourceToTarget(UserVO var) { UserDTO userDTO = new UserDTO(); BeanUtil.copyProperties(var, userDTO); return userDTO; } }
- 推薦:★★★☆☆
- 效能:★★★★☆
- 手段:基於ASM位元組碼框架實現
- 點評:與普通的 Bean Mapping 相比,效能有所提升,可以使用。
4.7 BeanCopier
這種方案動態生成一個要代理類的子類,其實就是通過位元組碼方式轉換成效能最好的get和set方式,重要的開銷在建立BeanCopier,整體效能接近原生程式碼處理,比BeanUtils要好很多,尤其在資料量很大時,但是針對複雜場景支援能力不足。
@Component public class BeanCopierAssembler implements IAssembler<UserVO, UserDTO> { @Override public UserDTO sourceToTarget(UserVO var) { UserDTO userDTO = new UserDTO(); BeanCopier beanCopier = BeanCopier.create(var.getClass(), userDTO.getClass(), false); beanCopier.copy(var, userDTO, null); return userDTO; } }
- 推薦:★★★☆☆
- 效能:★★★★☆
- 手段:基於CGlib位元組碼操作生成get、set方法
- 點評:整體效能很不錯,使用也不復雜,可以使用
4.8 Orika
@Component public class OrikaAssembler implements IAssembler<UserVO, UserDTO> { /** * 構造一個MapperFactory */ private static MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build(); static { mapperFactory.classMap(UserDTO.class, UserVO.class) .field("userId", "userId") // 欄位不一致時可以指定 .byDefault() .register(); } @Override public UserDTO sourceToTarget(UserVO var) { return mapperFactory.getMapperFacade().map(var, UserDTO.class); } }
- 官網:https://orika-mapper.github.io/orika-docs/
- 推薦:★★☆☆☆
- 效能:★★★☆☆
- 手段:基於位元組碼生成對映物件
- 點評:測試效能不是太突出,如果使用的話需要把 MapperFactory 的構建優化成 Bean 物件
4.9 Dozer
@Component public class DozerAssembler implements IAssembler<UserVO, UserDTO> { private static DozerBeanMapper mapper = new DozerBeanMapper(); @Override public UserDTO sourceToTarget(UserVO var) { return mapper.map(var, UserDTO.class); } }
- 官網:http://dozer.sourceforge.net/documentation/gettingstarted.html
- 推薦:★☆☆☆☆
- 效能:★★☆☆☆
- 手段:屬性對映框架,遞迴的方式複製物件
- 點評:效能有點差,不建議使用
4.10 ModelMapper
@Component public class ModelMapperAssembler implements IAssembler<UserVO, UserDTO> { private static ModelMapper modelMapper = new ModelMapper(); static { modelMapper.addMappings(new PropertyMap<UserVO, UserDTO>() { @Override protected void configure() { // 屬性值不一樣可以自己操作 map().setUserId(source.getUserId()); } }); } @Override public UserDTO sourceToTarget(UserVO var) { return modelMapper.map(var, UserDTO.class); } }
- 官網:http://modelmapper.org
- 推薦:★★★☆☆
- 效能:★★★☆☆
- 手段:基於ASM位元組碼實現
- 點評:轉換物件數量較少時效能不錯,如果同時大批量轉換物件,效能有所下降
4.11 JMapper
JMapper<UserDTO, UserVO> jMapper = new JMapper<>(UserDTO.class, UserVO.class, new JMapperAPI() .add(JMapperAPI.mappedClass(UserDTO.class) .add(JMapperAPI.attribute("userId") .value("userId")) .add(JMapperAPI.attribute("userNickName") .value("userNickName")) .add(JMapperAPI.attribute("createTime") .value("createTime")) ));
- 官網:https://github.com/jmapper-framework/jmapper-core/wiki
- 推薦:★★★★☆
- 效能:★★★★★
- 手段:Elegance, high performance and robustness all in one java bean mapper
- 點評:速度真心可以,不過結合 SpringBoot 感覺有的一點點麻煩,可能姿勢不對
4.12 MapStruct
maven依賴:
<properties> <org.mapstruct.version>1.3.1.Final</org.mapstruct.version> </properties> <dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build>
注意這裡配置一下lombok, 否則啟動會衝突。
程式碼實現:
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE, unmappedSourcePolicy = ReportingPolicy.IGNORE) public interface UserDTOMapping extends IMapping<UserVO, UserDTO> { /** 用於測試的單例 */ IMapping<UserVO, UserDTO> INSTANCE = Mappers.getMapper(UserDTOMapping.class); @Mapping(target = "userId", source = "userId") @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss") @Override UserDTO sourceToTarget(UserVO var1); @Mapping(target = "userId", source = "userId") @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss") @Override UserVO targetToSource(UserDTO var1); }
注意:
1:可以在Mapping類上加@Mapper(componentModel = "spring"), 通過注入的方式引入Mapper介面
2:可以通過Mappers.getMapper(UserDTOMapping.class) 的方式獲取Mapper介面
- 官網:https://github.com/mapstruct/mapstruct、ModelMapper - Getting Started
- 推薦:★★★★★
- 效能:★★★★★
- 手段:直接在編譯期生成對應的get、set,像手寫的程式碼一樣
- 點評:速度很快,不需要到執行期處理,結合到框架中使用方便
5 總結
- 其實物件屬性轉換的操作無非是基於反射、AOP、CGlib、ASM、Javassist 在編譯時和執行期進行處理,再有好的思路就是在編譯前生成出對應的get、set,就像手寫出來的一樣。
- 所以我更推薦我喜歡的 MapStruct,用起來還是比較舒服的,一種是來自於功能上的拓展性,易用性和相容性。
參考:
Java物件轉換方案分析與mapstruct實踐-阿里雲開發者社群 (aliyun.com)
MapStruct 1.4.2.Final Reference Guide
mapstruct/mapstruct-examples: Examples for using MapStruct (github.com)