SpringBoot集成MapStruct实现类型转换

在分层架构的开发中,经常遇到以下场景:将 DAO 查询出的 Entity 对象,转换为返回给前端的 VO 对象;或者将前端传来的 DTO 对象,转换为要存入数据库的 Entity 对象。

每当两个对象的字段很多时,用get和set的方法完成“数据搬运”的过程就比较麻烦,接下来介绍一下使用MapStruct的方法。

什么是 MapStruct?

MapStruct是一款强大的JavaBean映射框架,它的目标是简化JavaBean之间的映射过程。与手动编写繁琐的转换代码相比,MapStruct通过使用注解和自动生成的代码,提供了一种高效、类型安全、可维护的解决方案。

MapStruct 是一个 Java 注解处理器,用于在编译时自动生成类型安全、高性能的对象映射代码。 请注意这里的几个关键词:

编译时生成代码:它不是像 `BeanUtils.copyProperties` 那样在运行时使用反射,所以性能极高,与你手写的 `get/set` 代码完全一样。

类型安全:它在编译期间就会对字段类型进行检查。如果源对象和目标对象的字段类型不兼容,编译会直接报错,让你能尽早发现问题。

注解驱动:你只需要定义一个简单的 Java 接口并添加注解,MapStruct 就会在幕后为你完成所有繁重的工作。

MapStruct的主要特性包括:

  • 自动生成映射代码: 通过注解,MapStruct能够在编译期自动生成映射代码,避免了手动编写大量重复的转换逻辑。
  • 类型安全: MapStruct通过编译时检查,确保了映射的类型安全性,减少了在运行时发生的类型错误。
  • 可配置性: 支持通过注解进行高度的配置,满足各种复杂映射需求。
  • 高性能: 自动生成的代码经过优化,性能接近手写的映射代码,同时支持缓存策略,提高了映射的执行效率。

在Spring Boot应用中,数据的处理和转换是常见的任务。而MapStruct作为一个优秀的映射框架,为Spring Boot应用提供了以下优势:

  • 简化开发流程: 使用MapStruct可以大大减少手动编写转换逻辑的工作,使开发者能够更专注于业务逻辑的实现。
  • 类型安全: Spring Boot注重类型安全,而MapStruct在编译期间就能够检查出潜在的类型问题,提高了代码的质量。
  • 维护性: 自动生成的映射代码结构清晰,易于维护。当实体类发生变化时,MapStruct会自动更新映射代码,减少了手动维护的成本。
  • 性能优势: 自动生成的映射代码经过优化,性能接近手写的映射代码。在大规模数据转换的场景下,MapStruct能够提供较好的性能表现。

MapStruct的特性:

  • 注解支持: MapStruct使用@Mapper注解标识接口,通过在接口的抽象方法上添加@Mapping等注解来配置映射规则。
  • 编译时生成代码: MapStruct在编译时生成映射代码,消除了运行时的性能开销,同时提高了代码的类型安全性。
  • 类型转换支持: 支持各种基本数据类型、集合类型以及自定义类型之间的转换。
  • 灵活的配置: 提供丰富的配置选项,允许开发者自定义映射行为,满足复杂业务场景的需求。

Spring Boot项目集成MapStruct

引入MapStruct依赖

首先,在Spring Boot项目中引入MapStruct依赖。在pom.xml文件中添加以下依赖:

        <!-- MapStruct 核心库 实现 Java 对象映射 -->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>1.3.1.Final</version>
        </dependency>
        <!-- mapstruct包含了运行时需要的类,而mapstruct-processor是注解处理器,用于在编译时生成映射代码 -->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>1.3.1.Final</version>
        </dependency>

这里使用了mapstruct和mapstruct-processor两个依赖。mapstruct包含了运行时需要的类,而mapstruct-processor是注解处理器,用于在编译时生成映射代码。

新建实体类(PO 和 BO)

package com.demo.springboot.model.po;

import lombok.Data;

import java.util.Date;

/**
 * @Data 注解会自动生成 Getter、Setter、equals、hashCode、toString 等方法,可以大大简化类的编写,减少了样板代码。
 */
@Data
public class StudentPO {
    private Integer id;
    private String name;
    private Integer age;
    private String sex;
    private Date birthday;
}
View Code
package com.demo.springboot.model.bo;

import lombok.Data;

import java.util.Date;

/**
 * @Data 注解会自动生成 Getter、Setter、equals、hashCode、toString 等方法,可以大大简化类的编写,减少了样板代码。
 */
@Data
public class StudentBO {
    private Integer id;
    private String name;
    private Integer age;
    private String sex;
    private Date birthday;
}
View Code

创建 Mapper 接口 

现在需要定义“翻译规则”。你只需要创建一个接口,剩下的交给 MapStruct。

package com.demo.springboot.converter;

import com.demo.springboot.model.bo.StudentBO;
import com.demo.springboot.model.po.StudentPO;
import org.apache.ibatis.jdbc.Null;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

import java.util.List;

