mapstruct用法详解

1-1 mpg生成Dao接口和PO对象

1-2 MapStruct完成po/dto/vo的互相转换

Maven 项目引入 MapStruct

注意:
由于 MapStruct 依赖于 JavaBean 中有 getter/setter 方法,所以,如果使用了 lombok 来生成 getter/setter 方法的话,那么需要 lombok 先与 MapStruct “起作用”。

为了确保 lombok “先与 MapStruct 起作用”这需要在 maven 项目的 pom.xml 中的 plugins > plugin 下的 maven-compiler-plugin 插件下加上好大一坨 configuration 配置。

好在,虽然很繁琐,但是都是直接复制粘贴的事,不需要我们改动什么。

第 1 步:引入 pom 依赖
<properties>
   <org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
   <org.projectlombok.version>1.18.16</org.projectlombok.version>
  … 其它版本声明
</properties>

<dependencies>

  <!-- lombok dependencies should not end up on classpath -->
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${org.projectlombok.version}</version>
    <scope>provided</scope>
  </dependency>

  <dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>${org.mapstruct.version}</version>
  </dependency>

  … 其它依赖声明

</dependencies>
第 2 步:配置 maven 插件
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.8.1</version>
      <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <encoding>UTF-8</encoding>
<!-- 复制的开始 -->
        <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>${org.projectlombok.version}</version>
          </path>
          <path>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok-mapstruct-binding</artifactId>
            <version>0.2.0</version>
          </path>
      </annotationProcessorPaths>
<!-- 复制的结束 -->
      </configuration>
    </plugin>

    … 其它插件声明

  </plugins>
</build>
案例:最简单情况:PO 和 DTO 一模一样
第一步:准备好“源”

首先我们准备好 PO 类 Department:

@Data
@TableName("department")
public class Department {
    private Long id;
    private String name;
    private String location;
}
第二步:准备好“目标”

然后我们再准备好 DTO 类 DepartmentDto,两者的属性数量、类型甚至名称都是一样的:

@Data
public class DepartmentDto {
    private Long id;
    private String name;
    private String location;
}
第三步:准备好转换器

最后我们准备好转换工具类 Mapper/Converter:

import org.mapstruct.Mapper;
/**
 * 转换器,就MapStruct的格式完成
 * 1.转换器上面添加@Mapper注解,不要和ibatis下面的@Mapper注解混淆
 * 2.提供转换方法
 */
@Mapper(componentModel = "spring") //componentModel设置转换器对象获取的方式:spring
public interface DepartmentConverter {
    //单个po-->单个dto
//    目标类型 自定义方法名(源类型 姓名自定会议);
    DepartmentDto from(Department po);

    //多个po-->多个dto  List<po>--->List<dto>
//    List<目标类型> 自定义方法名(List<源类型> 姓名自定会议);
    List<DepartmentDto> from(List<Department> pos);
}

以上代码的编写要求

  • 类名任意;
  • 方法名任意;
  • 参数和返回值类型要符合逻辑【即将什么类型转换为什么类型,这个认知决定了方法形参类型和方法返回值类型】。
第四步:使用和验证
@SpringBootTest
class MallFrontApplicationTests {
    @Autowired
    private DepartmentConverter departmentConverter;

    /**
     * po--->Dto的代码
     */
    @Test
    public void contextLoads() {
        Department po = Department.builder()
                .id(1l)
                .name("java")
                .location("青岛").build();
        DepartmentDto dto =departmentConverter.from(po);
        System.out.println(dto);
        System.out.println("=============================================");
        List<Department> poList=Arrays.asList(
                Department.builder()
                        .id(1l)
                        .name("java")
                        .location("武汉").build(),
                Department.builder()
                        .id(2l)
                        .name("python")
                        .location("烟台").build(),
                Department.builder()
                        .id(3l)
                        .name("web")
                        .location("哈尔滨").build()
        );
       List<DepartmentDto> dtoList =departmentConverter.from(poList);
        System.out.println(dtoList);
    }
}

