1. 程式人生 > >第三十章:SpringBoot使用MapStruct自動對映DTO

第三十章:SpringBoot使用MapStruct自動對映DTO

MapStruct是一種型別安全的bean對映類生成java註釋處理器。
我們要做的就是定義一個對映器介面,宣告任何必需的對映方法。在編譯的過程中,MapStruct會生成此介面的實現。該實現使用純java方法呼叫的源和目標物件之間的對映,MapStruct節省了時間,通過生成程式碼完成繁瑣和容易出錯的程式碼邏輯。下面我們來揭開它的神祕面紗

本章目標

基於SpringBoot平臺完成MapStruct對映框架的整合。

SpringBoot 企業級核心技術學習專題

專題 專題名稱 專題描述
001 講解SpringBoot一些企業級層面的核心元件
002 Spring Boot 核心技術簡書每一篇文章碼雲對應原始碼
003 對Spring Cloud核心技術全面講解
004 Spring Cloud 核心技術簡書每一篇文章對應原始碼
005 全面講解QueryDSL核心技術以及基於SpringBoot整合SpringDataJPA

構建專案

我們使用idea開發工具建立一個SpringBoot專案,新增相應的依賴,pom.xml配置檔案如下所示:

...省略部分程式碼
<parent>
        <groupId>org.springframework.boot</groupId
>
<artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding
>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <org.mapstruct.version>1.2.0.CR1</org.mapstruct.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <!--<scope>provided</scope>--> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--mapStruct依賴--> <dependency> <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> <scope>provided</scope> </dependency> <dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <version>1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.31</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> ....省略部分程式碼

整合MapStruct官方提供了兩種方式,上面配置檔案內我們採用的是直接新增Maven依賴,而官方文件還提供了另外一種方式,採用Maven外掛形式配置,配置如下所示:

...引用官方文件
...
<properties>
    <org.mapstruct.version>1.2.0.CR1</org.mapstruct.version>
</properties>
...
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-jdk8</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>
...
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <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>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>
...

我個人比較喜歡採用第一種方式,不需要配置過多的外掛,依賴方式比較方便。
接下來我們開始配置下資料庫連線資訊以及簡單的兩張表的SpringDataJPA相關介面。

資料庫連線資訊

在resource下新建立一個application.yml檔案,並新增如下資料庫連線配置:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8
    username: root
    password: 123456
    #最大活躍數
    maxActive: 20
    #初始化數量
    initialSize: 1
    #最大連線等待超時時間
    maxWait: 60000
    #開啟PSCache,並且指定每個連線PSCache的大小
    poolPreparedStatements: true
    maxPoolPreparedStatementPerConnectionSize: 20
    #通過connectionProperties屬性來開啟mergeSql功能;慢SQL記錄
    #connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
    minIdle: 1
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: select 1 from dual
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    #配置監控統計攔截的filters,去掉後監控介面sql將無法統計,'wall'用於防火牆
    filters: stat, wall, log4j
  jpa:
    properties:
      hibernate:
        show_sql: true
        format_sql: true

有關SpringDataJPA相關的學習請訪問第三章:SpringBoot使用SpringDataJPA完成CRUD,我們在資料庫內建立兩張表資訊分別是商品基本資訊表、商品型別表。
兩張表有相應的關聯,我們在不採用連線查詢的方式模擬使用MapStruct,表資訊如下所示:

