前言
在日常开发中,对象之间的属性拷贝是一个非常常见的需求。我们经常需要将 DTO
转换为 Entity
,或者把 VO
映射到 DTO
,这种场景下很多人还在使用 BeanUtils.copyProperties
或者 Apache Commons
提供的工具类
但今天我想告诉你:是时候告别这些老古董了!MapStruct 才是你应该选择的对象映射神器!
为什么我不再推荐使用 BeanUtils?
性能堪忧
BeanUtils.copyProperties
是通过 反射机制 实现的,虽然写起来简单,但性能很差。尤其是在循环或处理大批量数据时,反射调用会导致严重的性能瓶颈。
类型不安全
BeanUtils
不做编译期检查,如果字段名拼错了,或者类型不匹配,只有在运行的时候才会抛出异常。这在大型项目中非常危险。
功能单一,难以扩展
当遇到字段名不同、嵌套对象、枚举转换等复杂情况时,BeanUtils
几乎无能为力,你需要手动编写大量代码来处理这些问题。
MapStruct 是什么?它为什么这么强?
MapStruct 是一个代码生成器,它基于注解处理器,在编译阶段自动生成 Java Bean 的映射实现类,完全避免了反射带来的性能损耗。
🎯 一句话总结:MapStruct = 编译期安全 + 零反射 + 极致性能 + 可扩展性强。
快速上手
添加依赖
xml
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<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>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
定义DO 和 DTO
less
@Setter
@Getter
public class User {
private long id;
private String name;
private int age;
}
less
@Setter
@Getter
@ToString
public class UserDTO {
private String name;
private int age;
}
定义 Mapper 接口
java
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
UserDTO toDTO(User user);
}
编译后,MapStruct 会自动生成一个 UserMapperImpl
类,里面就是直接赋值的代码,没有反射!
使用示例
scss
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
User user = new User();
user.setId(1L);
user.setName("Jerry");
user.setAge(30);
// mapstruct方案
UserDTO dto = UserMapper.INSTANCE.toDTO(user);
System.out.println(dto); // UserDTO(name=Jerry, age=30)
// BeanUtils方案
dto = new UserDTO();
BeanUtils.copyProperties(dto, user);
System.out.println(dto); // UserDTO(name=Jerry, age=30)
}
BeanUtils方案看似简单,不用定义Mapper,但是背后包含了方法调用,类型检查,效率其实是比较低下的
简单对比
特性 | BeanUtils | MapStruct |
---|---|---|
是否反射 | ✅ 是 | ❌ 否 |
编译期报错 | ❌ 否 | ✅ 是 |
易用性 | ✅ 简单 | ✅ 简单 + 强大 |
映射灵活性 | ❌ 差 | ✅ 支持字段重命名、嵌套等 |
性能 | ⚠️ 慢(尤其大数据) | ✅ 极快(接近原生赋值) |
MapStruct高级玩法
自定义字段映射
修改Mapper类和User类
less
@Setter
@Getter
public class User2 {
private long id;
private String userName;
private int age;
}
自定义字段映射:
kotlin
@Mapper
public interface UserMapper2 {
UserMapper2 INSTANCE = Mappers.getMapper(UserMapper2.class);
@Mapping(source = "userName",target = "name")
UserDTO toDTO(User2 user);
}
测试代码:
ini
@Test
public void test2(){
User2 user = new User2();
user.setId(1L);
user.setUserName("Jack");
user.setAge(20);
UserDTO dto = UserMapper2.INSTANCE.toDTO(user);
System.out.println(dto);
}
需要User类和DTO类和Mapper类
less
@Setter
@Getter
@ToString
public class UserDTO3 {
private String name;
private int age;
private String city;
}
less
@Setter
@Getter
public class User3 {
private long id;
private String userName;
private int age;
private Address address;
}
多合一映射:
less
@Mapper
public interface UserMapper3 {
UserMapper3 INSTANCE = Mappers.getMapper(UserMapper3.class);
@Mappings({
@Mapping(source = "userName", target = "name"),
@Mapping(source = "address.city", target = "city")
})
UserDTO3 toDTO(User3 user);
}
测试代码:
ini
@Test
public void test3(){
User3 user = new User3();
Address address = new Address();
address.setId("1");
address.setCity("北京");
user.setId(1L);
user.setUserName("Jerry");
user.setAge(30);
user.setAddress(address);
UserDTO3 dto = UserMapper3.INSTANCE.toDTO(user);
System.out.println(dto);
}
Spring 整合: 只需加一个 componentModel = "spring"
参数即可注入:
kotlin
@Mapper(componentModel = "spring") public interface UserMapper { ... }
性能测试
网上都在说MapStruct的性能比BeanUtils好很多,但是很多都只是在说这个结论 我们直接开测,做Java的,一提到压力测试就会想到Jmeter,但是其实还有一个工具:JMH
JMH(Java Microbenchmark Harness)是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级
上代码:
less
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(1)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 2)
public class JMHTest {
private User user;
@Setup
public void setup() {
User user = new User();
user.setId(1L);
user.setName("Jerry");
user.setAge(30);
this.user = user;
}
@Benchmark
public void testBeanUtilsCopy(Blackhole blackhole) throws InvocationTargetException, IllegalAccessException {
UserDTO target = new UserDTO();
BeanUtils.copyProperties(target, user);
blackhole.consume(target);
}
@Benchmark
public void testMapStructCopy(Blackhole blackhole) {
UserDTO target = UserMapper.INSTANCE.toDTO(user);
blackhole.consume(target);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHTest.class.getSimpleName())
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opt).run();
}
}
输出结果如下:
Benchmark | Mode | Cnt | Score (ns/op) | Error (ns/op) | Units |
---|---|---|---|---|---|
JMHTest.testBeanUtilsCopy | avgt | 5 | 10564.333 | ± 490.630 | ns/op |
JMHTest.testMapStructCopy | avgt | 5 | 4.784 | ± 0.229 | ns/op |
性能对比:
方法 | 平均耗时 | 性能倍数(相对 MapStruct) |
---|---|---|
BeanUtils.copyProperties |
~10.7 μs | 慢 2168 倍 |
MapStruct |
~4.9 ns | 1 倍(基准) |
为什么差距这么大?
特性 | BeanUtils |
MapStruct |
---|---|---|
实现方式 | 反射(Reflection) | 编译期生成字节码 |
调用开销 | 高(每次都要查找 setter/getter) | 极低(等同手写 setter) |
JIT 优化 | 难以优化 | 容易被 JIT 优化 |
类型安全 | 否 | 是 |
编译检查 | 否 | 是 |
💡 结论 :
BeanUtils.copyProperties
适合低频、简单的 DTO 转换 ;
MapStruct
适合高频、性能敏感的场景(如高并发 API、大批量数据处理)