Spring 常用类深度剖析(工具篇 01):BeanUtils,不只是属性拷贝
引言
用好 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 中的 id、name、age 被复制,birthday 因为类型不匹配(Date vs String)而被忽略,extra 保留原值。
3 其他常用方法
除了 copyProperties,BeanUtils 还提供了以下实用方法:
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) 方法中,主要步骤如下:
- 类型检查:确保 source 和 target 不为 null。
- 获取目标类的可编辑属性:如果指定了
editable,则限制属性范围(当只想拷贝基类/接口中定义的属性,而不想拷贝子类特有属性时使用)。- 类型检查:确保 target 对象是
editable类型的实例 - 缩小属性范围:只处理
editable指定的类(而非 target 的实际类)中定义的属性,忽略子类新增的属性
- 类型检查:确保 target 对象是
- 获取源类和目标类的 PropertyDescriptor 数组:这是关键,BeanUtils 通过 JDK 的内省(
Introspector)获取 Bean 的属性描述信息,包括属性的类型、读写方法等。 - 构建忽略属性集合:将需要忽略的属性名放入 Set。
- 遍历源类的 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 反射工具类的正确姿势。
更多推荐


所有评论(0)