簡介: 隨着系統模塊分層不斷細化,在Java日常開發中不可避免地涉及到各種對象的轉換,如:DO、DTO、VO等等,編寫映射轉換代碼是一個繁瑣重復且還易錯的工作,一個好的工具輔助,減輕了工作量、提升開發工作效率的同時還能減少bug的發生
作者 | 久賢
來源 | 阿里技術公眾號
一 前言
隨着系統模塊分層不斷細化,在Java日常開發中不可避免地涉及到各種對象的轉換,如:DO、DTO、VO等等,編寫映射轉換代碼是一個繁瑣重復且還易錯的工作,一個好的工具輔助,減輕了工作量、提升開發工作效率的同時還能減少bug的發生。
二 常用方案及分析
1 fastjson
CarDTO entity = JSON.parseObject(JSON.toJSONString(carDO), CarDTO.class);
這種方案因為通過生成中間json格式字符串,然后再轉化成目標對象,性能非常差,同時因為中間會生成json格式字符串,如果轉化過多,gc會非常頻繁,同時針對復雜場景支持能力不足,基本很少用。
2 BeanUtil類
BeanUtil.copyProperties()結合手寫get、set,對於簡單的轉換直接使用BeanUtil,復雜的轉換自己手工寫get、set。該方案的痛點就在於代碼編寫效率低、冗余繁雜還略顯丑陋,並且BeanUtil因為使用了反射invoke去賦值性能不高。
只能適合bean數量較少、內容不多、轉換不頻繁的場景。
apache.BeanUtils
org.apache.commons.beanutils.BeanUtils.copyProperties(do, entity);
這種方案因為用到反射的原因,同時本身設計問題,性能比較差。集團開發規約明確規定禁止使用。
spring.BeanUtils
org.springframework.beans.BeanUtils.copyProperties(do, entity);
這種方案針對apache的BeanUtils做了很多優化,整體性能提升不少,不過還是使用反射實現比不上原生代碼處理,其次針對復雜場景支持能力不足。
3 beanCopier
BeanCopier copier = BeanCopier.create(CarDO.class, CarDTO.class, false); copier.copy(do, dto, null);
這種方案動態生成一個要代理類的子類,其實就是通過字節碼方式轉換成性能最好的get和set方式,重要的開銷在創建BeanCopier,整體性能接近原生代碼處理,比BeanUtils要好很多,尤其在數據量很大時,但是針對復雜場景支持能力不足。
4 各種Mapping框架
分類
Object Mapping 技術從大的角度來說分為兩類,一類是運行期轉換,另一類則是編譯期轉換:
- 運行期反射調用 set/get 或者是直接對成員變量賦值。這種方式通過invoke執行賦值,實現時一般會采用beanutil, Javassist等開源庫。運行期對象轉換的代表主要是Dozer和ModelMaper。
- 編譯期動態生成 set/get 代碼的class文件,在運行時直接調用該class的 set/get 方法。該方式實際上仍會存在 set/get 代碼,只是不需要開發人員自己寫了。這類的代表是:MapStruct,Selma,Orika。
分析
- 無論哪種Mapping框架,基本都是采用xml配置文件 or 注解的方式供用戶配置,然后生成映射關系。
- 編譯期生成class文件方式需要DTO仍然有set/get方法,只是調用被屏蔽;而運行期反射方式在某些直接填充 field的方案中,set/get代碼也可以省略。
- 編譯期生成class方式會有源代碼在本地,方便排查問題。
- 編譯期生成class方式因為在編譯期才出現java和class文件,所以熱部署會受到一定影響。
- 反射型由於很多內容是黑盒,在排查問題時,不如編譯期生成class方式方便。參考GitHub上工程java-object-mapper-benchmark可以看出主要框架性能比較。
- 反射型調用由於是在運行期根據映射關系反射執行,其執行速度會明顯下降N個量級。
- 通過編譯期生成class代碼的方式,本質跟直接寫代碼區別不大,但由於代碼都是靠模板生成,所以代碼質量沒有手工寫那么高,這也會造成一定的性能損失。
綜合性能、成熟度、易用性、擴展性,mapstruct是比較優秀的一個框架。
三 Mapstruct使用指南
1 Maven引入
2 簡單入門案例
DO和DTO
這里用到了lombok簡化代碼,lombok的原理也是在編譯時去生成get、set等被簡化的代碼。
@Data
public class Car { private String make; private int numberOfSeats; private CarType type; } @Data public class CarDTO { private String make; private int seatCount; private String type; }
定義Mapper
@Mapper中描述映射,在編輯的時候mapstruct將會根據此描述生成實現類:
- 當屬性與其目標實體副本同名時,它將被隱式映射。
- 當目標實體中的屬性具有不同名稱時,可以通過@Mapping注釋指定其名稱。
@Mapper public interface CarMapper { @Mapping(source = "numberOfSeats", target = "seatCount") CarDTO CarToCarDTO(Car car); }
使用Mapper
通過Mappers 工廠生成靜態實例使用。
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "numberOfSeats", target = "seatCount") CarDTO CarToCarDTO(Car car); }
Car car = new Car(...); CarDTO carDTO = CarMapper.INSTANCE.CarToCarDTO(car);
getMapper會去load接口的Impl后綴的實現類。
通過生成spring bean注入使用,Mapper注解加上spring配置,會自動生成一個bean,直接使用bean注入即可訪問。
@Mapper(componentModel = "spring") public interface CarMapper { @Mapping(source = "numberOfSeats", target = "seatCount") CarDTO CarToCarDTO(Car car); }
自動生成的MapperImpl內容
如果配置了spring bean訪問會在注解上自動加上@Component。
3 進階使用
逆向映射
如果是雙向映射,例如 從DO到DTO以及從DTO到DO,正向方法和反向方法的映射規則通常是相似的,並且可以通過切換源和目標來簡單地逆轉。
使用注解@InheritInverseConfiguration 指示方法應繼承相應反向方法的反向配置。
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "numberOfSeats", target = "seatCount") CarDTO CarToCarDTO(Car car); @InheritInverseConfiguration Car CarDTOToCar(CarDTO carDTO); }
更新bean映射
有些情況下不需要映射轉換產生新的bean,而是更新已有的bean。
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "numberOfSeats", target = "seatCount") void updateDTOFromCar(Car car, @MappingTarget CarDTO carDTO);
集合映射
集合類型(List,Set,Map等)的映射以與映射bean類型相同的方式完成,即通過在映射器接口中定義具有所需源類型和目標類型的映射方法。MapStruct支持Java Collection Framework中的多種可迭代類型。
生成的代碼將包含一個循環,該循環遍歷源集合,轉換每個元素並將其放入目標集合。如果在給定的映射器或其使用的映射器中找到用於集合元素類型的映射方法,則將調用此方法以執行元素轉換,如果存在針對源元素類型和目標元素類型的隱式轉換,則將調用此轉換。
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "numberOfSeats", target = "seatCount") CarDTO CarToCarDTO(Car car); List<CarDTO> carsToCarDtos(List<Car> cars); Set<String> integerSetToStringSet(Set<Integer> integers); @MapMapping(valueDateFormat = "dd.MM.yyyy") Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source); }
編譯時生成的實現類:
多個源參數映射
MapStruct 還支持具有多個源參數的映射方法。例如,將多個實體組合成一個數據傳輸對象。
在原案例新增一個Person對象,CarDTO中新增driverName屬性,根據Person對象獲得。
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "car.numberOfSeats", target = "seatCount") @Mapping(source = "person.name", target = "driverName") CarDTO CarToCarDTO(Car car, Person person); }
編譯生成的代碼:
默認值和常量映射
如果相應的源屬性是null ,則可以指定默認值以將預定義值設置為目標屬性。在任何情況下,都可以指定常量來設置這樣的預定義值。默認值和常量被指定為字符串值。當目標類型是原始類型或裝箱類型時,String 值將采用字面量,在這種情況下允許位/八進制/十進制/十六進制模式,只要它們是有效的文字即可。在所有其他情況下,常量或默認值會通過內置轉換或調用其他映射方法進行類型轉換,以匹配目標屬性所需的類型。
@Mapper
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class ); @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined") @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1") @Mapping(target = "stringConstant", constant = "Constant Value") @Mapping(target = "integerConstant", constant = "14") @Mapping(target = "longWrapperConstant", constant = "3001") @Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014") @Mapping(target = "stringListConstants", constant = "jack-jill-tom") Target sourceToTarget(Source s); }
自定義映射方法或映射器
在某些情況下,可能需要手動實現 MapStruct 無法生成的從一種類型到另一種類型的特定映射。
可以在Mapper中定義默認實現方法,生成轉換代碼將調用相關方法:
@Mapper public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "numberOfSeats", target = "seatCount") @Mapping(source = "length", target = "lengthType") CarDTO CarToCarDTO(Car car); default String getLengthType(int length) { if (length > 5) { return "large"; } else { return "small"; } } }
也可以定義其他映射器,如下案例Car中Date需要轉換成DTO中的String:
public class DateMapper { public String asString(Date date) { return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).format( date ) : null; } public Date asDate(String date) { try { return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).parse( date ) : null; } catch ( ParseException e ) { throw new RuntimeException( e ); } } }
@Mapper(uses = DateMapper.class) public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "numberOfSeats", target = "seatCount") CarDTO CarToCarDTO(Car car); }
編譯生成的代碼:
若遇到多個類似的方法調用時會出現模棱兩可,需使用@qualifiedBy指定:
@Mapper public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "numberOfSeats", target = "seatCount") @Mapping(source = "length", target = "lengthType", qualifiedByName = "newStandard") CarDTO CarToCarDTO(Car car); @Named("oldStandard") default String getLengthType(int length) { if (length > 5) { return "large"; } else { return "small"; } } @Named("newStandard") default String getLengthType2(int length) { if (length > 7) { return "large"; } else { return "small"; } } }
表達式自定義映射
通過表達式,可以包含來自多種語言的結構。
目前僅支持 Java 作為語言。例如,此功能可用於調用構造函數,整個源對象都可以在表達式中使用。應注意僅插入有效的 Java 代碼:MapStruct 不會在生成時驗證表達式,但在編譯期間生成的類中會顯示錯誤。
@Data @AllArgsConstructor public class Driver { private String name; private int age; }
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "car.numberOfSeats", target = "seatCount") @Mapping(target = "driver", expression = "java( new com.alibaba.my.mapstruct.example4.beans.Driver(person.getName(), person.getAge()))") CarDTO CarToCarDTO(Car car, Person person); }
默認表達式是默認值和表達式的組合:
@Mapper( imports = UUID.class ) public interface SourceTargetMapper { SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class ); @Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )") Target sourceToTarget(Source s); }
裝飾器自定義映射
在某些情況下,可能需要自定義生成的映射方法,例如在目標對象中設置無法由生成的方法實現設置的附加屬性。
實現起來也很簡單,用裝飾器模式實現映射器的一個抽象類,在映射器Mapper中添加注解@DecoratedWith指向裝飾器類,使用時還是正常調用。
@Mapper
@DecoratedWith(CarMapperDecorator.class) public interface CarMapper { CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); @Mapping(source = "numberOfSeats", target = "seatCount") CarDTO CarToCarDTO(Car car); }
public abstract class CarMapperDecorator implements CarMapper { private final CarMapper delegate; protected CarMapperDecorator(CarMapper delegate) { this.delegate = delegate; } @Override public CarDTO CarToCarDTO(Car car) { CarDTO dto = delegate.CarToCarDTO(car); dto.setMakeInfo(car.getMake() + " " + new SimpleDateFormat( "yyyy-MM-dd" ).format(car.getCreateDate())); return dto; } }
原文鏈接
本文為阿里雲原創內容,未經允許不得轉載。