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、大批量数据处理)

相关推荐
小兔兔吃萝卜1 小时前
Spring 创建 Bean 的 8 种主要方式
java·后端·spring
Java中文社群2 小时前
26届双非上岸记!快手之战~
java·后端·面试
whitepure2 小时前
万字详解Java中的面向对象(一)——设计原则
java·后端
autumnTop2 小时前
为什么访问不了同事的服务器或者ping不通地址了?
前端·后端·程序员
用户6757049885022 小时前
SQL 判断是否“存在”?99% 的人还在写错!
后端
PetterHillWater2 小时前
12 MCP Servers的介绍
后端·aigc·mcp
杨杨杨大侠3 小时前
02 - 核心模型设计 🧩
后端