还在手写 DTO 转换?Spring Boot + MapStruct 让对象映射效率翻倍

137 阅读11分钟

在 Spring Boot 项目开发中,对象映射是高频场景,比如 DTO 与实体类、VO 与 DTO 之间的转换。传统映射方式存在诸多痛点,而 MapStruct 作为一款编译期对象映射工具, 能完美解决这些问题。本文将逐步讲解 MapStruct 与 Spring Boot 的整合及全方位使用。

一、为什么需要 MapStruct?

在日常开发中,我们经常需要在不同层的对象之间进行属性拷贝,比如将数据库查询得到的实体类(Entity)转换为对外提供接口的DTO(Data Transfer Object)。传统方式主要有以下几种,均存在明显问题。

1.1 手动编写 set/get 方法

手动映射属性逻辑繁琐,代码冗余,当对象属性较多或属性名发生变化时,维护成本极高,且易出现漏写、错写的情况。

示例代码

// 实体类
@Data
public class UserEntity {
    private Long id;
    private String username;
    private String password;
    private Integer age;
    private LocalDateTime createTime;
}
// DTO类
@Data
public class UserDto {
    private Long id;
    private String userName; // 与实体类属性名不一致(username vs userName)
    private Integer age;
    private String createTimeStr; // 类型不一致(LocalDateTime vs String)
}
// 手动映射工具类
public class UserConvertor {
    public static UserDto entityToDto(UserEntity entity) {
        if (entity == null) {
            return null;
        }
        UserDto dto = new UserDto();
        dto.setId(entity.getId());
        // 属性名不一致,需手动对应
        dto.setUserName(entity.getUsername());
        dto.setAge(entity.getAge());
        // 类型转换,需手动处理格式
        dto.setCreateTimeStr(entity.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        return dto;
    }
}

1.2 使用 BeanUtils 等反射工具

Spring 的 BeanUtils、Apache 的 BeanUtils 等工具基于反射实现自动映射,虽减少了代码量,但存在以下问题:

  • 反射性能较差,尤其在循环映射大量对象时,性能损耗明显;
  • 无法处理属性名不一致、类型不匹配的场景,需额外手动补充;
  • 不支持复杂映射逻辑(如表达式、自定义转换);
  • 编译期无法校验映射正确性,运行时才可能暴露问题。

二、MapStruct 如何解决问题?

MapStruct 是一款基于编译期生成代码的对象映射工具,核心原理是在编译时根据注解生成对应的映射实现类,底层采用原生 set/get 方法,而非反射,兼具性能与灵活性。 其优势如下:

  • 编译期生成代码,性能与手动编写一致;
  • 支持属性名映射、类型自动转换,支持自定义转换逻辑;
  • 编译期校验映射合法性,提前暴露问题;
  • 注解驱动,配置简洁,易维护;
  • 支持复杂场景(多源映射、表达式、集合映射等)。

示例代码(MapStruct 解决方案)

// 1. 引入依赖后,定义映射接口
@Mapper 
public interface UserMapper {
    // 单例实例(非Spring环境使用)
    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
    // 映射方法,通过注解配置特殊规则
    @Mapping(target = "userName", source = "username") // 解决属性名不一致
    @Mapping(target = "createTimeStr", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss") // 日期类型转换
    UserDto entityToDto(UserEntity entity);
}

编译后,MapStruct 会自动生成 UserMapperImpl 实现类,核心代码如下(无需手动编写):

@Component
public class UserMapperImpl implements UserMapper {
    @Override
    public UserDto entityToDto(UserEntity entity) {
        if (entity == null) {
            return null;
        }
        UserDto userDto = new UserDto();
        userDto.setId(entity.getId());
        userDto.setUserName(entity.getUsername()); // 对应@Mapping配置
        userDto.setAge(entity.getAge());
        if (entity.getCreateTime() != null) {
            // 对应dateFormat配置,自动格式化日期
            userDto.setCreateTimeStr(entity.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        }
        return userDto;
    }
}

在 Spring 环境中,直接注入使用即可:

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    public UserDto getUserById(Long id) {
        UserEntity entity = userRepository.findById(id).orElseThrow();
        return userMapper.entityToDto(entity); // 直接调用映射方法
    }
}

三、MapStruct 安装配置

3.1 依赖配置(Maven)

-Amapstruct.defaultComponentModel=spring 这个很重要,目的是让生成类注入spring容器


<properties>
    <org.mapstruct.version>1.6.3</org.mapstruct.version>
</properties>

<!-- MapStruct 核心依赖 -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.6.3</version> <!-- 建议使用最新稳定版 -->
</dependency><!-- 注解处理器,编译期生成代码 -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.6.3</version>
    <scope>provided</scope>
</dependency>

<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>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
		<compilerArgs>
		    <arg>
		        <!-- 注入spring容器 -->
			   -Amapstruct.defaultComponentModel=spring
		    </arg>
		    <arg>-parameters</arg>
		</compilerArgs>
            </configuration>
        </plugin>
    </plugins>
</build>
...

3.2 插件安装(IDE)

IntelliJ IDEA