--商品型別資訊表
CREATE TABLE `good_types` (
  `tgt_id` int(11) NOT NULL AUTO_INCREMENT,
  `tgt_name` varchar(30) DEFAULT NULL,
  `tgt_is_show` int(1) DEFAULT NULL,
  `tgt_order` int(255) DEFAULT NULL,
  PRIMARY KEY (`tgt_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

--商品基本資訊表
CREATE TABLE `good_infos` (
  `tg_id` int(11) NOT NULL AUTO_INCREMENT,
  `tg_type_id` int(11) DEFAULT NULL,
  `tg_title` varchar(30) DEFAULT NULL,
  `tg_price` decimal(8,2) DEFAULT NULL,
  `tg_order` int(2) DEFAULT NULL,
  PRIMARY KEY (`tg_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO `good_types` VALUES ('1', '青菜', '1', '1');
INSERT INTO `good_infos` VALUES ('1', '1', '芹菜', '12.40', '1');

下面我們根據這兩張表建立對應的實體類。

商品型別實體

package com.yuqiyu.chapter30.bean;

import lombok.Data;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/8/20
 * Time:11:17
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Entity
@Table(name = "good_types")
@Data
public class GoodTypeBean
{
    @Id
    @Column(name = "tgt_id")
    private Long id;

    @Column(name = "tgt_name")
    private String name;
    @Column(name = "tgt_is_show")
    private int show;
    @Column(name = "tgt_order")
    private int order;

}

商品基本資訊實體

package com.yuqiyu.chapter30.bean;

import lombok.Data;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/8/20
 * Time:11:16
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Entity
@Table(name = "good_infos")
@Data
public class GoodInfoBean
{
    @Id
    @Column(name = "tg_id")
    private Long id;
    @Column(name = "tg_title")
    private String title;
    @Column(name = "tg_price")
    private double price;
    @Column(name = "tg_order")
    private int order;
    @Column(name = "tg_type_id")
    private Long typeId;
}

接下來我們繼續建立相關的JPA。

商品型別JPA

package com.yuqiyu.chapter30.jpa;

import com.yuqiyu.chapter30.bean.GoodTypeBean;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/8/20
 * Time:11:24
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
public interface GoodTypeJPA
    extends JpaRepository<GoodTypeBean,Long>
{
}

商品資訊JPA

package com.yuqiyu.chapter30.jpa;

import com.yuqiyu.chapter30.bean.GoodInfoBean;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/8/20
 * Time:11:23
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
public interface GoodInfoJPA
    extends JpaRepository<GoodInfoBean,Long>
{

}

配置MapStruct

到目前為止我們的準備工作差不多完成了,下面我們開始配置使用MapStruct。我們的最終目的是為了返回一個自定義的DTO實體,那麼我們就先來建立這個DTO,DTO的程式碼如下所示:

package com.yuqiyu.chapter30.dto;

import lombok.Data;

/**
 * 轉換Dto
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/8/20
 * Time:11:25
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Data
public class GoodInfoDTO
{
    //商品編號
    private String goodId;
    //商品名稱
    private String goodName;
    //商品價格
    private double goodPrice;
    //型別名稱
    private String typeName;
}

可以看到GoodInfoDTO實體內集成了商品資訊、商品型別兩張表內的資料,對應查詢出資訊後,我們需要使用MapStruct自動對映到GoodInfoDTO。

建立Mapper

Mapper這個定義一般是被廣泛應用到MyBatis半自動化ORM框架上,而這裡的Mapper跟Mybatis沒有關係。下面我們先來看下程式碼,如下所示:

package com.yuqiyu.chapter30.mapper;

import com.yuqiyu.chapter30.bean.GoodInfoBean;
import com.yuqiyu.chapter30.bean.GoodTypeBean;
import com.yuqiyu.chapter30.dto.GoodInfoDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;

/**
 * 配置對映
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/8/20
 * Time:11:26
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Mapper(componentModel = "spring")
//@Mapper
public interface GoodInfoMapper
{
    //public static GoodInfoMapper MAPPER = Mappers.getMapper(GoodInfoMapper.class);

    @Mappings({
            @Mapping(source = "type.name",target = "typeName"),
            @Mapping(source = "good.id",target = "goodId"),
            @Mapping(source = "good.title",target = "goodName"),
            @Mapping(source = "good.price",target = "goodPrice")
    })
    public GoodInfoDTO from(GoodInfoBean good, GoodTypeBean type);
}

可以看到GoodInfoMapper是一個介面的形式存在的,當然也可以是一個抽象類,如果你需要在轉換的時候才用個性化的定製的時候可以採用抽象類的方式,相應的程式碼配置官方文件已經宣告。
@Mapper註解是用於標註介面、抽象類是被MapStruct自動對映的標識,只有存在該註解才會將內部的介面方法自動實現。
MapStruct為我們提供了多種的獲取Mapper的方式,比較常用的兩種分別是

預設配置

預設配置,我們不需要做過多的配置內容,獲取Mapper的方式就是採用Mappers通過動態工廠內部反射機制完成Mapper實現類的獲取。
預設方式獲取Mapper如下所示:

//Mapper介面內部定義
public static GoodInfoMapper MAPPER = Mappers.getMapper(GoodInfoMapper.class);

//外部呼叫
GoodInfoMapper.MAPPER.from(goodBean,goodTypeBean);
Spring方式配置

Spring方式我們需要在@Mapper註解內新增componentModel屬性值,配置後在外部可以採用@Autowired方式注入Mapper實現類完成對映方法呼叫。
Spring方式獲取Mapper如下所示:

//註解配置
@Mapper(componentModel = "spring")

//注入Mapper實現類
@Autowired
private GoodInfoMapper goodInfoMapper;

//呼叫
goodInfoMapper.from(goodBean,goodTypeBean);
@Mappings & @Mapping

Mapper介面定義方法上面聲明瞭一系列的註解對映@Mapping以及@Mappings,那麼這兩個註解是用來幹什麼工作的呢?
@Mapping註解我們用到了兩個屬性,分別是sourcetarget

source代表的是對映介面方法內的引數名稱,如果是基本型別的引數,引數名可以直接作為source的內容,如果是實體型別,則可以採用實體引數名.欄位名的方式作為source的內容,配置如上面GoodInfoMapper內容所示。

target代表的是對映到方法方法值內的欄位名稱,配置如上面GoodInfoMapper所示。

檢視Mapper實現

下面我們執行maven compile命令,到target/generated-sources/annotations目錄下檢視對應Mapper實現類,實現類程式碼如下所示:

package com.yuqiyu.chapter30.mapper;

import com.yuqiyu.chapter30.bean.GoodInfoBean;
import com.yuqiyu.chapter30.bean.GoodTypeBean;
import com.yuqiyu.chapter30.dto.GoodInfoDTO;
import javax.annotation.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2017-08-20T12:52:52+0800",
    comments = "version: 1.2.0.CR1, compiler: javac, environment: Java 1.8.0_111 (Oracle Corporation)"
)
@Component
public class GoodInfoMapperImpl implements GoodInfoMapper {

    @Override
    public GoodInfoDTO from(GoodInfoBean good, GoodTypeBean type) {
        if ( good == null && type == null ) {
            return null;
        }

        GoodInfoDTO goodInfoDTO = new GoodInfoDTO();

        if ( good != null ) {
            if ( good.getId() != null ) {
                goodInfoDTO.setGoodId( String.valueOf( good.getId() ) );
            }
            goodInfoDTO.setGoodName( good.getTitle() );
            goodInfoDTO.setGoodPrice( good.getPrice() );
        }
        if ( type != null ) {
            goodInfoDTO.setTypeName( type.getName() );
        }

        return goodInfoDTO;
    }
}

MapStruct根據我們配置的@Mapping註解自動將source實體內的欄位進行了呼叫target實體內欄位的setXxx方法賦值,並且做出了一切引數驗證。
我們採用了Spring方式獲取Mapper,在自動生成的實現類上MapStruct為我們自動添加了@ComponentSpring宣告式注入註解配置。

執行測試

下面我們來建立一個測試的Controller,用於訪問具體請求地址時查詢出商品的基本資訊以及商品的型別後呼叫GoodInfoMapper.from(xxx,xxx)方法完成返回GoodInfoDTO例項。Controller程式碼實現如下所示:

package com.yuqiyu.chapter30.controller;

import com.yuqiyu.chapter30.bean.GoodInfoBean;
import com.yuqiyu.chapter30.bean.GoodTypeBean;
import com.yuqiyu.chapter30.dto.GoodInfoDTO;
import com.yuqiyu.chapter30.jpa.GoodInfoJPA;
import com.yuqiyu.chapter30.jpa.GoodTypeJPA;
import com.yuqiyu.chapter30.mapper.GoodInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 測試控制器
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2017/8/20
 * Time:12:24
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@RestController
public class GoodInfoController
{
    /**
     * 注入商品基本資訊jpa
     */
    @Autowired
    private GoodInfoJPA goodInfoJPA;
    /**
     * 注入商品型別jpa
     */
    @Autowired
    private GoodTypeJPA goodTypeJPA;
    /**
     * 注入mapStruct轉換Mapper
     */
    @Autowired
    private GoodInfoMapper goodInfoMapper;

    /**
     * 查詢商品詳情
     * @param id
     * @return
     */
    @RequestMapping(value = "/detail/{id}")
    public GoodInfoDTO detail(@PathVariable("id") Long id)
    {
        //查詢商品基本資訊
        GoodInfoBean goodInfoBean = goodInfoJPA.findOne(id);
        //查詢商品型別基本資訊
        GoodTypeBean typeBean = goodTypeJPA.findOne(goodInfoBean.getTypeId());
        //返回轉換dto
        return goodInfoMapper.from(goodInfoBean,typeBean);
    }
}

在Controller內我們注入了GoodInfoJPAGoodTypeJPA以及GoodInfoMapper,在查詢商品詳情方法時做出了對映處理。接下來我們啟動專案訪問地址http://127.0.0.1:8080/detail/1檢視介面輸出效果,如下所示:

{
goodId: "1",
goodName: "芹菜",
goodPrice: 12.4,
typeName: "青菜"
}

可以看到介面輸出了GoodInfoDTO內的所有欄位內容,並且通過from方法將對應配置的target欄位賦值。

總結

本章主要講述了基於SpringBoot開發框架上整合MapStruct自動對映框架,完成模擬多表獲取資料後將某一些欄位通過@Mapping配置自動對映到DTO實體例項指定的欄位內。
MapStruct官方文件地址:http://mapstruct.org/documentation/dev/reference/html/

更多幹貨文章掃碼關注微信公眾號

掃碼關注 - 專注分享

加入知識星球,恆宇少年帶你走以後的技術道路!!!

微信掃碼加入