SpringBoot 对象转换 MapStruct
文章目录
- 工作原理
- 核心优势
- 为什么不使用 `BeanUtils`
- 使用步骤
- 添加依赖
- 定义实体类和VO类
- 定义映射接口
- 测试数据
- 参考
工作原理
基于 Java 的 JSR 269 规范,该规范允许在编译期处理注解,也就是 Java 注解处理器。MapStruct 通过定义的注解处理器,在编译期读取映射接口,并生成相应的实现类。这个过程中,它会解析接口中声明的映射方法,并创建对应的 getters 和 setters 调用
核心优势
- 零反射:生成的代码直接调用对象的
getter/setter
,性能接近手写代码。 - 编译时检查:映射错误(如字段不匹配)在编译时暴露,而非运行时。
- 灵活性:支持自定义转换逻辑、嵌套对象映射、集合转换等。
- 与 IDE 兼容:生成的代码可调试,便于跟踪问题。
为什么不使用 BeanUtils
在高并发的场景中,性能是最为重要的,BeanUtils
虽然可以快速完成 JavaBean 之间的转换,但是底层逻辑是基于反射实现的,这样会导致在高并发场景中性能下降,这时候最高效的处理办法就是手动的 getter/setter
,但是要大量处理这些可重复的操作会浪费大量时间,因此可以使用 MapStruct
解决
区别:
- 编译时生成代码 vs 运行时反射:
MapStruct
生成的映射代码是在编译时生成的,而BeanUtils
则是在运行时使用反射机制实现转换。 - 性能和可扩展性:由于 MapStruct 生成的代码是类型安全的,因此可以比使用反射更加高效和可靠。同时,MapStruct 还能够自定义转换逻辑并支持扩展,使得它更加灵活和可扩展。
- 集成方式:MapStruct 可以无缝集成到 Spring 中,也可以与其他 IoC 容器结合使用;而 BeanUtils 是 Spring 框架自带的工具类。
- 映射规则的定义方式:MapStruct 使用基于注解的方式在源代码中定义映射规则,而 BeanUtils 则需要手动编写复杂的转换方法。
使用步骤
添加依赖
在 pom.xml
中添加依赖
<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>1.5.5.Final</version>
</dependency>
<!-- 注解处理器 -->
<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>1.5.5.Final</version><scope>provided</scope>
</dependency>
mapstruct
依赖- 提供了 MapStruct 的核心注解,如
@Mapper
和@Mapping
,用于定义映射接口 - 在运行时,如果使用默认的组件模型(default),还需要依赖 Mappers.getMapper(…) 方法来获取映射器实例
- 提供了 MapStruct 的核心注解,如
mapstruct-processor
依赖- 注解处理器(annotation processor),在编译阶段扫描带有 MapStruct 注解的接口,并生成对应的实现类
- 不会在运行时参与应用程序的执行,因此通常不需要在运行时包含此依赖
根据 mapstruct-processor
依赖的定义,其实不应该将该依赖放在 dependencies
标签中,而是将 mapstruct-processor
作为注解处理器添加到 maven-compiler-plugin 的 annotationProcessorPaths 中,这也是官方推荐处理
...
<properties><org.mapstruct.version>1.6.3</org.mapstruct.version>
</properties>
...
<dependencies><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><path><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>${org.mapstruct.version}</version></path></annotationProcessorPaths></configuration></plugin></plugins>
</build>
...
如果是在已经有 Lombok
依赖的项目中加入 mapstruct
依赖需要注意这两个注解处理器的配置,不然在 mvn install
等命令执行时可能会发生关于 Lombok
依赖相关的错误
Lombok 1.18.16 引入了一个破坏性更改(变更日志)。必须添加额外的注解处理器
lombok-mapstruct-binding
(Maven),否则 MapStruct 将无法与 Lombok 兼容。
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.13.0</version><configuration><source>17</source><target>17</target><annotationProcessorPaths><path><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>${mapstruct.version}</version></path><path><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></path><!-- Lombok 版本从 1.18.16 开始必须添加,其他版本为可选 --><!-- Lombok 与 MapStruct 的绑定处理器 --><path><groupId>org.projectlombok</groupId><artifactId>lombok-mapstruct-binding</artifactId><version>0.2.0</version></path></annotationProcessorPaths></configuration></plugin></plugins>
</build>
定义实体类和VO类
// 实体类
@Data
@Accessors(chain = true)
public class Student implements Serializable { private Long id; private String name; private Integer age; private String studentNo; }// VO类
@Data
@Accessors(chain = true)
public class StudentVO implements Serializable { private Long voId; private String voName; private Integer voAge; private String voStudentNo; }
定义映射接口
定义抽象接口
@MapperConfig
public interface IMapping<SOURCE, TARGET> { TARGET sourceToTarget(SOURCE source); // 反向映射(需配置反向方法)@InheritInverseConfiguration(name = "sourceToTarget") SOURCE targetToSource(TARGET target); @InheritConfiguration(name = "sourceToTarget") List<TARGET> sourceToTarget(List<SOURCE> sourceList); @InheritConfiguration(name = "sourceToTarget") List<SOURCE> targetToSource(List<TARGET> targetList); List<TARGET> sourceToTarget(Stream<SOURCE> stream); List<SOURCE> targetToSource(Stream<TARGET> stream); }
定义映射接口,@Mapper(componentModel = "spring")
。默认情况下,mapstruct
生成的 Mapper 实现类不会被 Spring 容器管理。如果不指定 componentModel
,需要通过 StudentMapping mapper = Mappers.getMapper(StudentMapping.class)
手动获取 Mapper 实例。
通过设置 componentModel = "spring"
,mapstruct
会在生成的实现类上添加 @Component
注解,使其成为 Spring 管理的 Bean,从而可以在其他组件中通过依赖注入方式使用。因此,StudentMapping
接口通过 @Mapper(componentModel = "spring")
注解,其实现类被注册为 SpringBean
,可以通过构造函数注入方式使用该 Mapper
。
@Mapper(componentModel = "spring")
public interface StudentMapping extends IMapping<Student, StudentVO>{ @Override @Mapping(source = "id", target = "voId") @Mapping(source = "name", target = "voName") @Mapping(source = "age", target = "voAge") @Mapping(source = "studentNo", target = "voStudentNo") StudentVO sourceToTarget(Student student); @Override List<StudentVO> sourceToTarget(List<Student> students);
}
测试数据
默认情况下,Spring 在实例化测试类时使用无参构造函数,并通过字段注入(@Autowired
)方式注入依赖。而使用 @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
注解后,Spring 会尝试使用带参数的构造函数,并通过构造函数注入方式自动注入所需的依赖,也就是可以使用 Lombok
依赖中的 @RequiredArgsConstructor
注解
@SpringBootTest
@RequiredArgsConstructor
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
public class StudentMapStructTest { private final StudentMapping studentMapping; @Test public void test() { Student student = new Student(); student.setId(1L); student.setName("Mayer"); student.setAge(18); student.setStudentNo("20250001"); StudentVO studentVO = studentMapping.sourceToTarget(student); System.out.println(studentVO); System.out.println(studentMapping.targetToSource(studentVO)); } }
打印结果
StudentVO(voId=1, voName=Mayer, voAge=18, voStudentNo=20250001)
Student(id=1, name=Mayer, age=18, studentNo=20250001)
参考
- Java-Mapstruct 实践 | 无垠之境
- Java Review - MapStruct 全掌握:8 个案例探究高效快捷的 Java 对象映射 - CharyGao - 博客园
- 【MapStruct】还在用BeanUtils?不如试试MapStruct - CharyGao - 博客园