  1. 打开 File > Settings > Plugins
  2. 搜索 MapStruct Support,点击安装并重启 IDE;
  3. 确保注解处理器启用:File > Settings > Build, Execution, Deployment > Compiler > Annotation Processors,勾选 Enable annotation processing

Eclipse

  1. 打开 Help > Eclipse Marketplace
  2. 搜索 MapStruct Eclipse Plugin,点击安装并重启 IDE;
  3. 配置注解处理器:右键项目 > Properties > Java Compiler > Annotation Processing,勾选 Enable annotation processing
  4. Annotation Processing > Factory Path 中,添加 mapstruct-processor-${version}.jar 依赖。

四、MapStruct 详细使用

4.1 @Mapping 详细使用

@Mapping 是 MapStruct 核心注解,用于配置单个属性的映射规则,支持多种复杂场景。

4.1.1 基础属性映射

@Mapper
public interface UserMapper {
    // 属性名不一致
    @Mapping(target = "userName", source = "username") 
    // 日期格式化
    @Mapping(target = "createTimeStr", source = "createTime", dateFormat = "yyyy-MM-dd") 
    // 数字格式化
    @Mapping(target = "priceStr", source = "price", numberFormat = "#,##0.00") 
   //嵌套源bean映射,可使用“.”作为目标。这将指示MapStruct将源bean的所有属性映射到目标对象
   @Mapping( target = ".", source = "account" )
    UserDto entityToDto(UserEntity entity);
}

4.1.2 添加自定义方法

当自动映射无法满足需求时,可在映射接口中定义默认方法或抽象类方法,实现自定义映射逻辑。当参数类型与返回类型匹配时,自动使用该方法

// 方式1:接口默认方法(Java 8+)
@Mapper
public interface UserMapper {
    @Mapping(target = "fullName", source = "username")
    UserDto entityToDto(UserEntity entity);
    // 自定义方法,处理复杂逻辑
    default String encryptPassword(String password) {
        // 模拟密码加密逻辑
        return DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8));
    }
}
// 方式2:抽象类(支持定义字段和复杂逻辑)
@Mapper
public abstract class UserAbstractMapper {
    // 可定义字段
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Mapping(target = "fullName", source = "username")
    @Mapping(target = "password", expression = "java(encryptPassword(entity.getPassword()))")
    public abstract UserDto entityToDto(UserEntity entity);
    // 自定义映射方法,可依赖Spring组件
    protected String encryptPassword(String password) {
        return passwordEncoder.encode(password);
    }
}

4.1.3 多源参数映射

支持将多个源对象的属性映射到同一个目标对象,适用于组合数据场景。

// 源对象1
@Data
public class UserEntity {
    private Long id;
    private String username;
}
// 源对象2
@Data
public class UserExtEntity {
    private Integer age;
    private String email;
}
// 目标DTO
@Data
public class UserFullDto {
    private Long id;
    private String username;
    private Integer age;
    private String email;
}
// 映射接口
@Mapper
public interface UserFullMapper {
    @Mapping(target = "id", source = "user.id")
    @Mapping(target = "username", source = "user.username")
    @Mapping(target = "age", source = "userExt.age")
    @Mapping(target = "email", source = "userExt.email")
    UserFullDto combineToDto(UserEntity user, UserExtEntity userExt);
}

