前言

哈喽大家好,我是卡诺,一名致力于成为全栈的全粘工程师!

相信大家在日常开发中一定遇到过这样的烦恼,A对象和B对象属性不同,需要将A对象的属性值拷贝到B对象的属性中。我们最常用的做法应该就是getter/setter了吧!今天咱们玩个不一样的,通过合理运用反射和注解,一行代码搞定属性拷贝问题!希望通过本章的案例,可以让大家对反射和注解有更深入的了解,反射注解的基础知识请查看​​ ​【再学一次Java】​ ​​专栏。

本文曾于2019-03-26发布在这里​​点击查看​​,本次基于前面学习的反射、注解、函数式接口的知识做了部分优化调整。

本文已经加入​​ ​【再学一次Java】​ ​​​、 ​【问题解决方案】​ ​​专栏!

  • 【再学一次Java】​ ​​专栏旨在重温Java知识,夯实基础,包含:Lambda、反射、注解、多线程等进阶知识。
  • 【问题解决方案】​ ​​专栏旨在为业务中遇到问题及BUG提供相关解决方案,包含:BUG处理、前端、后端业务问题解决方案等
  • 小伙伴如果有需要可以关注一下哦,所有均专栏持续更新ing

问题引入

拷贝不同对象的相同属性名,一般情况下直接使用Spring提供的​​org.springframework.beans.BeanUtils.copyProperties(Object source, Object target)​​来进行拷贝处理,对于不同不同属性名的情况,代码如下:

/**
* 源用户
*
* @author : uu
* @version : v1.0
* @Date 2022/2/12
*/
@Getter @Setter@ToString
public class OriginUser {
/**id*/
private Long originId;

/**名称*/
private String originName;

/**密码*/
private String password;

/**出生日期*/
private Date originBirthDay;

/**是否健康*/
private Boolean originHealth;
}

/**
* 目标User
* @author : uu
* @version : v1.0
* @Date 2022/2/12
*/
@Getter@Setter@ToString
public class TargetUser {
/**id*/
private Long targetId;

/**名称*/
private String targetName;

/**密码*/
private String password;

/**出生日期*/
private Date targetBirthDay;

/**是否健康*/
private Boolean targetHealth;
}

我们希望将​​OriginUser​​产生的对象数据,拷贝生成​​TargetUser​​对象,Spring提供的工具类就直接歇菜了,常用的getter/setter代码方式如下:

/**
* 初始化源用户
* @return
*/
public OriginUser initOriginUser() {
OriginUser originUser = new OriginUser();
originUser.setOriginId(1L);
originUser.setOriginName("卡诺来了");
originUser.setPassword("123");
originUser.setOriginBirthDay(new Date());
originUser.setOriginHealth(Boolean.TRUE);
return originUser;
}

@Test
@DisplayName("getter/setter方式拷贝")
public void test(){
OriginUser originUser = initOriginUser();

// 将originUser的属性值拷贝到targetUser中
TargetUser targetUser = new TargetUser();
targetUser.setTargetId(originUser.getOriginId());
targetUser.setTargetName(originUser.getOriginName());
targetUser.setTargetBirthDay(originUser.getOriginBirthDay());
targetUser.setTargetHealth(originUser.getOriginHealth());
}

getter/setter这种方式简单、粗暴、易写、不易扩展。如果属性过多,肯定写到吐血。有什么好的方法呢?小伙伴请继续向下看!

问题思考

对象的拷贝,我们可以使用反射进行处理,但是两个不同属性的对象进行拷贝的问题在于,我们如何让两个不同的属性名进行关联。顺着这个思路,我们可以考虑设置一个工具类专门存放两个对象的属性对应关系。这个时候问题又出现了,如果有成千上万的对象,建立关系映射又是浩大的工程。

如果大家用过fastJson这个工具类,那么一定了解​​@JSONField​​注解,利用该注解可以给属性设置别名​​@JSONField(name=“xxx”)​​,那么在拷贝不同属性对象时,我们也可以使用这种方案进行处理。

