1.對(duì)象屬性映射的苦惱
在日常開發(fā)中,常常涉及到接收Request對(duì)象,屬性映射到內(nèi)部交互的VO對(duì)象、甚至需要進(jìn)一步映射到DTO對(duì)象,以完成相關(guān)的業(yè)務(wù)邏輯。
舉個(gè)最近的栗子,接收的業(yè)務(wù)請(qǐng)求對(duì)象是這樣:
@Data
@ApiModel(description = "配置信息請(qǐng)求體")
public class TrackingDataConfigRequest {
@NotBlank(message = "dimension不能為空,可選值:PROJECT、TENANT、BRAND、VEHICLE_SERIE、VEHICLE_MODEL、EVENT、VIN")
@ApiModelProperty(value = "dimension", required = true)
String dimension;
@ApiModelProperty(value = "brand", required = false)
String brand;
@ApiModelProperty(value = "vehicleSeries", required = false)
String vehicleSeries;
@ApiModelProperty(value = "vehicleModel", required = false)
String vehicleModel;
@ApiModelProperty(value = "eventId", required = false)
String eventId;
@ApiModelProperty(value = "vin", required = false)
String vin;
@ApiModelProperty(value = "attrs", required = false)
List<String> attrs;
@ApiModelProperty(value = "switch", required = false)
String switchState;
@ApiModelProperty(value = "reportFrequency", required = false)
Integer reportFrequency;
@ApiModelProperty(value = "reportRecords", required = false)
Integer reportRecords;
@ApiModelProperty(value = "reportSize", required = false)
Integer reportSize;
}
請(qǐng)求體的非@NotBlank注解的字段為非必填(可能有值、也可以沒有)。接口接收到請(qǐng)求參數(shù)后,需要轉(zhuǎn)為如下內(nèi)部交互VO結(jié)構(gòu):
public class TrackingDataConfigVo {
String projectId;
String tenantId;
String dimension;
String brand;
String vehicleSeries;
String vehicleModel;
String eventId;
String vin;
List<String> attrs;
String switchState;
Integer reportFrequency;
Integer reportRecords;
Integer reportSize;
}
要實(shí)現(xiàn)這個(gè)映射有很多辦法,比如:
- 最直接地,一個(gè)個(gè)請(qǐng)求體對(duì)象判斷有值、設(shè)置到VO對(duì)象。更友好點(diǎn), 針對(duì)VO對(duì)象提供Builder,實(shí)現(xiàn)鏈?zhǔn)綄傩再x值操作、最后build出對(duì)象
- 使用BeanUtils這樣的工具類,比如
org.springframework.beans.BeanUtils,還有Apache的org.apache.commons.beanutils.BeanUtilsBean。
痛點(diǎn)就不必贅述了,BeanUtils工具類的主要問題是只能針對(duì)映射對(duì)象、被映射對(duì)象同名同類型屬性進(jìn)行映射,但是實(shí)際開發(fā)場(chǎng)景還有很多屬性名稱不同、類型不同、屬性默認(rèn)值、屬性需要按規(guī)則生成的場(chǎng)景,導(dǎo)致BeanUtils工具類失效。
2.MapStruct實(shí)現(xiàn)對(duì)象屬性映射
就以文章開頭提到的兩個(gè)對(duì)象映射為例,大致需要如下操作。
2.1引入pom依賴
<dependencies>
<!-- …… -->
<dependency>
<!-- jdk8以下就使用mapstruct -->
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
<plugins>
<!-- …… -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombook.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<compilerArg>
-Amapstruct.suppressGeneratorTimestamp=true
</compilerArg>
<compilerArg>
-Amapstruct.suppressGeneratorVersionInfoComment=true
</compilerArg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
mapstruct版本項(xiàng)目中使用的1.3.1.Final,最新的版本查了下應(yīng)該是1.4.1.Final,沒仔細(xì)看區(qū)別。
2.2 建立屬性映射關(guān)系
需要新建一個(gè)接口,指明映射對(duì)象與被映射對(duì)象,如果有字段需要特殊的映射規(guī)則,可以在轉(zhuǎn)換方法配置,比如目標(biāo)映射屬性與源屬性名稱不同、屬性類型不同等等。
@Mapper(componentModel = "spring")
public interface GeneralBurrypointConfigConvertor {
/**
* 完成請(qǐng)求TrackingDataConfigRequest到TrackingDataConfigVo屬性映射(projectId等參數(shù)無法映射)
*
* @param request
* @return
*/
@Mappings({
@Mapping(target = "projectId", defaultValue = ""),
@Mapping(target = "tenantId", defaultValue = "")})
TrackingDataConfigVo convertVo(TrackingDataConfigRequest request);
}
簡單說明下:
-
componentModel = "spring"是因?yàn)槭褂肧pring構(gòu)建的項(xiàng)目,這里配置是指該接口生成的實(shí)現(xiàn)類上面會(huì)自動(dòng)添加一個(gè)@Component注解,可以通過Spring的@Autowired方式進(jìn)行注入. - 由于請(qǐng)求的
TrackingDataConfigRequest沒有projectId、tenantId參數(shù),所以映射的規(guī)則增加了這兩個(gè)屬性的默認(rèn)值配置。 - 其它參數(shù)類型、屬性都是一一對(duì)應(yīng),所以“約定大于配置”,就默認(rèn)逐個(gè)屬性都會(huì)映射。
2.3 編譯生成實(shí)現(xiàn)類
這里補(bǔ)充說明下,mapstruct是在編譯時(shí)期生成這個(gè)接口的實(shí)現(xiàn)類,所以不用有反射、性能這樣的擔(dān)憂。
生成的實(shí)現(xiàn)類如下:
@Generated(
value = "org.mapstruct.ap.MappingProcessor"
)
@Component
public class GeneralBurrypointConfigConvertorImpl implements GeneralBurrypointConfigConvertor {
@Override
public TrackingDataConfigVo convertVo(TrackingDataConfigRequest request) {
if ( request == null ) {
return null;
}
TrackingDataConfigVo trackingDataConfigVo = new TrackingDataConfigVo();
trackingDataConfigVo.setDimension( request.getDimension() );
trackingDataConfigVo.setBrand( request.getBrand() );
trackingDataConfigVo.setVehicleSeries( request.getVehicleSeries() );
trackingDataConfigVo.setVehicleModel( request.getVehicleModel() );
trackingDataConfigVo.setEventId( request.getEventId() );
trackingDataConfigVo.setVin( request.getVin() );
List<String> list = request.getAttrs();
if ( list != null ) {
trackingDataConfigVo.setAttrs( new ArrayList<String>( list ) );
}
trackingDataConfigVo.setSwitchState( request.getSwitchState() );
trackingDataConfigVo.setReportFrequency( request.getReportFrequency() );
trackingDataConfigVo.setReportRecords( request.getReportRecords() );
trackingDataConfigVo.setReportSize( request.getReportSize() );
return trackingDataConfigVo;
}
}
3. 其它場(chǎng)景展示
3.1 不同屬性類型之間映射
比如源屬性為String,由逗號(hào)分隔屬性組成,類似:“a,b, c……”,目標(biāo)對(duì)象屬性為List類型。解決辦法是實(shí)現(xiàn)一個(gè)工具類方法完成String轉(zhuǎn)List,配置mapstruct規(guī)則即可:
@Mapper(componentModel = "spring",
imports = {ConfigConvertUtil.class},
unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface GeneralBurrypointConfigConvertor {
@Mappings({
@Mapping(target = "attrs", expression = "java( ConfigConvertUtil.convertStringToList( config.getAttrs() ) )")})
TrackingDataConfigVo convertVo(TdpGeneralBurrypointConfig config);
}
轉(zhuǎn)換方法如下:
public static List<String> convertStringToList(String attrs) {
if (!StringUtils.isEmpty(attrs)) {
return Arrays.asList(attrs.split(CommonConstants.COMMN_SIGN))
.stream().map(s -> (s.trim()))
.collect(Collectors.toList());
}
return new ArrayList<String>();
}
注意需要在接口配置ConfigConvertUtil工具方法。
可以順便看到實(shí)現(xiàn)類:
@Override
public TrackingDataConfigVo convertVo(TdpGeneralBurrypointConfig config) {
if ( config == null ) {
return null;
}
TrackingDataConfigVo trackingDataConfigVo = new TrackingDataConfigVo();
trackingDataConfigVo.setProjectId( config.getProjectId() );
trackingDataConfigVo.setTenantId( config.getTenantId() );
trackingDataConfigVo.setDimension( config.getDimension() );
trackingDataConfigVo.setBrand( config.getBrand() );
trackingDataConfigVo.setVehicleSeries( config.getVehicleSeries() );
trackingDataConfigVo.setVehicleModel( config.getVehicleModel() );
trackingDataConfigVo.setEventId( config.getEventId() );
trackingDataConfigVo.setVin( config.getVin() );
trackingDataConfigVo.setSwitchState( config.getSwitchState() );
trackingDataConfigVo.setReportFrequency( config.getReportFrequency() );
trackingDataConfigVo.setReportRecords( config.getReportRecords() );
trackingDataConfigVo.setReportSize( config.getReportSize() );
// 實(shí)現(xiàn)類使用了配置指定的工具類進(jìn)行屬性值處理
trackingDataConfigVo.setAttrs( ConfigConvertUtil.convertStringToList( config.getAttrs() ) );
return trackingDataConfigVo;
}
3.2 屬性默認(rèn)值生成
@Mapper(componentModel = "spring",
imports = {ConfigConvertUtil.class, DeleteFlagType.class, CommonConstants.class, Date.class},
unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface GeneralBurrypointConfigConvertor {
GeneralBurrypointConfigConvertor INSTANCE = Mappers.getMapper(GeneralBurrypointConfigConvertor.class);
/**
* TrackingDataConfigVo轉(zhuǎn)為數(shù)據(jù)庫TdpGeneralBurrypointConfig,需要增加無法映射的一些默認(rèn)值
*
* @param config
* @return
*/
@Mappings({
@Mapping(target = "configId", expression = "java( ConfigConvertUtil.generateConfigId( config ) )"),
@Mapping(target = "deleteFlag", expression = "java( DeleteFlagType.VALIDATE.getCode() )"),
@Mapping(target = "attrs", expression = "java( ConfigConvertUtil.convertListToString( config.getAttrs() ) )"),
@Mapping(target = "createBy", expression = "java( CommonConstants.DEFAULT_ADMINASTRATOR )"),
@Mapping(target = "updateBy", expression = "java( CommonConstants.DEFAULT_ADMINASTRATOR )"),
@Mapping(target = "createTime", expression = "java( new Date() )"),
@Mapping(target = "updateTime", expression = "java( new Date() )")})
TdpGeneralBurrypointConfig convertDto(TrackingDataConfigVo config);
// ……
}
注意這些屬性的映射:
- configId:使用工具類,對(duì)請(qǐng)求體提取固定參數(shù)值按規(guī)則生成,
expression表示按照此規(guī)則生成。 - deleteFlag:是利用了枚舉類的賦值對(duì)象的默認(rèn)值。
- createBy、updateBy:使用的常量類的值賦值對(duì)象的默認(rèn)值。
- createTime、updateTime:使用的是
Date實(shí)現(xiàn)設(shè)置賦值對(duì)象屬性當(dāng)前時(shí)間。
檢查實(shí)現(xiàn)類可以進(jìn)一步確認(rèn):
@Override
public TdpGeneralBurrypointConfig convertDto(TrackingDataConfigVo config) {
if ( config == null ) {
return null;
}
TdpGeneralBurrypointConfig tdpGeneralBurrypointConfig = new TdpGeneralBurrypointConfig();
tdpGeneralBurrypointConfig.setProjectId( config.getProjectId() );
tdpGeneralBurrypointConfig.setTenantId( config.getTenantId() );
tdpGeneralBurrypointConfig.setDimension( config.getDimension() );
tdpGeneralBurrypointConfig.setBrand( config.getBrand() );
tdpGeneralBurrypointConfig.setVehicleSeries( config.getVehicleSeries() );
tdpGeneralBurrypointConfig.setVehicleModel( config.getVehicleModel() );
tdpGeneralBurrypointConfig.setEventId( config.getEventId() );
tdpGeneralBurrypointConfig.setVin( config.getVin() );
tdpGeneralBurrypointConfig.setSwitchState( config.getSwitchState() );
tdpGeneralBurrypointConfig.setReportFrequency( config.getReportFrequency() );
tdpGeneralBurrypointConfig.setReportRecords( config.getReportRecords() );
tdpGeneralBurrypointConfig.setReportSize( config.getReportSize() );
// 配置的默認(rèn)值生成規(guī)則
tdpGeneralBurrypointConfig.setDeleteFlag( DeleteFlagType.VALIDATE.getCode() );
tdpGeneralBurrypointConfig.setCreateBy( CommonConstants.DEFAULT_ADMINASTRATOR );
tdpGeneralBurrypointConfig.setUpdateBy( CommonConstants.DEFAULT_ADMINASTRATOR );
tdpGeneralBurrypointConfig.setCreateTime( new Date() );
tdpGeneralBurrypointConfig.setConfigId( ConfigConvertUtil.generateConfigId( config ) );
tdpGeneralBurrypointConfig.setUpdateTime( new Date() );
tdpGeneralBurrypointConfig.setAttrs( ConfigConvertUtil.convertListToString( config.getAttrs() ) );
return tdpGeneralBurrypointConfig;
}
4. 遇到的問題
在使用過程中,由于指定了MyBatis的掃描范圍為整個(gè)項(xiàng)目的包,導(dǎo)致誤把MapStruct的@Mapper注解也納入掃描范圍,找不到數(shù)據(jù)庫操作實(shí)現(xiàn)。所以,教訓(xùn)是配置MyBatis的掃描包范圍時(shí),一定避開MapStruct的包范圍,比如指定最小化到dao層:@MapperScan("com.fawvw.ms.tdp.data.config.dao")