[注意] 提示
其实 MapStruct 的实现原理很简单,就是根据我们在 Mapper 接口中使用的 @Mapper 和 @Mapping 等注解,在运行时生成接口的实现类,写完接口后,可以利用compile命令编译后,打开项目的 target 目录查看xxxConverter对应的xxxConverterImpl实现类代码。

DTO 和 PO 一丢丢不一样
  • 情况一:属性数量不一样。

    通常页面上常常不需要返回那么多的数据,所以 DTO 的属性可能比 PO 属性少。这种情况最简单,上面的代码不需要做任何变化即可。删除 DepartmentDto 中的 location 属性,验证。

  • 情况二:属性名称不一样。

    这种情况下,需要在转换类的转换方法上额外使用 @Mapping 注解,标识出不一样的那些些属性(一样的属性不用标识)。例如:

情况一和情况二的解决办法:

 @Data
 public class DepartmentDto {
    private Long id;
    private String departmentName; // 和表的字段名不一样
//  private String location;       // 少了一个字段
}

@Mapper
public interface DepartmentDtoConverter {

    @Mapping(source = "name", target = "departmentName")
    DepartmentDto from(Department po);

    @Mapping(source = "name", target = "departmentName")
    List<DepartmentDto> from(List<Department> po);
}
  • 情况三:数据类型不一样。

    这种情况最常见是出现在 po 类是日期时间类型,而 dto 是 String 类型上。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeDto {
    private String hireDate; // 表和 PO 类中是 date 类型
    … 其他属性
}

@Mapper
public interface EmployeeDtoConverter {
    // @Mapping 注解的 source 属性可省略。不过写上逻辑更清晰一些。
    @Mapping(target = "hireDate", dateFormat = "yyyy-MM-dd")
    EmployeeDto from(Employee po);

    @Mapping(target = "hireDate", dateFormat = "yyyy-MM-dd")
    List<EmployeeDto> from(List<Employee> po);

}

[注意] 提示
上面三种情况可能会参杂在一起。

MapStruct使用expression处理某些属性使用特定值的情况

第一步:准备好“源”

首先我们准备好 PO 类 Department:

@Data //getter和setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Department {
    private Long id;
    private String name;
    private String location;
    private LocalDateTime createTime;
    private Integer status;//状态 1 2
}
第二步:准备好“目标”

然后我们再准备好 DTO 类 DepartmentDto,此处的特点是:status变成了String类型,我们希望dto中status保存“正常”或“异常”格式:

@Data
public class DepartmentDto {
    private Long id;
    private String name;
    private String loc;
    private String myTime;
    private String status;
}
第三步:准备好转换器

最后我们准备好转换工具类 Mapper/Converter:

@Mapper(componentModel = "spring",uses = EmployeeConverter.class) //componentModel设置转换器对象获取的方式:spring
public interface DepartmentConverter {
    //单个po-->单个dto
//    目标类型 自定义方法名(源类型 姓名自定会议);
    @Mapping(source = "location",target = "loc")
    @Mapping(source = "createTime",target = "myTime",dateFormat = "yyyy年MM月dd日 HH:mm:ss")
    @Mapping(target = "status",expression = "java(po.getStatus()==1?\"正常\":\"禁用\")")
    @Mapping(target = "empDtoList",source = "empPoList")
    DepartmentDto from(Department po);

    //多个po-->多个dto  List<po>--->List<dto>
//    List<目标类型> 自定义方法名(List<源类型> 姓名自定会议);
    @Mapping(source = "location",target = "loc")
    @Mapping(source = "createTime",target = "myTime",dateFormat = "yyyy年MM月dd日 HH:mm:ss")
    //使用expression指定表达式的值赋给target属性,所以不要再写source了
    @Mapping(target = "status",expression = "java(po.getStatus()==1?\"正常\":\"禁用\")")
    List<DepartmentDto> from(List<Department> pos);
}
第四步:使用和验证
@SpringBootTest
class WoniumallFrontApplicationTests {
    @Autowired
    private DepartmentConverter departmentConverter;

    /**
     * po--->Dto的代码
     */
    @Test
    public void contextLoads() {
        Department po = Department.builder()
                .id(1l)
                .name("java")
            	.status(1)
                .location("青岛").build();
        DepartmentDto dto =departmentConverter.from(po);
        System.out.println(dto);
    }
}