解决思路


  1. 声明​​@FieldAlias​​注解,在需要操作的类属性上标记需要拷贝的属性名之间实际属性名的映射关系;
  2. 基于反射构建源对象实际属性名和get方法的fieldName-fieldValue关系;
  3. 基于反射获取目标对象的所有属性,通过步骤2构建的源对象的fieldName-fieldValue关系,对目标对象的属性进行值的设置。

接下来我们进入代码实操环节。

代码实操

声明注解FieldAlias

/**
* 属性别名注解
*
* @author : uu
* @Date 2022/2/12
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FieldAlias {
// 属性别名
String value();
}

如果属性使用该注解,则表示该属性的实际属性名为注解中value对应的值,否则属性名为原属性名

调整OriginUser类

我们需要将OriginUser对象的属性拷贝到TargetUser对象中,那么开始使用​​@FieldAlias​​注解进行对OriginUser类进行改造吧,改造代码如下:

@Getter @Setter@ToString
public class OriginUser {
/**id*/
@FieldAlias("targetId")
private Long originId;

/**名称*/
@FieldAlias("targetName")
private String originName;

/**密码*/
private String password;

/**出生日期*/
@FieldAlias("targetBirthDay")
private Date originBirthDay;

/**是否健康*/
@FieldAlias("targetHealth")
private Boolean originHealth;
}

编写拷贝工具类

/**
* beanUtil工具类
*
* @author : uu
* @version : v1.0
* @Date 2022/2/12
*/
public class BeanUtil {

/**
* <h3>拷贝一个对象的属性至另一个对象</h3>
* <p>
* 支持两个对象之间不同属性名称进行拷贝,使用注解{@link FieldAlias}
* </p>
*
* @param originBean 源对象
* @param targetBean 目标对象
*/
public static void copyBean(Object originBean, Object targetBean) {
Map<String, Object> originFieldKeyWithValueMap = new HashMap<>(16);
PropertyDescriptor propertyDescriptor = null;
//生成源bean的属性及其值的字典
operateBeanFieldWithValue(originBean,
propertyDescriptor,
originFieldKeyWithValueMap,
originBean.getClass(),
(bean, descriptor, realFieldName, fieldWithMethodMap)-> {
try {
//获取当前属性的get方法
Method method = descriptor.getReadMethod();
//设置值
Object value = method.invoke(bean);
//将源对象值缓存设置值
fieldWithMethodMap.put(realFieldName, value);
} catch (IllegalAccessException e) {
System.err.println("【源对象】异常:" + realFieldName + "的get方法执行失败!");
} catch (InvocationTargetException e) {
System.err.println("【源对象】异常:" + realFieldName + "的get方法执行失败!");
}
}
);
//设置目标bean的属性值
operateBeanFieldWithValue(targetBean, propertyDescriptor, originFieldKeyWithValueMap, targetBean.getClass(),
(bean, descriptor, realFieldName, fieldWithMethodMap)-> {
try {
//获取当前属性的set方法
Method method = descriptor.getWriteMethod();
method.invoke(bean, fieldWithMethodMap.get(realFieldName));
} catch (IllegalAccessException e) {
System.err.println("【目标对象】异常:" + realFieldName + "的set方法执行失败!");
} catch (InvocationTargetException e) {
System.err.println("【目标对象】异常:" + realFieldName + "的set方法执行失败!");
}
});

}

/**
* 操作bean
* 对于源对象:生成需要被拷贝的属性字典 属性-属性值,递归取父类属性值
* 对于目标对象:设置源对象的属性值
* @param bean 当前被操作的bean
* @param descriptor 属性描述器,可以获取bean中的属性及方法
* @param originFieldNameWithValueMap 存放待拷贝的属性和属性值
* @param beanClass 被操作的class[可能是超类的class]
*/
private static void operateBeanFieldWithValue(Object bean,
PropertyDescriptor descriptor,
Map<String, Object> originFieldNameWithValueMap,
Class<?> beanClass,
CVFunction cvFunction
) {
/**如果不存在超类,那么跳出循环*/
if (beanClass.getSuperclass() == null) {
return;
}
Field[] fieldList = beanClass.getDeclaredFields();
for (Field field : fieldList) {
try {
/*获取属性上的注解。如果不存在,使用属性名,如果存在使用注解名*/
FieldAlias fieldAlias = field.getAnnotation(FieldAlias.class);
String realFieldName = Objects.isNull(fieldAlias) ? field.getName() : fieldAlias.value();
//初始化
descriptor = new PropertyDescriptor(field.getName(), beanClass);
cvFunction.apply(bean, descriptor, realFieldName, originFieldNameWithValueMap);
} catch (IntrospectionException e) {
System.err.println("【源对象】异常:" + field.getName() + "不存在对应的get方法,无法参与拷贝!");
}
}
//生成超类 属性-value
operateBeanFieldWithValue(bean, descriptor, originFieldNameWithValueMap, beanClass.getSuperclass(), cvFunction);
}
}
复制代码

