1. 概述
在Java开发中,对象间属性复制是常见需求。Spring框架提供的BeanUtils.copyProperties
方法因其便捷性被广泛使用,但默认会复制所有匹配属性。实际开发中常遇到两种场景:
- 源对象和目标对象结构不同
- 只需复制部分字段(如敏感数据过滤)
当需要精确控制复制字段时,必须定制化复制行为。本文将介绍三种实现方案,并提供实战代码示例。
2. 使用ignoreProperties参数
BeanUtils.copyProperties
支持第三个参数ignoreProperties
,可指定不复制的字段名(支持可变参数)。以下为基础模型类:
public class SourceBean {
private String name;
private String address;
private int age;
// 构造方法和getter/setter
}
public class TargetBean {
private String name;
private String address;
private int age;
// 构造方法和getter/setter
}
⚠️ 关键点:通过指定"address"
排除该字段复制:
@Test
public void givenObjects_whenUsingIgnoreProperties_thenCopyProperties() {
SourceBean sourceBean = new SourceBean("Peter", 30, "LA");
TargetBean targetBean = new TargetBean();
// 排除address字段复制
BeanUtils.copyProperties(sourceBean, targetBean, "address");
assertEquals(targetBean.getName(), sourceBean.getName());
assertEquals(targetBean.getAge(), sourceBean.getAge());
assertNull(targetBean.getAddress()); // address未被复制
}
✅ 适用场景:排除少量字段时最简单粗暴,但字段较多时参数列表会冗长。
3. 自定义工具类封装
当需要显式指定要复制的字段时,可封装工具类:通过反射获取所有属性,反向构建排除列表:
public static void copySpecifiedProperties(Object source, Object target, Set<String> props) {
String[] excludedProperties = Arrays.stream(BeanUtils.getPropertyDescriptors(source.getClass()))
.map(PropertyDescriptor::getName)
.filter(name -> !props.contains(name))
.toArray(String[]::new);
BeanUtils.copyProperties(source, target, excludedProperties);
}
🔧 使用示例:只复制name
和age
字段:
@Test
public void givenObjects_whenUsingCustomWrapper_thenCopyProperties() {
SourceBean sourceBean = new SourceBean("Peter", 30, "LA");
TargetBean targetBean = new TargetBean();
// 显式指定要复制的字段
BeanUtilsCopyProperties
.copySpecifiedProperties(sourceBean, targetBean,
new HashSet<>(Arrays.asList("name", "age")));
assertEquals(targetBean.getName(), sourceBean.getName());
assertEquals(targetBean.getAge(), sourceBean.getAge());
assertNull(targetBean.getAddress());
}
✅ 优势:字段控制更直观,适合需要白名单机制的场景
❌ 缺点:需维护额外工具类,反射操作有轻微性能损耗
4. 中间DTO对象方案
通过创建临时DTO对象作为数据中转站,实现字段过滤:
public class TempDTO {
public String name; // 只保留需要复制的字段
public int age;
}
📝 执行流程:源对象 → DTO → 目标对象
@Test
public void givenObjects_whenUsingIntermediateObject_thenCopyProperties() {
SourceBean sourceBean = new SourceBean("Peter", 30, "LA");
// 第一步:源对象复制到DTO(自动过滤address)
TempDTO tempDTO = new TempDTO();
BeanUtils.copyProperties(sourceBean, tempDTO);
// 第二步:DTO复制到目标对象
TargetBean targetBean = new TargetBean();
BeanUtils.copyProperties(tempDTO, targetBean);
assertEquals(targetBean.getName(), sourceBean.getName());
assertEquals(targetBean.getAge(), sourceBean.getAge());
assertNull(targetBean.getAddress());
}
✅ 核心价值:
- 字段映射逻辑集中在DTO定义中
- 避免直接操作源对象
- 适合复杂字段转换(如重命名、类型转换)
⚠️ 性能提示:涉及两次复制操作,性能略低于前两种方案
5. 方案对比总结
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
ignoreProperties |
排除少量字段 | 实现简单,无额外代码 | 字段多时参数冗长 |
自定义工具类 | 白名单控制字段 | 字段列表清晰可维护 | 需反射操作,有性能损耗 |
中间DTO对象 | 复杂字段映射/类型转换 | 解耦复制逻辑,扩展性强 | 双次复制,性能最低 |
💡 最佳实践建议:
- 简单场景直接用
ignoreProperties
- 需要显式字段控制时封装工具类
- 复杂数据转换优先选DTO方案
踩坑提示:BeanUtils.copyProperties
基于属性名匹配,不处理类型转换。当源/目标对象字段类型不兼容时会抛出异常,需提前做好类型检查。