MapStruct使用

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 看下使用 之前遇到j(luò)avaBean之間的轉(zhuǎn)換,可能一般都用BeanUtil.copyProperties直接轉(zhuǎn)換了...
    wang_cheng閱讀 2,715評(píng)論 0 0
  • 背景 在代碼開發(fā)中,我們通常都會(huì)使用分層架構(gòu),在分層架構(gòu)中都會(huì)使用模型轉(zhuǎn)換,在不同的層使用不同的模型。以 DDD ...
    原水寒閱讀 13,757評(píng)論 1 6
  • 對(duì)象映射工具的由來 大型項(xiàng)目采用分層開發(fā),每層的數(shù)據(jù)模型都不同:在持久化層,模型層為 PO(Persistent ...
    holmes000閱讀 1,976評(píng)論 0 3
  • 日常開發(fā)中,我們時(shí)長會(huì)寫很多關(guān)于PO轉(zhuǎn)VO的代碼或者是VO轉(zhuǎn)DTO相關(guān)的代碼,造成我們的程序異常的臃腫。如下: 編...
    茶還是咖啡閱讀 23,463評(píng)論 0 10
  • 推薦指數(shù): 6.0 書籍主旨關(guān)鍵詞:特權(quán)、焦點(diǎn)、注意力、語言聯(lián)想、情景聯(lián)想 觀點(diǎn): 1.統(tǒng)計(jì)學(xué)現(xiàn)在叫數(shù)據(jù)分析,社會(huì)...
    Jenaral閱讀 5,984評(píng)論 0 5

友情鏈接更多精彩內(nèi)容