Java 项目里最烦的重复劳动之一就是实体转换------PO、DO、DTO、VO 之间互相转,字段多的类写几十行 get/set。BeanUtils 虽然简单但性能差、容易出 Bug。MapStruct 是编译期生成转换代码,零反射、高性能。
一、为什么不用 BeanUtils
java
// BeanUtils 的问题:
// 1. 性能差(反射,大量转换时慢)
// 2. 字段名不一致就静默失败
// 3. 类型不一致报错诡异
// 4. 没有编译期检查
User user = new User("张三", 25, "13912345678");
UserVO vo = new UserVO();
BeanUtils.copyProperties(user, vo); // 如果字段名对不上,你都不知道
二、MapStruct 入门
1. 引入依赖
xml
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
xml
<!-- Maven 编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
关键: mapstruct-processor 和 lombok 要一起配在 annotationProcessorPaths 里,否则 Lombok 和 MapStruct 会打架。
2. 最简单的转换
java
// Entity
@Data
public class User {
private Long id;
private String username;
private String password;
private String email;
private String phone;
private LocalDateTime createTime;
}
// VO(对外展示,不暴露密码)
@Data
public class UserVO {
private Long id;
private String username;
private String email;
private String phone;
private LocalDateTime createTime;
}
Mapper 接口------这是 MapStruct 的精髓:
java
@Mapper(componentModel = "spring")
public interface UserMapper {
// 实例(Spring 环境下注入使用)
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
// VO 转 Entity(同名同类型字段自动映射)
UserVO toVO(User user);
// Entity 转 VO(List 批量转换)
List<UserVO> toVOList(List<User> users);
}
使用:
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public UserVO getUser(Long id) {
User user = userMapper.selectById(id);
// 一行转换,比 BeanUtils 快 10 倍
return userMapper.toVO(user);
}
public List<UserVO> getUserList() {
List<User> users = userMapper.selectList(null);
return userMapper.toVOList(users);
}
}
三、字段名不一致
java
// Entity
@Data
public class Product {
private Long id;
private String productName; // 数据库字段:product_name
private BigDecimal productPrice;
}
// DTO
@Data
public class ProductDTO {
private Long id;
private String name; // 字段名不一样
private BigDecimal price; // 字段名不一样
}
java
@Mapper(componentModel = "spring")
public interface ProductMapper {
// 用 @Mapping 指定字段映射
@Mapping(source = "productName", target = "name")
@Mapping(source = "productPrice", target = "price")
ProductDTO toDTO(Product product);
// 批量转换
List<ProductDTO> toDTOList(List<Product> products);
}
四、类型不一致
java
// Entity
@Data
public class Order {
private Long id;
private LocalDateTime orderTime; // 数据库存的是 LocalDateTime
}
// DTO
@Data
public class OrderDTO {
private Long id;
private String orderTime; // 前端要的是字符串 "2026-06-30 14:30:00"
}
java
@Mapper(componentModel = "spring", imports = {LocalDateTimeUtil.class})
public interface OrderMapper {
@Mapping(target = "orderTime", expression = "java(formatTime(order.getOrderTime()))")
OrderDTO toDTO(Order order);
default String formatTime(LocalDateTime time) {
if (time == null) return "";
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return time.format(fmt);
}
}
五、忽略字段
java
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "password", ignore = true) // 密码不暴露
@Mapping(target = "createTime", ignore = true)
UserVO toVO(User user);
}
六、更新已有对象
java
@Mapper(componentModel = "spring")
public interface UserMapper {
// 把 DTO 的字段合并到 Entity 中(更新已存在的对象)
@Mapping(target = "id", ignore = true)
@Mapping(target = "password", ignore = true)
void updateEntity(UserDTO dto, @MappingTarget User user);
}
java
// 使用:前端传了修改后的 DTO,合并到已有 Entity
User user = userService.getById(1L);
userMapper.updateEntity(dto, user); // dto 的非空字段合并到 user
userService.updateById(user);
七、多个对象合并
java
@Mapper(componentModel = "spring")
public interface OrderDetailMapper {
// 把 Order 和 User 合并成 OrderDetailVO
@Mapping(source = "order.id", target = "orderId")
@Mapping(source = "order.orderNo", target = "orderNo")
@Mapping(source = "user.username", target = "userName")
@Mapping(source = "user.phone", target = "userPhone")
OrderDetailVO toOrderDetail(Order order, User user);
}
八、自定义类型转换
java
@Mapper(componentModel = "spring")
public interface ProductMapper {
// 自定义转换方法
default String bigDecimalToString(BigDecimal value) {
if (value == null) return "0.00";
return value.setScale(2, RoundingMode.HALF_UP).toString();
}
// MapStruct 会自动调用上面的方法
@Mapping(target = "price", source = "productPrice")
ProductDTO toDTO(Product product);
}
九、性能对比
| 工具 | 原理 | 性能 | 编译期检查 |
|---|---|---|---|
| 手写 get/set | 硬编码 | ⭐⭐⭐⭐⭐ 最快 | ✅ |
| MapStruct | 编译期生成代码 | ⭐⭐⭐⭐⭐ | ✅ |
| BeanUtils | 运行时反射 | ⭐⭐ 慢 | ❌ |
| Spring BeanUtils | 运行时反射 | ⭐⭐ 慢 | ❌ |
| Orika | 运行时字节码 | ⭐⭐⭐ 中等 | ❌ |
MapStruct 在编译期生成转换代码,跟手写 get/set 性能几乎一样,但代码量减少 90%。
十、配合 Lombok 的注意事项
java
// ❌ 常见错误:Lombok 和 MapStruct 一起用时报错
// 原因:annotationProcessorPaths 里没有同时配 lombok 和 mapstruct-processor
// ✅ 正确配置 pom.xml 的 compiler 插件(看上面第二部分)
生成代码的位置: 编译后在 target/generated-sources/annotations/ 下可以找到 MapStruct 生成的实现类,感兴趣可以打开看看。
总结
MapStruct 的核心优势就一句话:写一个接口,编译期自动生成转换实现,零反射、高性能、编译期检查。
简单转换 → 写接口,字段名一致不用加注解
字段不同 → @Mapping(source = ..., target = ...)
忽略字段 → @Mapping(target = ..., ignore = true)
类型转换 → 自定义 default 方法
批量转换 → 定义 List<> 方法
💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。