MapStruct完成级联对象的转换

第一步:准备好“源”

首先我们准备好 PO 类 Employee,其内部提供了一个Department的实体类属性:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Employee {
    private Integer id;
    private String empName;
    //员工和部门的实体关系:1-1
    /**
     * po里面如果要建议其他对象的引用,内部的引用对象类型一定是po
     */
    private Department department;
}
第二步:准备好“目标”

然后我们再准备好 DTO 类 EmployeeDto,此处的特点是:员工所属部门是DepartmentDto对象

@Data
public class EmployeeDto {
    private Integer id;
    private String empName;
    //员工和部门的实体关系:1-1
    /**
     * po里面如果要建议其他对象的引用,内部的引用对象类型一定是po
     */
    private DepartmentDto departmentDto;
}
第三步:准备好转换器

最后我们准备好转换工具类 Mapper/Converter:

/**
 * 因为EmployeeDto中有一个DepartmentDto,所以声明转换器需要指定内部对象的转换格式
 */
@Mapper(componentModel = "spring",uses = DepartmentConverter.class ) //uses指定当前转换器要引用其他转换器协助完成转换工作
public interface EmployeeConverter {
    //定义转换方法
    @Mapping(source = "department",target = "departmentDto")
    EmployeeDto from(Employee po);

    @Mapping(source = "department",target = "departmentDto")
    List<EmployeeDto> from(List<Employee> po);
}
第四步:使用和验证
@SpringBootTest
class WoniumallFrontApplicationTests {
    @Autowired
    private EmployeeConverter employeeConverter;

   @Test
    public void contextLoads3() {
        Employee emp=new Employee().builder()
                .id(1001)
                .empName("小心")
                .department(
                        Department.builder()
                                .id(1l)
                                .name("java")
                                .location("青岛")
                                .status(1)
                                .createTime(LocalDateTime.now()).build()
                )
                .build();
        System.out.println(employeeConverter.from(emp));
    }
}

分页查询的结果映射

PageInfo         PageInfo
└──> PO    ==>   └──> DTO

对于 mybatis 的 page-helper 的分页查询的结果,如果需要映射,需要使用上一章的子对象映射功能。将 PageInfo<PO> 转换成 PageInfo<Dto> 。

[!cite] 提示
解决思路就是上一个案例中级联对象操作的方案

@Mapper(componentModel = "spring", uses = DepartmentDtoConverter.class)
public interface EmployeeDtoConverter {

    @Mapping(source = "department",target = "departmentDto")
    EmployeeDto from(Employee po);

    @Mapping(source = "department",target = "departmentDto")
    List<EmployeeDto> from(List<Employee> poList);

    @Mapping(source = "list", target = "list")
    PageInfo<EmployeeDto> from(PageInfo<Employee> poPage);
}

MapStruct扩展了解

MapStruct 使用对象工厂

默认情况下,MapStruct 使用的是对象的默认构造器(即,无参构造器)创建对象,而后,再对其属性进行复制。

但是,有时候我们不希望 MapStruct 使用默认构造来创建目标对象,而是使用对象工厂。最典型的场景就是:需要向 new 出来的对象中注入一个单利对象。比如,向 Java Bean 中注入 Repository 或者是“更厉害”的 ApplicationContext 。

这种情况下,需要结合 @ObjectFactory 注解和 @Mapper 注解的 uses 属性来实现。

首先,为目标对象准备好工厂类,例如:

@Component  
@RequiredArgsConstructor  
public class DepartmentFactory {  
  
    private final ApplicationContext applicationContext;  
  
    @ObjectFactory  // <- 看这里
    public Department createDepartment() {  
        System.out.println("通过 DepartmentFactory 创建 Department 对象");  
        return new Department(applicationContext);  
    }  
}

在工厂方法上标注 @ObjectFactory 注解。

然后,在转换器的 @Mapper 注解中使用 ueses 属性来“告知”MapStruct 使用这个工厂类(的工厂)方法来创建目标对象。

@Mapper(componentModel = "spring", uses = DepartmentFactory.class)  
public interface DepartmentConverter {  
    Department from(DepartmentPo po);  
}