4.1.4 直接引用源参数

支持将源参数本身(非属性)直接映射到目标属性,适用于简单参数映射场景。

@Mapper
public interface UserMapper {
    // 将参数hn(Integer类型)直接映射到target的houseNumber属性
    @Mapping(target = "houseNumber", source = "hn")
    UserAddressDto buildAddressDto(UserEntity user, Integer hn);
}

4.1.5 Map 转 Bean

支持将 Map<String, ?> 映射到实体类,通过键名匹配属性名。

@Mapper
public interface MapToBeanMapper {
    @Mapping(target = "username", source = "userName") // Map键名是userName,目标属性是username
    UserEntity mapToEntity(Map<String, Object> map);
}

4.1.6 构造函数映射

MapStruct 支持通过构造函数实例化目标对象,优先选择单参数构造函数,或通过@Default 注解指定构造函数。

// 目标对象(无无参构造,有带参构造)
@Data
public class UserDto {
    private Long id;
    private String username;
    // 带参构造
    public UserDto(Long id, String username) {
        this.id = id;
        this.username = username;
    }
}
// 映射接口
@Mapper
public interface UserMapper {
    // 自动匹配构造函数参数
    UserDto entityToDto(UserEntity entity);
}

4.1.7 @Mapping.expression 使用

支持通过 Java 表达式实现复杂映射逻辑,适用于动态计算属性值场景。

@Mapper(imports = {StringUtils.class}) // 导入需要的工具类
public interface UserMapper {
    @Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())")
    @Mapping(target = "isAdult", expression = "java(user.getAge() >= 18)")
    @Mapping(target = "email", expression = "java(StringUtils.lowerCase(user.getEmail()))")
    UserDto entityToDto(UserEntity user);
}

4.2 @Mapper.uses 使用

@Mapper(uses = ...) 用于引入外部映射器或自定义转换类,实现映射逻辑复用。

// 自定义日期转换类
public class DateConvertor {
    //参数类型和返回类型匹配的字段都会调用该方法
    public String localDateTimeToString(LocalDateTime dateTime) {
        return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
}
// 映射接口,引入外部转换类
@Mapper(uses = {DateConvertor.class})
public interface UserMapper {
    // 自动使用DateConvertor中的方法进行类型转换
    UserDto entityToDto(UserEntity entity);
}

4.3 @Context 使用

@Context 用于传递上下文信息(如用户信息、配置参数),上下文参数会贯穿整个映射过程,支持在自定义方法、生命周期方法中使用。

// 上下文类
@Data
public class MappingContext {
    private Long operatorId; // 操作人ID
    private LocalDateTime operateTime; // 操作时间
}
// 映射接口
@Mapper
public interface UserMapper {
    // 上下文参数添加@Context注解
    @Mapping(target = "operatorId", source = "context.operatorId")
    @Mapping(target = "operateTime", source = "context.operateTime")
    UserDto entityToDto(UserEntity entity, @Context MappingContext context);
    // 自定义方法中使用上下文
    default String buildRemark(Sring remark,@Context MappingContext context) {
        return "操作人:" + context.getOperatorId() + ",操作时间:" + context.getOperateTime() + "," + remark;
    }
}

4.4 @Named 使用

@Named 用于给映射方法标记别名,当存在多个同类型映射方法时,通过别名指定使用哪个方法,解决歧义。

@Mapper
public interface UserMapper {
    // 标记别名
    @Named("encryptPassword")
    default String encryptPassword(String password) {
        return DigestUtils.md5DigestAsHex(password.getBytes());
    }
    @Named("plainPassword")
    default String plainPassword(String password) {
        return password;
    }
    // 通过qualifiedByName指定使用的方法
    @Mapping(target = "password", source = "password", qualifiedByName = "encryptPassword")
    UserDto entityToDtoWithEncrypt(UserEntity entity);
    @Mapping(target = "password", source = "password", qualifiedByName = "plainPassword")
    UserDto entityToDtoWithPlain(UserEntity entity);
}

//使用单独映射器类

@Named("TitleTranslator")
public class Titles {

