1. 程式人生 > 其它 >利用反射和註解,拷貝型別相同,屬性名不同的物件

利用反射和註解,拷貝型別相同,屬性名不同的物件

利用反射和註解,拷貝型別相同,屬性名不同的物件

轉載自:https://segmentfault.com/a/1190000018623737?sort=votes

1、前言

最近開發遇到一個問題,兩個物件進行屬性值拷貝。理論上來說可以直接藉助org.springframework.beans.BeanUtils.copyProperties(Object source, Object target)來進行拷貝,奈何兩個物件屬性名不同,懵逼臉。

2、問題引入

待拷貝類

/**
 * @author : uu
 * @version v1.0
 * @Description: 源User
 */
public class OriginUser {

    /**id*/
    private Long originId;

    /**名稱*/
    private String originName;

    /**密碼*/
    private String password;

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

    /**是否健康*/
    private Boolean originHealth;
    
    /**getter/setter省略*/
}

目標類

/**
 * @author : uu
 * @version v1.0
 * @Description: 目標User
 */
public class TargetUser {
    /**id*/
    private Long targetId;

    /**名稱*/
    private String targetName;

    /**密碼*/
    private String password;

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

    /**是否健康*/
    private Boolean targetHealth;
    
    /**getter/setter省略*/
}

拷貝上述兩個類產生的物件,spring為我們提供的工具類就直接歇菜了。最初想到的方案便是targetUser.setXxx(originUser.getXxx()),這種方式簡單粗暴,易寫,不易擴充套件。如果屬性過多的時候,寫到吐血。

3、問題思考

物件的拷貝,我們可以使用反射進行處理,但是兩個不同屬性的物件進行拷貝的問題在於,我們如何讓兩個不同的屬性名進行關聯。順著這個思路,我開始考慮設定一個工具類專門存放兩個物件的屬性對應關係。這個時候問題又出現了,如果有成千上萬的物件,建立關係對映又是浩大的工程。

偶然間想到fastJson中利用@JSONField(name="xxx")註解可以給屬性設定別名,那麼在拷貝不同屬性物件時,我們也可以使用這種方案。

4、程式碼開發

4.1 CopyField註解

/**
 * 該註解應用於類屬性上,主要為了設定屬性別名,適用於不同屬性拷貝
 * @author : uu
 * @version v1.0
 * @Description: 常用bean相關方法
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CopyField {
    /**
     * 在即將被拷貝的屬性上面,設定目標屬性名
     * @return
     */
    String targetName() default "";

    /**
     * 在即將拷貝至改屬性上面,設定源屬性名
     * @return
     */
    String originName() default "";
}

4.2 bean改造

  • 註解中設定了兩個方法,為了縮小篇幅,我會在原對類和目標類中使用同一個註解,建議實踐中分離為兩個註解。

待拷貝bean

public class OriginUser {

    /**id*/
    @CopyField(targetName = "targetId")
    private Long originId;

    /**名稱*/
    @CopyField(targetName = "targetName")
    private String originName;

    /**密碼*/
    private String password;

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

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

目標bean

public class TargetUser {
    /**id*/
    private Long targetId;

    /**名稱*/
    private String targetName;

    /**密碼*/
    private String password;

    /**出生日期*/
    @CopyField(originName = "originBirthDay")
    private Date targetBirthDay;

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

4.3 BeanUtil工具類

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

/**
 * @author : uu
 * @version v1.0
 * @Description: 常用bean相關方法
 */
public class BeanUtils {

    private static Logger logger = LoggerFactory.getLogger(BeanUtils.class);

    /**
     * <h3>拷貝一個物件的屬性至另一個物件</h3>
     * <p>
     *     支援兩個物件之間不同屬性名稱進行拷貝,使用註解{@link CopyField}
     * </p>
     * @param originBean  源物件 使用註解{@link CopyField#targetName()}
     * @param targetBean  目標物件 使用註解{@link CopyField#originName()}
     */
    public static void copyBean(Object originBean, Object targetBean) {
        Map<String, Object> originFieldKeyWithValueMap = new HashMap<>(16);
        PropertyDescriptor propertyDescriptor = null;
        //生成源bean的屬性及其值的字典
        generateOriginFieldWithValue(propertyDescriptor, originBean, originFieldKeyWithValueMap, originBean.getClass());
        //設定目標bean的屬性值
        settingTargetFieldWithValue(propertyDescriptor, targetBean, originFieldKeyWithValueMap, targetBean.getClass());

    }

