利用反射和註解,拷貝類型相同,屬性名不一樣的對象

一、前言

最近開發遇到一個問題,兩個對象進行屬性值拷貝。理論上來講能夠直接藉助org.springframework.beans.BeanUtils.copyProperties(Object source, Object target)來進行拷貝,奈何兩個對象屬性名不一樣,懵逼臉。

二、問題引入

待拷貝類java

/**
 * @author : weenie
 * @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省略*/
}

目標類git

/**
 * @author : weenie
 * @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()),這種方式簡單粗暴,易寫,不易擴展。若是屬性過多的時候,寫到吐血。github

三、問題思考

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

偶然間想到fastJson中利用@JSONField(name="xxx")註解能夠給屬性設置別名,那麼在拷貝不一樣屬性對象時,咱們也可使用這種方案。框架

四、代碼開發

4.1 CopyField註解

/**
 * 該註解應用於類屬性上,主要爲了設置屬性別名,適用於不一樣屬性拷貝
 * @author : weenie
 * @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 : weenie
 * @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 : weenie
 * @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);
    }
}

運行結果:spa

clipboard.png

五、總結

  • BeanUtils.copyBean()方法支持拷貝超類的屬性,屬性須要有getter和setter方法,不然拋異常(隻影響無get/set方法的屬性)
  • PropertyDescriptor屬性描述器,能夠很方便的獲取讀取和寫入方法,減小getMethod經過字符串拼接獲取方法的成本
  • class.getFields()只能獲取公開的屬性,getDeclaredFields能夠獲取任意,但只包含本類中,父類須要使用class.getSuperclass()遞歸向上尋找

Diboot - 簡單高效的輕代碼開發框架code

相關文章
相關標籤/搜索