BeanUtils 你走好!MapStruct 才是对象转换的真香神器!

前言

在日常开发中,对象之间的属性拷贝是一个非常常见的需求。我们经常需要将 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、大批量数据处理)

相关推荐
Marktowin17 分钟前
Mybatis-Plus更新操作时的一个坑
java·后端
赵文宇40 分钟前
CNCF Dragonfly 毕业啦!基于P2P的镜像和文件分发系统快速入门,在线体验
后端
程序员爱钓鱼1 小时前
Node.js 编程实战:即时聊天应用 —— WebSocket 实现实时通信
前端·后端·node.js
Libby博仙2 小时前
Spring Boot 条件化注解深度解析
java·spring boot·后端
源代码•宸2 小时前
Golang原理剖析(Map 源码梳理)
经验分享·后端·算法·leetcode·golang·map
小周在成长2 小时前
动态SQL与MyBatis动态SQL最佳实践
后端
瓦尔登湖懒羊羊2 小时前
TCP的自我介绍
后端
小周在成长2 小时前
MyBatis 动态SQL学习
后端
子非鱼9212 小时前
SpringBoot快速上手
java·spring boot·后端
我爱娃哈哈2 小时前
SpringBoot + XXL-JOB + Quartz:任务调度双引擎选型与高可用调度平台搭建
java·spring boot·后端