另外,在更复杂的情况中,如果有两个类有引用关系,需要转换器转换,这里的工厂类和之前的内容不冲突:

@Mapper(componentModel = "spring", uses = {  
        EmployeeFactory.class,  
        DepartmentConverter.class  
})  
public interface EmployeeConverter {  
    Employee from(EmployeePo po);  
}

在上面的例子中,使用工厂类 EmployeeFactory 来创建 Employee 类的对象,而 Employee 对象有 deparmtent 属性,它又是利用 DepartmentConverter 转换器创建出来的。

合并映射

MapStruct 也支持把多个对象属性“聚拢”到一个对象中去。

  • 例如这里把 Member 和 Order 的部分属性映射到 MemberOrderDto 中去;
@Data
@EqualsAndHashCode(callSuper = false)
public class MemberOrderDto extends MemberDto{
    private String orderSn;
    private String receiverAddress;
}
  • 然后在 Mapper 中添加 toMemberOrderDto 方法,这里需要注意的是由于参数中具有两个属性,需要通过 <参数名称>.<属性> 的名称来指定 source 来防止冲突(这两个参数中都有 id 属性);
@Mapper
public interface MemberMapper {
    MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);

    @Mapping(source = "member.phone", target = "phoneNumber")
    @Mapping(source = "member.birthday", target = "birthday",dateFormat = "yyyy-MM-dd")
    @Mapping(source = "member.id", target = "id")
    @Mapping(source = "order.orderSn", target = "orderSn")
    @Mapping(source = "order.receiverAddress", target = "receiverAddress")
    MemberOrderDto toMemberOrderDto(Member member, Order order);
}
  • 接下来在 Controller 中创建测试接口,直接通过 Mapper 中的 INSTANCE 实例调用转换方法 toMemberOrderDto;

使用常量、默认值和表达式

使用 MapStruct 映射属性时,我们可以设置属性为常量或者默认值,也可以通过 Java 中的方法编写表达式来自动生成属性。

  • 例如下面这个商品类 Product 对象;
@Data
@EqualsAndHashCode(callSuper = false)
public class Product {
    private Long id;
    private String productSn;
    private String name;
    private String subTitle;
    private String brandName;
    private Double price;
    private Integer count;
}
  • 我们想把 Product 转换为 ProductDto 对象,id 属性设置为常量,count 设置默认值为 1 ,productSn 设置为 UUID 生成;
@Data
@EqualsAndHashCode(callSuper = false)
public class ProductDto {
    private Long id;            // 使用常量
    private String productSn;   // 使用表达式生成属性
    private String name;
    private String subTitle;
    private String brandName;
    private Double price;
    private Integer count;      // 使用默认值
}
  • 创建 ProductMapper 接口,通过 @Mapping 注解中的 constant、defaultValue、expression 设置好映射规则;
@Mapper(imports = {UUID.class})
public interface ProductMapper {
    ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class);

    @Mapping(target = "id", constant = "-1L")
    @Mapping(target = "count", source = "count", defaultValue = "1")
    @Mapping(target = "productSn", expression = "java(UUID.randomUUID().toString())")
    ProductDto toDto(Product product);
}
ProductDtoConverter instance = ProductDtoConverter.INSTANCE;  
  
Product product = new Product(1L, "aaa", "bbb", "ccc", "ddd", 1.2, null);  
  
ProductDto dto = instance.toDto(product);  
System.out.println(dto);

在映射前后进行自定义处理

MapStruct 也支持在映射前后做一些自定义操作,类似 AOP 中的切面。

由于此时我们需要创建自定义处理方法,创建一个抽象类 ProductRoundMapper ,通过 @BeforeMapping 注解自定义映射前操作,通过 @AfterMapping 注解自定义映射后操作;

@Mapper(imports = {UUID.class})
public abstract class ProductRoundMapper {
    public static ProductRoundMapper INSTANCE = Mappers.getMapper(ProductRoundMapper.class);

    @Mapping(target = "id",constant = "-1L")
    @Mapping(source = "count",target = "count",defaultValue = "1")
    @Mapping(target = "productSn",expression = "java(UUID.randomUUID().toString())")
    public abstract ProductDto toDto(Product product);

