什么是 MapStruct?
MapStruct 是一个 Java 注解处理器,用于生成类型安全的 bean 映射代码。它通过在编译时生成映射代码,避免了手动编写繁琐的对象转换代码,同时保证了高性能(因为生成的代码是普通的 Java 方法调用,没有反射开销)。
主要特点
- 高性能:编译时生成代码,无反射开销
- 类型安全:编译器会检查映射是否正确
- 易于调试:生成的代码是普通 Java 代码,可以轻松调试
- 配置灵活:支持自定义映射规则
- 丰富的注解:提供多种注解满足不同映射需求
- 支持多级映射
实践
1.引入依赖
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</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>1.5.5.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
2.简单类相互转换
1.两个user类
@Data
@I18nClassAnnotation
public class User {
public String id;
public String name;
public String desc;
public Integer age;
public BigDecimal count;
private List<String> a;
}
@Data
@Table(name = "`user`")
public class UserDTO {
private int id;
private String name;
private Date createdTime;
private BigDecimal number;
private List<String> a;
}
2.添加UserMapperStruct
package com.example.demo.mapstruct;
import com.example.demo.domain.User;
import com.example.demo.domain.po.UserDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* @author zhouxy
* @date 2025年04月10日 16:34
*/
@Mapper
public interface UserMapperStruct {
UserMapperStruct INSTANCE = Mappers.getMapper(UserMapperStruct.class);
/**
* 单个对象映射
*
* @author zhouxy
* @date 2025/4/11 10:26
*/
//属性转换
@Mapping(source = "count", target = "number")
//忽略某个属性赋值
@Mapping(target = "id", ignore = true)
UserDTO userToDTO(User user);
/**
* 集合映射
*
* @author zhouxy
* @date 2025/4/10 17:27
*/
//属性转换
@Mapping(source = "count", target = "number")
//忽略某个属性赋值
@Mapping(target = "id", ignore = true)
List<UserDTO> usersToDTOs(List<User> users);
}
3.测试
@Test
public void testMapping() {
User user = new User();
user.setId("3");
user.setName("张三");
user.setCount(BigDecimal.valueOf(2l));
user.setA(Arrays.asList("2","李四"));
UserDTO userDTO = UserMapperStruct.INSTANCE.userToDTO(user);
log.info("{}", JSON.toJSONString(userDTO));
}
测试结果:{"a":["2","李四"],"id":0,"name":"张三","number":2}
@Test
public void testListMapping() {
User user = new User();
user.setId("3");
user.setName("张三");
user.setCount(BigDecimal.valueOf(2l));
user.setA(Arrays.asList("2","李四"));
List<User> users = new ArrayList<>();
users.add(user);
List<UserDTO> userDTOs = UserMapperStruct.INSTANCE.usersToDTOs(users);
log.info("{}", JSON.toJSONString(userDTOs));
}
测试结果:[{"a":["2","李四"],"id":0,"name":"张三","number":2}]
3.复杂类相互转换
场景,代码中有一段是将各种机票改期数据整合出改期单的价格信息。如图:
//初始化价格详情
List<QueryChangeDetailResp.OrderPriceVo> prices = new ArrayList<>();
buildChangePrices(adtChangeDetails, "成人", currentChangeInfo, prices);
buildChangePrices(chdChangeDetails, "儿童", currentChangeInfo, prices);
buildChangePrices(infChangeDetails, "婴儿", currentChangeInfo, prices);
currentChangeInfo.setPrices(prices);
//参数说明
转换代码
private static void buildChangePrices(
List<TicketChangeDetail> changeDetails,
String type,
QueryChangeDetailResp.ChangeDetail currentChangeInfo,
List<QueryChangeDetailResp.OrderPriceVo> prices) {
if (CollectionUtils.isNotEmpty(changeDetails)) {
StringBuilder prdType = new StringBuilder();
prdType.append(type);
prdType.append("(");
prdType.append(currentChangeInfo.getOrgTrip().getTripType());
prdType.append("/");
prdType.append(currentChangeInfo.getOrgTrip().getTripIndex());
prdType.append(")");
QueryChangeDetailResp.OrderPriceVo adtPrice = new QueryChangeDetailResp.OrderPriceVo();
adtPrice.setPrdType(prdType.toString());
adtPrice.setDiffPrice(changeDetails.get(0).getDiffPrice());
adtPrice.setSize(changeDetails.size());
adtPrice.setTotalPrice(changeDetails.get(0).getPrice());
adtPrice.setChangeFee(changeDetails.get(0).getChangeFee());
prices.add(adtPrice);
}
}
prices的数据效果如下:
[{"changeFee":3,"diffPrice":2,"prdType":"成人(去程/第1程) ","size":2,"totalPrice":5}]
现在使用MapStruct实现上述代码,
首先,需要添加转换器ChangePriceMapperStruct
@Mapper
public interface ChangePriceMapperStruct {
ChangePriceMapperStruct INSTANCE = Mappers.getMapper(ChangePriceMapperStruct.class);
/**
* 复杂场景-@Mapping用法
* expression 和 source不能混用
*
* @author zhouxy
* @date 2025/4/10 18:32
*/
@Mapping(target = "prdType", expression = "java(type + \"(\" + trip.getTripType() + \"/第\" + trip.getTripIndex() + \"程) \")")
@Mapping(target = "size", expression = "java(changeDetails.size())")
@Mapping(target = "changeFee", expression = "java(changeDetails.get(0).getChangeFee())")
@Mapping(target = "diffPrice", expression = "java(changeDetails.get(0).getDiffPrice())")
@Mapping(target = "totalPrice", expression = "java(changeDetails.get(0).getPrice())")
QueryChangeDetailResp.OrderPriceVo toOrderPrice(List<TicketChangeDetail> changeDetails,
String type,
QueryChangeDetailResp.Trip trip,
QueryChangeDetailResp.ChangeDetail currentChangeInfo);
}
添加测试类
@Test
public void complexityMapping() {
List<TicketChangeDetail> changeDetails = new ArrayList<>();
TicketChangeDetail changeDetail1 = new TicketChangeDetail();
changeDetail1.setChangeFee(BigDecimal.valueOf(3));
changeDetail1.setDiffPrice(BigDecimal.valueOf(2));
changeDetail1.setPrice(BigDecimal.valueOf(5));
changeDetails.add(changeDetail1);
TicketChangeDetail changeDetail2 = new TicketChangeDetail();
changeDetail2.setChangeFee(BigDecimal.valueOf(3));
changeDetail2.setDiffPrice(BigDecimal.valueOf(2));
changeDetail2.setPrice(BigDecimal.valueOf(5));
changeDetails.add(changeDetail2);
String type = "成人";
QueryChangeDetailResp.Trip trip = new QueryChangeDetailResp.Trip();
trip.setTripIndex(1);
trip.setTripType("去程");
log.info("{}", JSON.toJSONString(ChangePriceMapperStruct.INSTANCE.toOrderPrice(changeDetails, type, trip, new QueryChangeDetailResp.ChangeDetail())));
//返回结果:{"changeFee":3,"diffPrice":2,"prdType":"成人","size":2,"totalPrice":5}
}
效果:
{"changeFee":3,"diffPrice":2,"prdType":"成人(去程/第1程) ","size":2,"totalPrice":5}
从结果可以看出,使用MapStruct返回的对象的结果,和使用java代码转换返回的集合中的对象的结果是一样的。
4.MapStruct和BeanUtils比较
前面两个例子只是将Java的属性赋值换了个写法,两者各有各的好处,在实际应用中就是萝卜白菜各有所爱了。但是与BeanUtils通过反射进行赋值的方式比较,MapStruct的性能优势就凸显出来了。
1. 核心机制对比
特性 | MapStruct | BeanUtils (Apache/Spring) |
---|---|---|
实现原理 | 编译时生成Java映射代码 | 运行时反射 |
代码可见性 | 生成可调试的Java类 | 黑箱操作,无可见代码 |
性能 | 接近手写代码的性能(无反射开销) | 反射调用,性能较差(尤其批量操作) |
类型安全 | 编译时检查类型匹配 | 运行时可能抛出异常 |
针对代码可见性/性能/类型安全简单举个例子:前面的两个MapStruct,比如ChangePriceMapperStruct,其实编译的时候,就会生成一个实现类,如下图所示。
转换为java代码,也就是运行时实际上跑的是上图的Java代码,如果编译时生成实现类代码有问题,就会直接报编译错误。
另附:原始接口方法
/**
* 复杂场景-@Mapping用法
* expression 和 source不能混用
*
* @author zhouxy
* @date 2025/4/10 18:32
*/
@Mapping(target = "prdType", expression = "java(type + \"(\" + trip.getTripType() + \"/第\" + trip.getTripIndex() + \"程) \")")
@Mapping(target = "size", expression = "java(changeDetails.size())")
@Mapping(target = "changeFee", expression = "java(changeDetails.get(0).getChangeFee())")
@Mapping(target = "diffPrice", expression = "java(changeDetails.get(0).getDiffPrice())")
@Mapping(target = "totalPrice", expression = "java(changeDetails.get(0).getPrice())")
QueryChangeDetailResp.OrderPriceVo toOrderPrice(List<TicketChangeDetail> changeDetails,
String type,
QueryChangeDetailResp.Trip trip,
QueryChangeDetailResp.ChangeDetail currentChangeInfo);
2. 性能对比,deepseek示例
基准测试示例(100万次映射)
// MapStruct 平均耗时:~50ms
CarDto dto = CarMapper.INSTANCE.carToDto(car);
// BeanUtils.copyProperties 平均耗时:~500ms
CarDto dto = new CarDto();
BeanUtils.copyProperties(car, dto);
✅ MapStruct 快 5-10 倍,差距在批量操作中更明显。
3.总结
- 选MapStruct:追求性能、类型安全、复杂映射
- 选BeanUtils:快速原型开发、简单POJO拷贝
- 混合使用:非关键路径用BeanUtils,核心业务用MapStruct
常见注解
注解 | 说明 |
---|---|
@Mapper | 标记接口为映射器 |
@Mapping | 定义字段映射规则 |
@Mappings | 包含多个@Mapping 注解 |
@MappingTarget | 标记目标对象用于更新操作 |
@BeforeMapping | 在映射前执行的方法 |
@AfterMapping | 在映射后执行的方法 |
@Named | 为映射方法命名,用于qualifiedByName |
缺点
- 无法在MapperStruct的接口类中使用@Slf4j
- @BeforeMapping设置前提条件,不支持拦截,只能通过报错或者上下文处理。
问题
问题一:引入mapStruct依赖之后,导致@Data注解不生效,如何解决
当 MapStruct 和 Lombok 一起使用时,可能会出现 @Data 注解不生效的情况,这是因为两个注解处理器的执行顺序问题导致的。可以使用下述方案解决:
- 正确配置注解处理器顺序(推荐)
<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 必须在前 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!-- 然后是 MapStruct -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
2.添加lombok以及mapStruct依赖
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
</dependencies>
lombok和mapStruct的版本适配,lombok推荐使用1.18.30+
MapStruct 版本 | 推荐的 Lombok 版本 | 说明 |
---|---|---|
1.4.x.Final | 1.18.16+ | 基本兼容 |
1.5.x.Final | 1.18.24+ | 最佳兼容 |
1.6.x.Beta | 1.18.30+ | 支持新特性 |