/**
 * MapStruct 是一个 Java 注解处理器,用于在编译时自动生成类型安全、高性能的对象映射代码
 * 只需要创建一个接口,剩下的交给 MapStruct
 * 通过设置 componentModel = “spring”,mapstruct 会在生成的实现类上添加 @Component注解,使其成为 Spring 管理的 Bean,从而可以在其他组件中通过依赖注入方式使用
 */
@Mapper(uses = {Null.class}, componentModel = "spring")
public interface StudentConverter {
    /**
     * 将单个 DTO 转换为实体。
     * MapStruct 会自动映射所有同名、同类型的字段。
     * 我们只需为有特殊转换需求的字段(如日期格式)添加规则。
     * @param dto 源 DTO 对象
     * @return 转换后的实体对象
     */
    @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd'T'HH:mm:ss")
    StudentBO toDataProduct(StudentPO dto);

    /**
     * 将 DTO 列表转换为实体列表。
     * MapStruct 会自动复用上面的 toEntity 方法来处理列表中的每一个元素。
     *
     * @param dtoList 源 DTO 列表
     * @return 转换后的实体列表
     */
    List<StudentBO> toDataProductList(List<StudentPO> dtoList);
}
View Code

在 Service 中使用 Mapper

可以在业务逻辑中,用一行代码替代原来繁琐的转换过程。

package com.demo.springboot.controller;

import com.demo.springboot.converter.StudentConverter;
import com.demo.springboot.model.bo.StudentBO;
import com.demo.springboot.model.po.StudentPO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

/**
 * 测试控制器
 * @RestController 该注解是 @Controller 和 @ResponseBody 注解的组合注解,注解在类上就意味着该Controller的所有方法都默认加上了@ResponseBody。
 * @ResponseBody 该注解将java对象转为json格式的数据,使用此注解之后不会再走视图处理器 , 可以把后台传到前端的数据自动转换为json
 */
@RestController
public class HelloController {
   
    @Autowired
    private StudentConverter studentConverter;

    @RequestMapping("hello")
    public String hello() {
        //在Spring5.3之后,StringUtils.isEmpty()被标记为过时,官方推荐使用hasLength(String)或hasText(String)作为替代
        boolean empty = StringUtils.hasText("");
        System.out.println(StringUtils.isEmpty(""));
        System.out.println(StringUtils.hasText(""));
        System.out.println(StringUtils.hasLength(""));
        //将字符串按指定分隔符分割为字符串数组
        String[] split = StringUtils.tokenizeToStringArray("ni shi ge cao bao", " ");
        for (String s:split) {
            System.out.println(s);
        }
        //使用 MapStruct 实现 Java 对象映射
        List<StudentPO> poList = new ArrayList<>();
        StudentPO student = new StudentPO();
        student.setId(1);
        student.setName("张三");
        poList.add(student);
        //将PO转换为BO
        List<StudentBO> boList = studentConverter.toDataProductList(poList);
        System.out.println("将PO转换为BO后集合大小:" + boList.size());      
        return "Hello Spring Boot!";
    }
}
View Code

注解介绍:@Mapper@Mapping

@Mapper注解

@Mapper注解用于标识一个接口为MapStruct映射接口。通过componentModel属性,可以指定生成的映射实现类的组件模型,如Spring的componentModel = "spring"

@Mapper(uses = {Null.class}, componentModel = "spring")
public interface StudentConverter {
    // 映射方法定义
}
@Mapping注解

@Mapping注解用于配置字段之间的映射关系。可以指定源属性、目标属性、以及转换表达式等。

基本数据类型转换

在Spring Boot项目中,经常会涉及到基本数据类型的转换,例如整数到字符串的转换、日期类型的处理等。MapStruct可以通过简单的注解配置来实现这些转换。

@Mapper(uses = {Null.class}, componentModel = "spring")
public interface StudentConverter {
    /**
     * 将单个 DTO 转换为实体。
     * MapStruct 会自动映射所有同名、同类型的字段。
     * 我们只需为有特殊转换需求的字段(如日期格式)添加规则。
     * @param dto 源 DTO 对象
     * @return 转换后的实体对象
     */
    @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd'T'HH:mm:ss")
    StudentBO toDataProduct(StudentPO dto);

    /**
     * 将 DTO 列表转换为实体列表。
     * MapStruct 会自动复用上面的 toEntity 方法来处理列表中的每一个元素。
     *
     * @param dtoList 源 DTO 列表
     * @return 转换后的实体列表
     */
    List<StudentBO> toDataProductList(List<StudentPO> dtoList);
}
自定义映射方法
@Mapper(componentModel = "spring")
public interface CustomMapper {
    @Mapping(target = "status", expression = "java(order.getStatus().getCode())")
    OrderDTO orderToOrderDTO(Order order);
}

字符串与枚举的转换

在实际项目中,枚举类型的处理是常见的需求。MapStruct支持字符串与枚举类型之间的转换,通过qualifiedByName注解实现

@Mapper(componentModel = "spring")
public interface OrderMapper {
    @Mapping(source = "status", target = "statusStr", qualifiedByName = "mapStatusEnumToString")
    OrderDTO orderToOrderDTO(Order order);

