MapStruct使用

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
                    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")

你可能感兴趣的:(MapStruct使用)