
作者 | 久賢
來源 | 阿裡技術公衆号
一 前言
随着系統子產品分層不斷細化,在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;
}
}
相關參考https://mapstruct.org/ https://github.com/arey/java-object-mapper-benchmark https://github.com/mapstruct/mapstruct-examples
技術公開課
Java進階程式設計
本課程共162課時,包含Java多線程程式設計、常用類庫、IO程式設計、網絡程式設計、類集架構、JDBC等實用開發技術,幫助同學們掌握系統提供的類庫并熟練使用JavaDoc文檔。同時考慮到對面向對象的了解以及常用類的設計模式,在課程講解中還将進行源代碼的使用分析與結構分析。
點選這裡,快去學習吧~