1.对象属性映射的苦恼
在日常开发中,常常涉及到接收Request对象,属性映射到内部交互的VO对象、甚至需要进一步映射到DTO对象,以完成相关的业务逻辑。
举个最近的栗子,接收的业务请求对象是这样:
@Data
@ApiModel(description = "配置信息请求体")
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 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;
}
请求体的非@NotBlank
注解的字段为非必填(可能有值、也可以没有)。接口接收到请求参数后,需要转为如下内部交互VO结构:
public class TrackingDataConfigVo {
String projectId;
String tenantId;
String dimension;
String brand;
String vehicleSeries;
String vehicleModel;
String eventId;
String vin;
List attrs;
String switchState;
Integer reportFrequency;
Integer reportRecords;
Integer reportSize;
}
要实现这个映射有很多办法,比如:
- 最直接地,一个个请求体对象判断有值、设置到VO对象。更友好点, 针对VO对象提供Builder,实现链式属性赋值操作、最后build出对象
- 使用BeanUtils这样的工具类,比如
org.springframework.beans.BeanUtils
,还有Apache的org.apache.commons.beanutils.BeanUtilsBean
。
痛点就不必赘述了,BeanUtils工具类的主要问题是只能针对映射对象、被映射对象同名同类型属性进行映射,但是实际开发场景还有很多属性名称不同、类型不同、属性默认值、属性需要按规则生成的场景,导致BeanUtils工具类失效。
2.MapStruct实现对象属性映射
就以文章开头提到的两个对象映射为例,大致需要如下操作。
2.1引入pom依赖
org.mapstruct
mapstruct-jdk8
${org.mapstruct.version}
org.mapstruct
mapstruct-processor
${org.mapstruct.version}
org.apache.maven.plugins
maven-compiler-plugin
1.8
org.mapstruct
mapstruct-processor
${org.mapstruct.version}
org.projectlombok
lombok
${lombook.version}
-Amapstruct.suppressGeneratorTimestamp=true
-Amapstruct.suppressGeneratorVersionInfoComment=true
mapstruct版本项目中使用的1.3.1.Final
,最新的版本查了下应该是1.4.1.Final
,没仔细看区别。
2.2 建立属性映射关系
需要新建一个接口,指明映射对象与被映射对象,如果有字段需要特殊的映射规则,可以在转换方法配置,比如目标映射属性与源属性名称不同、属性类型不同等等。
@Mapper(componentModel = "spring")
public interface GeneralBurrypointConfigConvertor {
/**
* 完成请求TrackingDataConfigRequest到TrackingDataConfigVo属性映射(projectId等参数无法映射)
*
* @param request
* @return
*/
@Mappings({
@Mapping(target = "projectId", defaultValue = ""),
@Mapping(target = "tenantId", defaultValue = "")})
TrackingDataConfigVo convertVo(TrackingDataConfigRequest request);
}
简单说明下:
-
componentModel = "spring"
是因为使用Spring构建的项目,这里配置是指该接口生成的实现类上面会自动添加一个@Component
注解,可以通过Spring的@Autowired
方式进行注入. - 由于请求的
TrackingDataConfigRequest
没有projectId、tenantId参数,所以映射的规则增加了这两个属性的默认值配置。 - 其它参数类型、属性都是一一对应,所以“约定大于配置”,就默认逐个属性都会映射。
2.3 编译生成实现类
这里补充说明下,mapstruct是在编译时期生成这个接口的实现类,所以不用有反射、性能这样的担忧。
生成的实现类如下:
@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 list = request.getAttrs();
if ( list != null ) {
trackingDataConfigVo.setAttrs( new ArrayList( list ) );
}
trackingDataConfigVo.setSwitchState( request.getSwitchState() );
trackingDataConfigVo.setReportFrequency( request.getReportFrequency() );
trackingDataConfigVo.setReportRecords( request.getReportRecords() );
trackingDataConfigVo.setReportSize( request.getReportSize() );
return trackingDataConfigVo;
}
}
3. 其它场景展示
3.1 不同属性类型之间映射
比如源属性为String,由逗号分隔属性组成,类似:“a,b, c……”,目标对象属性为List类型。解决办法是实现一个工具类方法完成String转List,配置mapstruct规则即可:
@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);
}
转换方法如下:
public static List 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();
}
注意需要在接口配置ConfigConvertUtil
工具方法。
可以顺便看到实现类:
@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() );
// 实现类使用了配置指定的工具类进行属性值处理
trackingDataConfigVo.setAttrs( ConfigConvertUtil.convertStringToList( config.getAttrs() ) );
return trackingDataConfigVo;
}
3.2 属性默认值生成
@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转为数据库TdpGeneralBurrypointConfig,需要增加无法映射的一些默认值
*
* @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:使用工具类,对请求体提取固定参数值按规则生成,
expression
表示按照此规则生成。 - deleteFlag:是利用了枚举类的赋值对象的默认值。
- createBy、updateBy:使用的常量类的值赋值对象的默认值。
- createTime、updateTime:使用的是
Date
实现设置赋值对象属性当前时间。
检查实现类可以进一步确认:
@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() );
// 配置的默认值生成规则
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的扫描范围为整个项目的包,导致误把MapStruct的@Mapper
注解也纳入扫描范围,找不到数据库操作实现。所以,教训是配置MyBatis的扫描包范围时,一定避开MapStruct的包范围,比如指定最小化到dao层:@MapperScan("com.fawvw.ms.tdp.data.config.dao")