引言

用好 BeanUtils,让你的代码更简洁;读懂 BeanUtils,让你对 Spring 的内省机制了然于胸。
Spring 常用类深度解析系列开篇:那些我们每天都在用,却未必真正理解的类


在日常开发中,我们经常需要将一个对象的属性值复制到另一个对象中,比如 DTO 转 VO、PO 转 DTO。手写一堆 getter/setter 不仅繁琐,而且容易遗漏字段。这时,Spring 提供的 BeanUtils 就成了我们的好帮手。

但很多开发者对 BeanUtils 的认知停留在 copyProperties 方法上,对其背后的原理、性能表现、深拷贝问题、类型转换等知之甚少。一旦遇到属性名不一致、类型不匹配、嵌套对象拷贝等场景,就会陷入迷茫,甚至写出有性能隐患的代码。

本文将深入剖析 Spring 的 BeanUtils,从基本用法到源码解析,再到高阶技巧和常见坑点,带你彻底掌握这个“熟悉又陌生”的工具类。

BeanUtils 的基本用法:几秒钟搞定对象拷贝

1 引入依赖

BeanUtils 位于 spring-beans 模块中,如果你的项目使用了 Spring,通常已经包含了该依赖。如果是普通 Java 项目,可以单独引入:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>5.3.x</version>
</dependency>
2 最常用的 copyProperties

BeanUtils.copyProperties(Object source, Object target) 方法用于将源对象的属性值复制到目标对象中(仅复制名称和类型匹配的属性)。

示例代码:

public class User {
    private Long id;
    private String name;
    private Integer age;
    private Date birthday;
    // getter/setter 略
}

public class UserVO {
    private Long id;
    private String name;
    private Integer age;
    private String birthday; // 类型不同,不会被复制
    private String extra;    // 目标对象独有属性,保持不变
    // getter/setter 略
}

// 使用
User user = new User(1L, "张三", 25, new Date());
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);

执行后,userVO 中的 idnameage 被复制,birthday 因为类型不匹配(Date vs String)而被忽略,extra 保留原值。

3 其他常用方法

除了 copyPropertiesBeanUtils 还提供了以下实用方法:

  • getPropertyDescriptors(Class<?> beanClass):获取 Bean 的所有属性描述符(PropertyDescriptor)。
  • findMethod(Class<?> clazz, String methodName, Class<?>... paramTypes):查找方法。
  • resolveSignature(String signature, Class<?> clazz):解析方法签名。

这些方法在框架开发或反射工具类中非常有用。

深入源码:BeanUtils 是如何实现属性拷贝的?

要理解 BeanUtils 的工作原理,我们得深入到 Spring 5.3.x 的源码中一探究竟。

1 入口方法

BeanUtils.copyProperties(source, target) 最终会调用 copyProperties(source, target, null, (String[]) null)

在这里插入图片描述

2 核心逻辑

核心逻辑在 copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties) 方法中,主要步骤如下:

  1. 类型检查:确保 source 和 target 不为 null。
  2. 获取目标类的可编辑属性:如果指定了 editable,则限制属性范围(当只想拷贝基类/接口中定义的属性,而不想拷贝子类特有属性时使用)。
    • 类型检查:确保 target 对象是 editable 类型的实例
    • 缩小属性范围:只处理 editable 指定的类(而非 target 的实际类)中定义的属性,忽略子类新增的属性
  3. 获取源类和目标类的 PropertyDescriptor 数组:这是关键,BeanUtils 通过 JDK 的内省(Introspector)获取 Bean 的属性描述信息,包括属性的类型、读写方法等。
  4. 构建忽略属性集合:将需要忽略的属性名放入 Set。
  5. 遍历源类的 PropertyDescriptor
    • 跳过被忽略的属性。
    • 检查源属性是否有读方法(getter)。
    • 在目标类的 PropertyDescriptor 中查找名称相同的属性,并检查其是否有写方法(setter)。
    • 检查属性类型是否兼容(源属性类型可赋值给目标属性类型)。
    • 如果都满足,则调用源 getter 获取值,然后调用目标 setter 设置值。
3 核心源码

在这里插入图片描述
从源码可以看出,BeanUtils 只做“同名同类型”的属性复制,不进行任何类型转换,这也是它高效且安全的原因。

4 核心类:PropertyDescriptor

PropertyDescriptor 是 JDK 提供的类,用于描述 Java Bean 的属性,包含属性名、类型、读方法、写方法等信息。BeanUtils 通过 BeanUtils.getPropertyDescriptors(clazz) 获取该类的所有属性描述符,其内部调用了 Introspector.getBeanInfo(clazz).getPropertyDescriptors()

CachedIntrospectionResults 是 Spring 对内省结果的缓存,避免重复内省带来的性能开销。每个类只会被内省一次,结果缓存起来。

