您的位置 首页 java

利用反射和注解,一行代码,拷贝属性名不同的对象

问题引入

拷贝不同对象的相同属性名,一般情况下直接使用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);
}  

总结

  • 本章主要利用我们之前学习的反射、注解、函数式接口知识点,解决业务中遇到的对象拷贝问题;
  • PropertyDescriptor属性描述器,可以很方便的获取读取和写入方法,减少通过字符串拼接获取方法的成本;

文章来源:智云一二三科技

文章标题:利用反射和注解,一行代码,拷贝属性名不同的对象

文章地址:https://www.zhihuclub.com/174452.shtml

关于作者: 智云科技

热门文章

网站地图