在Java项目开发过程中,经常会遇到需要将一个对象的属性值复制到另一个同类型或者不同类型但字段名相同的对象中的场景,比如DTO和实体类的转换、不同层之间的对象传递等。如果手动为每个类编写属性拷贝代码,不仅工作量巨大,还容易出现字段遗漏或者赋值错误的问题。借助Java反射机制,我们可以动态获取类的字段信息,实现一套通用的Bean拷贝工具,自动完成属性映射逻辑。

反射实现Bean拷贝的核心思路
通用Bean拷贝工具的核心逻辑可以分为以下几个步骤:
- 获取源对象和目标对象的Class对象,用于后续反射操作
- 遍历目标对象的所有字段,找到源对象中同名字段
- 设置字段的可访问性,跳过访问权限检查
- 读取源对象的字段值,赋值给目标对象的对应字段
- 处理不同类型的字段赋值兼容问题
基础版通用Bean拷贝工具实现
首先我们实现一个最基础的拷贝工具,支持同名字段的直接赋值,不考虑复杂类型和类型转换的极端情况:
import java.lang.reflect.Field;
public class BeanCopyUtil {
/**
* 通用Bean拷贝方法
* @param source 源对象
* @param target 目标对象
* @throws IllegalAccessException 反射访问字段异常
*/
public static void copyProperties(Object source, Object target) throws IllegalAccessException {
if (source == null || target == null) {
throw new IllegalArgumentException("源对象和目标对象不能为空");
}
// 获取源对象和目标对象的Class对象
Class<?> sourceClass = source.getClass();
Class<?> targetClass = target.getClass();
// 获取目标对象的所有字段
Field[] targetFields = targetClass.getDeclaredFields();
for (Field targetField : targetFields) {
// 获取源对象中同名字段
try {
Field sourceField = sourceClass.getDeclaredField(targetField.getName());
// 设置字段可访问,跳过private等权限检查
sourceField.setAccessible(true);
targetField.setAccessible(true);
// 获取源字段值并赋值给目标字段
Object value = sourceField.get(source);
targetField.set(target, value);
} catch (NoSuchFieldException e) {
// 源对象不存在同名字段,跳过该字段
continue;
}
}
}
}
工具使用示例
我们定义两个测试用的Bean类,验证基础版拷贝工具的效果:
// 源对象类
class UserSource {
private String name;
private Integer age;
private String email;
// 省略getter、setter和构造方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
// 目标对象类
class UserTarget {
private String name;
private Integer age;
private String phone;
// 省略getter、setter方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
// 测试代码
public class Test {
public static void main(String[] args) throws IllegalAccessException {
UserSource source = new UserSource();
source.setName("张三");
source.setAge(25);
source.setEmail("test@ipipp.com");
UserTarget target = new UserTarget();
BeanCopyUtil.copyProperties(source, target);
System.out.println("姓名:" + target.getName());
System.out.println("年龄:" + target.getAge());
// phone字段源对象不存在,不会被赋值
System.out.println("电话:" + target.getPhone());
}
}
优化拷贝工具处理类型兼容问题
基础版工具存在类型不兼容的问题,比如源字段是int类型,目标字段是Integer类型,或者基本类型和包装类型互相转换时可能会报错。我们可以增加类型判断逻辑,处理常见的类型兼容场景:
import java.lang.reflect.Field;
public class AdvancedBeanCopyUtil {
public static void copyProperties(Object source, Object target) throws IllegalAccessException {
if (source == null || target == null) {
throw new IllegalArgumentException("源对象和目标对象不能为空");
}
Class<?> sourceClass = source.getClass();
Class<?> targetClass = target.getClass();
Field[] targetFields = targetClass.getDeclaredFields();
for (Field targetField : targetFields) {
try {
Field sourceField = sourceClass.getDeclaredField(targetField.getName());
sourceField.setAccessible(true);
targetField.setAccessible(true);
Object sourceValue = sourceField.get(source);
if (sourceValue == null) {
continue;
}
Class<?> sourceType = sourceField.getType();
Class<?> targetType = targetField.getType();
// 处理基本类型和包装类型的兼容
if (isPrimitiveAndWrapper(sourceType, targetType)) {
targetField.set(target, sourceValue);
} else if (targetType.isAssignableFrom(sourceType)) {
// 目标类型可以接收源类型的值,直接赋值
targetField.set(target, sourceValue);
} else {
// 类型不兼容,跳过该字段
continue;
}
} catch (NoSuchFieldException e) {
continue;
}
}
}
/**
* 判断两个类型是否是基本类型和对应的包装类型
*/
private static boolean isPrimitiveAndWrapper(Class<?> type1, Class<?> type2) {
if (type1 == int.class && type2 == Integer.class) return true;
if (type1 == Integer.class && type2 == int.class) return true;
if (type1 == long.class && type2 == Long.class) return true;
if (type1 == Long.class && type2 == long.class) return true;
if (type1 == boolean.class && type2 == Boolean.class) return true;
if (type1 == Boolean.class && type2 == boolean.class) return true;
if (type1 == double.class && type2 == Double.class) return true;
if (type1 == Double.class && type2 == double.class) return true;
if (type1 == float.class && type2 == Float.class) return true;
if (type1 == Float.class && type2 == float.class) return true;
if (type1 == byte.class && type2 == Byte.class) return true;
if (type1 == Byte.class && type2 == byte.class) return true;
if (type1 == short.class && type2 == Short.class) return true;
if (type1 == Short.class && type2 == short.class) return true;
if (type1 == char.class && type2 == Character.class) return true;
if (type1 == Character.class && type2 == char.class) return true;
return false;
}
}
注意事项和扩展方向
使用反射实现Bean拷贝工具时需要注意以下几点:
- 反射操作会有一定的性能开销,如果对性能要求很高的场景,可以考虑增加缓存机制,缓存类的字段信息,避免重复获取
- 默认只拷贝当前类的字段,不会拷贝父类的字段,如果需要支持父类字段拷贝,可以递归获取父类的Class对象
- 对于嵌套对象,默认是浅拷贝,也就是只复制对象的引用,如果需要深拷贝,可以递归调用拷贝方法处理嵌套对象
- 可以通过添加注解的方式,自定义字段映射规则,比如源字段名和目标字段名不同时,通过注解指定映射关系
通过反射实现的通用Bean拷贝工具,能够大幅减少重复的属性赋值代码,只要掌握反射获取字段、设置字段可访问性、处理类型兼容这几个核心技巧,就可以根据实际需求扩展出适配各种场景的拷贝工具。