    /**
     * 生成需要被拷貝的屬性字典 屬性-屬性值<br/>
     * 遞迴取父類屬性值
     * @param propertyDescriptor  屬性描述器,可以獲取bean中的屬性及方法
     * @param originBean 待拷貝的bean
     * @param originFieldKeyWithValueMap 存放待拷貝的屬性和屬性值
     * @param beanClass 待拷貝的class[可能是超類的class]
     */
    private static void generateOriginFieldWithValue(PropertyDescriptor propertyDescriptor, Object originBean, Map<String, Object> originFieldKeyWithValueMap, Class<?> beanClass) {
        /**如果不存在超類,那麼跳出迴圈*/
        if (beanClass.getSuperclass() == null) {
            return;
        }
        Field[] originFieldList = beanClass.getDeclaredFields();
        for (Field field : originFieldList) {
            try {
                /*獲取屬性上的註解。如果不存在,使用屬性名,如果存在使用註解名*/
                CopyField annotation = field.getAnnotation(CopyField.class);
                String targetName = "";
                if (annotation != null) {
                    targetName = annotation.targetName();
                } else {
                    targetName = field.getName();
                }
                //初始化
                propertyDescriptor = new PropertyDescriptor(field.getName(), beanClass);
                //獲取當前屬性的get方法
                Method method = propertyDescriptor.getReadMethod();
                //設定值
                Object value = method.invoke(originBean);
                //設定值
                originFieldKeyWithValueMap.put(targetName, value);
            } catch (IntrospectionException e) {
                logger.warn("【源物件】異常:" + field.getName() + "不存在對應的get方法,無法參與拷貝!");
            } catch (IllegalAccessException e) {
                logger.warn("【源物件】異常:" + field.getName() + "的get方法執行失敗!");
            } catch (InvocationTargetException e) {
                logger.warn("【源物件】異常:" + field.getName() + "的get方法執行失敗!");
            }
        }
        //生成超類 屬性-value
        generateOriginFieldWithValue(propertyDescriptor, originBean, originFieldKeyWithValueMap, beanClass.getSuperclass());
    }

    /**
     *
     * @param propertyDescriptor 屬性描述器,獲取當前傳入屬性的(getter/setter)方法
     * @param targetBean 目標容器bean
     * @param originFieldKeyWithValueMap 待拷貝的屬性和屬性值
     * @param beanClass 待設定的class[可能是超類的class]
     */
    private static void settingTargetFieldWithValue(PropertyDescriptor propertyDescriptor, Object targetBean, Map<String, Object> originFieldKeyWithValueMap, Class<?> beanClass) {
        /**如果不存在超類,那麼跳出迴圈*/
        if (beanClass.getSuperclass() == null) {
            return;
        }
        Field[] targetFieldList = beanClass.getDeclaredFields();
        for (Field field : targetFieldList) {
            try {
                /*獲取屬性上的註解。如果不存在,使用屬性名,如果存在使用註解名*/
                CopyField annotation = field.getAnnotation(CopyField.class);
                String originName = "";
                if (annotation != null) {
                    originName = annotation.originName();
                } else {
                    originName = field.getName();
                }
                //初始化當前屬性的描述器
                propertyDescriptor = new PropertyDescriptor(field.getName(), beanClass);
                //獲取當前屬性的set方法
                Method method = propertyDescriptor.getWriteMethod();
                method.invoke(targetBean, originFieldKeyWithValueMap.get(originName));
            } catch (IntrospectionException e) {
                logger.warn("【目標物件】異常:" + field.getName() + "不存在對應的set方法,無法參與拷貝!");
            } catch (IllegalAccessException e) {
                logger.warn("【目標物件】異常:" + field.getName() + "的set方法執行失敗!");
            } catch (InvocationTargetException e) {
                logger.warn("【目標物件】異常:" + field.getName() + "的set方法執行失敗!");
            }
        }
        //設定超類屬性
        settingTargetFieldWithValue(propertyDescriptor, targetBean, originFieldKeyWithValueMap, beanClass.getSuperclass());
    }
}

4.4 測試

/**
 * @author : uu
 * @version v1.0
 * @Description:
 * @Date 2019-03-23  09:48
 */
public class MainTest {
    public static void main(String[] args) {
        OriginUser originUser = new OriginUser();
        originUser.setOriginId(1000L);
        originUser.setOriginName("張四");
        originUser.setPassword("123456");
        originUser.setOriginBirthDay(new Date());
        originUser.setOriginHealth(true);
        
        //拷貝
        TargetUser targetUser = new TargetUser();
        BeanUtils.copyBean(originUser, targetUser);
        System.out.println(targetUser);
    }
}

執行結果:

  • TargetUser(targetId=1000, targetName=張四, password=123456, targetBirthDay=Fri Sep 03 13:40:52 CST 2021, targetHealth=true)

5、總結

  • BeanUtils.copyBean()方法支援拷貝超類的屬性,屬性需要有getter和setter方法,否則拋異常(隻影響無get/set方法的屬性)
  • PropertyDescriptor屬性描述器,可以很方便的獲取讀取和寫入方法,減少getMethod通過字串拼接獲取方法的成本
  • class.getFields()只能獲取公開的屬性,getDeclaredFields可以獲取任意,但只包含本類中,父類需要使用class.getSuperclass()遞歸向上尋找
學習使我充實,分享給我快樂!