上述代码中我们使用了自定义函数式接口,接口定义如下:

/**
* 属性CV操作的函数式接口
*
* @author : uu
* @version : v1.0
* @Date 2022/2/12
*/
@FunctionalInterface
public interface CVFunction {
/**
* 执行CV操作
*
* @param bean
* @param descriptor
* @param realFieldName
* @param fieldWithMethodMap
*/
void apply(Object bean, PropertyDescriptor descriptor, String realFieldName, Map<String, Object> fieldWithMethodMap);
}

测试代码

@Test
@DisplayName("注解和反射方式拷贝")
public void test2(){
OriginUser originUser = initOriginUser();
// OriginUser(originId=1, originName=卡诺来了, password=123, originBirthDay=Sat Feb 12 22:08:46 CST 2022, originHealth=true)
System.out.println(originUser);

// 将originUser的属性值拷贝到targetUser中
TargetUser targetUser = new TargetUser();

// ===========执行拷贝================
BeanUtil.copyBean(originUser, targetUser);
// TargetUser(targetId=1, targetName=卡诺来了, password=123, targetBirthDay=Sat Feb 12 22:08:46 CST 2022, targetHealth=true)
System.out.println(targetUser);
}
@Test@DisplayName("注解和反射方式拷贝")public void test2(){    OriginUser originUser = initOriginUser();    // OriginUser(originId=1, originName=卡诺来了, password=123, originBirthDay=Sat Feb 12 22:08:46 CST 2022, originHealth=true)    System.out.println(originUser);    // 将originUser的属性值拷贝到targetUser中    TargetUser targetUser = new TargetUser();    // ===========执行拷贝================    BeanUtil.copyBean(originUser, targetUser);    // TargetUser(targetId=1, targetName=卡诺来了, password=123, targetBirthDay=Sat Feb 12 22:08:46 CST 2022, targetHealth=true)    System.out.println(targetUser);}复制代码

源码

总结

  • 本章主要利用我们之前学习的反射、注解、函数式接口知识点,解决业务中遇到的对象拷贝问题;
  • PropertyDescriptor属性描述器,可以很方便的获取读取和写入方法,减少通过字符串拼接获取方法的成本;
  • 本文主要目的是提供解决思路,上述工具类依然有优化的空间,有兴趣的小伙伴可以获取源码后根据需要再做调整。

最后

  • 感谢铁子们耐心看到最后,如果大家感觉本文有所帮助,麻烦给个​赞????​或​关注➕
  • 由于本人技术有限,文章和代码可能存在错误,希望大家评论指出,万分感激????;
  • 同时也欢迎大家一起讨论学习前端、Java知识,一起卷一起进步。