    @Named("EnglishToGerman")
    public String translateTitleEG(String title) {
        // some mapping logic
    }

    @Named("GermanToEnglish")
    public String translateTitleGE(String title) {
        // some mapping logic
    }
}


@Mapper( uses = Titles.class )
public interface MovieMapper {

     @Mapping( target = "title", qualifiedByName = { "TitleTranslator", "EnglishToGerman" } )
     GermanRelease toGerman( OriginalRelease movies );

}

4.5 @MapMapping 使用

@MapMapping 用于配置 Map 类型的映射规则,支持键/值的格式化、类型转换。

@Mapper
public interface MapMapper {
    // 配置值的日期格式化
    @MapMapping(valueDateFormat = "yyyy-MM-dd")
    Map<String, String> longDateMapToStringMap(Map<Long, LocalDate> sourceMap);
}

4.6 使用条件映射

  • @Condition 用于定义条件映射规则,只有某个属性满足条件时才进行属性映射,支持自定义条件逻辑。
  • @SourceParameterCondition:参数级别的条件过滤,能精准控制哪些参数参与映射过程。(比如多源映射,过滤哪些源参与映射)
@Mapper
public interface UserMapper {
    // 自定义条件:字符串非空且非空白才映射
    @Condition
    default boolean isNonBlank(String value) {
        return StringUtils.isNotBlank(value);
    }
    // 只有满足isNonBlank条件时,才映射username属性
    @Mapping(target = "username", source = "username")
    UserDto entityToDto(UserEntity entity);
    