    @BeforeMapping
    public void beforeMapping(Product product){
        //映射前当price<0时设置为0
        if(product.getPrice().compareTo(BigDecimal.ZERO)<0){
            product.setPrice(BigDecimal.ZERO);
        }
    }

    @AfterMapping
    public void afterMapping(@MappingTarget ProductDto productDto){
        //映射后设置当前时间为createTime
        productDto.setCreateTime(new Date());
    }
}
  • 测试

处理映射异常

代码运行难免会出现异常,MapStruct 也支持处理映射异常。

  • 我们需要先创建一个自定义异常类;
public class ProductValidatorException extends Exception {
    public ProductValidatorException(String message) {
        super(message);
    }
}
  • 然后创建一个验证类,当 price 设置小于 0 时抛出我们自定义的异常;
public class ProductValidator {
    public BigDecimal validatePrice(BigDecimal price) throws ProductValidatorException {
        if(price.compareTo(BigDecimal.ZERO)<0){
            throw new ProductValidatorException("价格不能小于0!");
        }
        return price;
    }
}
  • 之后我们通过 @Mapper 注解的 uses 属性运用验证类;
@Mapper(uses = {ProductValidator.class},imports = {UUID.class})
public interface ProductExceptionMapper {
    ProductExceptionMapper INSTANCE = Mappers.getMapper(ProductExceptionMapper.class);

    @Mapping(target = "id",constant = "-1L")
    @Mapping(source = "count",target = "count",defaultValue = "1")
    @Mapping(target = "productSn",expression = "java(UUID.randomUUID().toString())")
    ProductDto toDto(Product product) throws ProductValidatorException;
}
  • 然后在 Controller 中添加测试接口,设置 price 为 -1 ,此时在进行映射时会抛出异常;
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MapStruct是一个Java注解处理器,可以帮助开发者快速生成Java Bean之间的映射代码。通过注解和代码生成,MapStruct能够自动映射两个Java Bean之间的属性。 下面是一个使用MapStruct的示例: 首先,在Maven项目中添加以下依赖: ```xml <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.4.2.Final</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.4.2.Final</version> <scope>provided</scope> </dependency> ``` 然后,我们需要定义两个Java Bean类,例如: ```java public class SourceBean { private String name; private int age; // getter and setter } public class TargetBean { private String name; private int age; // getter and setter } ``` 接下来,我们需要定义一个Mapper接口,用于映射两个Java Bean之间的属性。例如: ```java @Mapper public interface MyMapper { MyMapper INSTANCE = Mappers.getMapper(MyMapper.class); TargetBean sourceToTarget(SourceBean sourceBean); } ``` 在这个接口中,我们使用了MapStruct提供的@Mapper注解,表示这个接口是一个Mapper接口。同时,我们还定义了一个静态的INSTANCE实例,并使用了Mappers.getMapper方法来获取该实例。 接着,我们在这个接口中定义了一个sourceToTarget方法,用于将SourceBean映射到TargetBean。在这个方法中,我们只需要编写一个简单的映射规则即可,例如: ```java @Mapping(source = "name", target = "name") @Mapping(source = "age", target = "age") TargetBean sourceToTarget(SourceBean sourceBean); ``` 在这个方法中,我们使用了MapStruct提供的@Mapping注解,指定了SourceBean和TargetBean中属性之间的映射关系。例如,source = "name"表示SourceBean中的name属性映射到TargetBean中的name属性。 最后,在我们的代码中,我们可以使用MyMapper.INSTANCE.sourceToTarget方法来将SourceBean转换为TargetBean,例如: ```java SourceBean sourceBean = new SourceBean(); sourceBean.setName("张三"); sourceBean.setAge(18); TargetBean targetBean = MyMapper.INSTANCE.sourceToTarget(sourceBean); System.out.println(targetBean.getName()); // 输出:张三 System.out.println(targetBean.getAge()); // 输出:18 ``` 这样,我们就完成了两个Java Bean之间的属性映射。使用MapStruct能够让我们的代码更加简洁和可维护,特别是在处理复杂的Java Bean之间的映射时,MapStruct能够大大提高我们的开发效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值