其关键源码如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

高级用法与注意事项

1 忽略特定字段

有时我们不想复制某些字段(如密码、创建时间等),可以使用 copyProperties 的重载方法:

BeanUtils.copyProperties(source, target, "password", "createTime");
2 深拷贝问题

BeanUtils 进行的是浅拷贝。如果源对象的属性是引用类型(如另一个对象、List、Map),拷贝的只是引用,修改目标对象的该属性会影响源对象。如果需要深拷贝,需要手动处理或使用其他工具(如 JSON 序列化反序列化、Cloneable、Apache Commons SerializationUtils 等)。

3 属性名不一致时的处理

BeanUtils 要求属性名完全相同,如果属性名不一致,有三种解决方案:

  • 在目标类中添加一个与源类属性名相同的新属性,然后业务处理时转换。
  • 使用 BeanWrapper 或手动调用 getter/setter。
  • 借助第三方工具(如 MapStruct)在编译期生成转换代码。
4 与其他属性拷贝工具的对比

除了 Spring 的 BeanUtils,Java 生态中还有其他常用的属性拷贝工具,例如 Apache Commons BeanUtils、Cglib BeanCopier、MapStruct 等。它们各有优劣,我们来做个对比。

工具 原理 性能 类型转换 优点 缺点
Spring BeanUtils 反射 + 内省缓存 中等 内置简单转换 与 Spring 集成好,简单易用 性能一般,不支持深拷贝
Apache Commons BeanUtils 反射 较差 内置转换(性能差) 功能丰富 性能极差(有类型转换开销),不推荐
Cglib BeanCopier 动态生成字节码 极高 需手动处理 速度最快 只复制同名同类型字段,复杂类型需转换器
MapStruct 编译期生成代码 与手写一致 丰富(可配置) 性能最好,类型安全 需要额外配置注解处理器

简单做一个 JMH 基准测试,对比手写 getter/setter、Spring BeanUtils、Cglib BeanCopier 和 MapStruct 在拷贝 10 万次对象时的耗时。结果大致如下(仅供参考,具体环境有差异):

工具 平均耗时(ms)
手写 2
MapStruct 2
Cglib BeanCopier 5
Spring BeanUtils 120
Apache BeanUtils 850

可见,Spring BeanUtils 比手写慢约 60 倍,但在大多数业务场景下(单次拷贝),这点差距可以忽略。只有在超高并发循环拷贝时才需要考虑性能问题。

5. 常见面试题与坑点
Q1:BeanUtils.copyProperties 是深拷贝还是浅拷贝?

A:浅拷贝。引用类型属性只复制引用地址。如果需要深拷贝,需要手动处理或使用其他工具(如序列化、CloneUtils)。

Q2:如果源和目标属性类型不同,会发生什么?

A:Spring BeanUtils 不进行类型转换,如果类型不兼容,会直接忽略该属性,不会报错。但 Apache BeanUtils 会尝试转换,可能抛出异常。

Q3:如何忽略 null 值?

A:Spring BeanUtils 本身不支持忽略 null。如果需要,可以自己封装,遍历属性判断 null 后再调用 copyProperties,或者使用 BeanWrapper 进行精细控制。

Q4:拷贝过程中抛出异常怎么办?

A:BeanUtils 会捕获底层反射异常并包装成 FatalBeanException(继承自 RuntimeException),一般无需显式 try-catch,但要注意事务回滚等场景。

常见坑点
  • 属性名相同但类型不匹配:会被默默忽略,可能导致目标对象属性为 null,业务逻辑出错。
  • 布尔类型的 getter 命名不规范:如 isDeleted 方法,BeanUtils 能正确识别,但如果有自定义 getter 命名,可能会匹配失败。
  • 静态属性:不会被复制。
  • final 字段:如果没有 setter,不会被复制。

总结与下期预告

Spring BeanUtils 的核心就是**基于 Java 内省机制,通过反射调用 getter/setter 完成属性赋值。**它具备基本的类型转换能力,并且通过缓存内省结果提升了性能。

通过本文,我们深入理解了 Spring BeanUtils 的基本用法、源码实现、性能特点以及常见坑点。它是日常开发中不可或缺的工具,但使用时也要注意其浅拷贝、类型不匹配等特性,避免引入隐蔽的 Bug。

下一篇文章,我们将继续 “Spring 常用类深度剖析(工具篇 02):ReflectionUtils——优雅操作反射的利器”,敬请期待。


互动话题:你在使用 BeanUtils 时遇到过哪些“坑”?或者你封装过哪些好用的属性拷贝工具类?欢迎在评论区分享。
下篇预告:ReflectionUtils 深度解析,解锁 Spring 反射工具类的正确姿势。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