    //entity有效时才映射,否则返回null
    @SourceParameterCondition
    default boolean hasEntity(UserEntity entity) {
        return entity != null && entity.getId() != null;
    }
}

4.7 更新对象

某些情况下不想生成目标类型新实例、而是更新现有实例的映射。此类映射可通过为目标对象添加参数并标注@MappingTarget实现


@Mapperpublic interface CarMapper {
    //使用给定 CarDto 对象的属性更新传入的 Car 实例
    void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
    // 可以返回目标实例
    Car updateCarFromDtoAndReturn(CarDto carDto, @MappingTarget Car car);

}

五、常用注解说明

注解作用关键属性
@Mapper标记接口/抽象类为映射器,编译期生成实现类componentModel(组件模型,如spring)、uses(引入外部映射器)、imports(导入类)
@Mapping配置单个属性的映射规则target(目标属性)、source(源属性)、dateFormat(日期格式)、numberFormat(数字格式)、expression(表达式)、ignore(是否忽略)
@IterableMapping配置集合类型的映射规则elementTargetType(元素目标类型)、dateFormat、numberFormat
@Context标记上下文参数,用于传递映射过程中的附加信息
@Named给映射方法标记别名,解决多方法歧义value(别名)
@MapMapping配置Map类型的映射规则keyDateFormat、valueDateFormat、keyTargetType、valueTargetType
@ValueMapping配置枚举值的映射规则source(源枚举值)、target(目标枚举值)
@EnumMapping配置枚举类型的整体映射策略nameTransformationStrategy(名称转换策略)、mappingStrategy(映射策略)
@InheritConfiguration继承其他映射方法的配置name(要继承的方法名)
@InheritInverseConfiguration继承反向映射方法的配置(如DTO转Entity继承Entity转DTO的规则)name(要继承的反向方法名)
@SubclassMapping配置子类的映射规则,支持多态映射source(源子类)、target(目标子类)
@BeanMapping配置Bean级别的映射规则resultType(结果类型)、nullValueMappingStrategy(空值映射策略)
@SourcePropertyName在条件方法(@Condition)中获取源属性名
@TargetPropertyName在条件方法(@Condition)中获取目标属性名
@SourceParameterCondition标记源参数的条件方法,控制是否映射该参数
@MappingTarget标记目标对象,用于更新现有对象(而非创建新对象)
@MapperConfig定义共享映射配置,供多个@Mapper继承与@Mapper属性一致,支持全局配置
@DecoratedWith给映射器添加装饰器,自定义映射逻辑value(装饰器类)
@BeforeMapping标记映射前执行的生命周期方法
@AfterMapping标记映射后执行的生命周期方法

六、MapStruct SPI 简单介绍

MapStruct 提供 SPI(Service Provider Interface)扩展机制,允许开发者自定义映射行为,适配特殊场景。以下是常用 SPI 及说明:

6.1 org.mapstruct.ap.spi.AccessorNamingStrategy

自定义属性访问器命名策略,适用于非标准 JavaBean 命名规则的场景(如 Lombok 的 fluent 风格)。

使用示例(适配 Fluent 风格)

// 自定义访问器策略
public class FluentAccessorNamingStrategy implements AccessorNamingStrategy {
    @Override
    public boolean isGetter(ExecutableElement method) {
        // Fluent风格getter无前缀,返回当前对象类型
        return !method.getParameters().isEmpty() == false
                && method.getReturnType().equals(method.getEnclosingElement().asType());
    }
    @Override
    public String getPropertyName(ExecutableElement getterOrSetterMethod) {
        // 直接使用方法名作为属性名
        return getterOrSetterMethod.getSimpleName().toString();
    }
    // 其他方法实现...
}
// 配置SPI:在META-INF/services下创建文件org.mapstruct.ap.spi.AccessorNamingStrategy
// 文件内容:com.example.mapstruct.spi.FluentAccessorNamingStrategy

6.2 org.mapstruct.ap.spi.MappingExclusionProvider

自定义排除映射的属性,用于过滤不需要映射的属性(如特定前缀、注解标记的属性)。

6.3 org.mapstruct.ap.spi.BuilderProvider

自定义构建器(Builder)探测策略,支持非标准 Builder 模式的对象映射(如自定义 Builder 方法名)。

6.4 org.mapstruct.ap.spi.EnumMappingStrategy

自定义枚举映射策略,控制枚举值的映射规则(如按名称、按序数)。

6.5 org.mapstruct.ap.spi.EnumTransformationStrategy

自定义枚举名称转换策略,支持枚举值名称的自动转换(如大小写、下划线转驼峰)。

6.6 org.mapstruct.ap.spi.AdditionalSupportedOptionsProvider

扩展 MapStruct 支持的自定义选项,允许添加自定义配置参数。

七、与 Lombok 协同

MapStruct 与 Lombok 协同使用时,需配置正确的注解处理器顺序,否则可能出现编译异常(如 Lombok 生成的 get/set 方法无法被 MapStruct 识别)。

7.1 依赖配置(Maven)

<properties>
    <org.mapstruct.version>1.6.3</org.mapstruct.version>
    <org.projectlombok.version>1.18.30</org.projectlombok.version>
</properties>
<dependencies>
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${org.projectlombok.version}</version>
        <scope>provided</scope>
    </dependency><!-- MapStruct -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>
<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>
                <annotationProcessorPaths><!-- 必须按此顺序配置:lombok → lombok-mapstruct-binding → mapstruct-processor -->
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>${org.projectlombok.version}</version>
                        </path>
                    <!-- Lombok 1.18.16+ 必须添加此绑定包 -->
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok-mapstruct-binding</artifactId>
                        <version>0.2.0</version>
                    </path>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

7.2 注意事项

  • 注解处理器顺序不可颠倒,必须先执行 Lombok 生成 get/set 方法,再执行 MapStruct 生成映射代码;
  • Lombok 版本需与 mapstruct-processor 版本兼容,建议使用最新稳定版;
  • 若使用 Lombok 的 @Builder 注解,MapStruct 可自动探测 Builder 并生成对应映射代码。

总结

MapStruct 作为编译期对象映射工具,在 Spring Boot 项目中能高效解决对象转换问题,兼具性能、灵活性与可维护性。通过本文的配置与示例,可快速上手 MapStruct 并应用于实际开发,大幅减少重复代码,提升开发效率。