    @Named("mapStatusEnumToString")
    static String mapStatusEnumToString(OrderStatus status) {
        return status.toString();
    }
}

在上述例子中,我们通过qualifiedByName注解引用了一个自定义的静态方法mapStatusEnumToString,该方法负责将枚举类型转换为字符串。 

嵌套对象的映射

当实体类中存在嵌套对象关系时,MapStruct可以轻松处理这种情况。 

@Mapper(componentModel = "spring")
public interface OrderMapper {
    @Mapping(source = "customer.name", target = "customerName")
    OrderDTO orderToOrderDTO(Order order);
}

在上述例子中,我们通过customer.name指定了嵌套对象customer中的name属性到目标对象的映射。

集合类型的转换

处理集合类型是MapStruct的又一强项。例如,将订单列表转换为订单DTO列表。

@Mapper(componentModel = "spring")
public interface OrderMapper {
    OrderDTO orderToOrderDTO(Order order);
    
    List<OrderDTO> ordersToOrderDTOs(List<Order> orders);
}

MapStruct会递归处理集合中的元素,自动调用相应的映射方法完成转换。

映射规则自定义

在某些情况下,我们需要自定义映射规则,MapStruct允许通过自定义方法实现。

@Mapper(componentModel = "spring")
public interface CustomMapper {
    @Mapping(target = "status", expression = "java(mapStatus(order.getStatus()))")
    OrderDTO orderToOrderDTO(Order order);

    default String mapStatus(OrderStatus status) {
        // 自定义映射逻辑
        return status.toString();
    }
}

在上述例子中,我们通过expression属性调用了自定义方法mapStatus,实现了对订单状态的自定义映射。

条件映射与表达式

MapStruct支持条件映射和表达式的使用,可以根据某些条件决定是否进行映射,或者在映射过程中使用表达式进行计算。

@Mapper(componentModel = "spring")
public interface OrderMapper {
    @Mapping(target = "status", expression = "java(mapStatus(order.getStatus()))")
    OrderDTO orderToOrderDTO(Order order);

    @Mapping(target = "discount", source = "totalAmount", condition = "java(order.getTotalAmount() > 1000)")
    OrderDTO orderToOrderDTOWithDiscount(Order order);

    default String mapStatus(OrderStatus status) {
        // 自定义映射逻辑
        return status.toString();
    }
}

在上述例子中,通过condition属性,我们指定了映射discount属性的条件,只有在totalAmount大于1000时才进行映射。同时,通过expression属性,我们调用了自定义方法mapStatus实现了对状态的自定义映射。

高级配置

MapStruct提供了许多高级配置选项,可以在@Mapper注解中进行设置。例如,可以通过componentModel属性配置生成的映射实现类的组件模型,可以选择使用defaultComponentModel属性配置默认的组件模型。

@Mapper(componentModel = "spring", uses = {AnotherMapper.class}, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface OrderMapper {
    OrderDTO orderToOrderDTO(Order order);
}

在上述例子中,我们配置了使用Spring的组件模型,并且通过uses属性引入了另一个映射器AnotherMapper,通过injectionStrategy属性配置了依赖注入的策略为构造函数注入。

自定义转换器

在某些情况下,MapStruct的默认转换规则无法满足需求,这时可以使用自定义转换器。自定义转换器是一个带有@Mapper注解的类,其中包含了一些自定义的映射方法。

@Mapper(componentModel = "spring", uses = {CustomConverter.class})
public interface OrderMapper {
    OrderDTO orderToOrderDTO(Order order);
}

在上述例子中,我们通过uses属性引入了自定义转换器CustomConverter,MapStruct将使用该转换器中的方法进行相应类型的转换。

性能优化

MapStruct性能对比

MapStruct通过在编译时生成映射代码,避免了运行时的性能开销。在大规模数据转换的场景下,MapStruct通常表现出色,并且通过合理的配置可以进一步提升性能。

缓存策略配置

MapStruct提供了缓存策略,可以通过unmappedTargetPolicy属性配置未映射字段的处理方式。例如,通过配置为IGNORE,可以忽略未映射的字段。

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface OrderMapper {
    OrderDTO orderToOrderDTO(Order order);
}

在上述例子中,我们配置了unmappedTargetPolicyIGNORE,即忽略未映射的字段。这样可以提高映射性能,同时避免了不必要的映射警告。

遇到的问题与解决方案

在实际应用中,可能会遇到一些问题,以下是一些可能的问题及解决方案:

  • 循环引用问题: 当实体类之间存在循环引用时,可能导致栈溢出或无限递归。解决方案包括在映射接口上使用@Context注解,或者通过配置@Mapping注解的ignore属性来避免。
  • 复杂的映射规则: 在涉及到复杂的映射规则时,可能需要使用自定义转换器或表达式,以满足特定业务需求。
  • 性能问题: 对于大规模的数据转换,可能需要考虑性能优化。可以通过合理配置MapStruct的缓存策略、选择合适的组件模型等方式进行优化。

  

posted @ 2025-08-22 18:12  以德为先  阅读(182)  评论(0)    收藏  举报