網頁

2019/10/27

使用Spring BeanUtils.copyProperties()複製Java Bean或POJO的properties值

本篇介紹利用Spring BeanUtils.copyProperties(Object source, Object target)方法把JavaBean/POJO物件的properties值複製到另一個物件。


例如下面範例把Member物件m1中的屬性值複製到同為Memberm2

package com.abc.demo;

import java.util.Arrays;
import java.util.List;

import org.springframework.beans.BeanUtils;

public class Main {

    public static void main(String[] args) {

        List<Article> articleList = Arrays.asList(new Article(1L, "hello world"));

        Member m1 = new Member("Eddy", articleList);
        Member m2 = new Member();
        BeanUtils.copyProperties(m1, m2);

        System.out.println("m1:" + m1); // m1:name:Eddy, articleList:[id:1, content:hello world]
        System.out.println("m2:" + m2); // m2:name:Eddy, articleList:[id:1, content:hello world]
        
    }

}

class Member {

    private String name;
    private List<Article> articleList;

    public Member() {
    }

    public Member(String name, List<Article> articleList) {
        this.name = name;
        this.articleList = articleList;
    }

    @Override
    public String toString() {
        return "name:" + name + ", " + "articleList:" + articleList;
    }

    // getters and setters
}

class Article {
    private Long id;
    private String content;

    public Article(Long id, String content) {
        this.id = id;
        this.content = content;
    }

    @Override
    public String toString() {
        return "id:" + id + ", content:" + content;
    }

    // getters and setters
}

上面可以看到經由BeanUtils.copyProperties()複製後m2的屬性值確實與m1相同,裡面的List<Article>物件屬性也會被複製,屬於深拷貝(deep copy)。


下面範例則是把m1複製到另一個類別User,除了articlelist屬性名稱外幾乎相同。

package com.abc.demo;

import java.util.Arrays;
import java.util.List;

import org.springframework.beans.BeanUtils;

public class Main {

    public static void main(String[] args) {

        List<Article> articleList = Arrays.asList(new Article(1L, "hello world"));

        Member m1 = new Member("Eddy", articleList);
        
        User u1 = new User();
        BeanUtils.copyProperties(m1, u1);
        
        System.out.println("m1:" + m1); // m1:name:Eddy, articleList:[id:1, content:hello world]
        System.out.println("u1:" + u1); // u1:name:Eddy, articlelist:null

    }

}

class Member {

    private String name;
    private List<Article> articleList; // <-- small camel case

    public Member() {
    }

    public Member(String name, List<Article> articleList) {
        this.name = name;
        this.articleList = articleList;
    }

    @Override
    public String toString() {
        return "name:" + name + ", " + "articleList:" + articleList;
    }

    // getters and setters
}

class Article {
    private Long id;
    private String content;

    public Article(Long id, String content) {
        this.id = id;
        this.content = content;
    }

    @Override
    public String toString() {
        return "id:" + id + ", content:" + content;
    }

    // getters and setters
}

class User {

    private String name;
    private String articlelist; // <-- all lowercase

    public String getName() {
        return name;
    }
    
    @Override
    public String toString() {
        return "name:" + name + ", " + "articleList:" + articlelist;
    }

    // getters and setters
}

此時u1.articlelist的內容卻是null,由此可知BeanUtils.copyProperties()只會複製屬性名稱相同的屬性值,若屬性名稱不同則被忽略。


BeanUtils.copyProperties()是利用Java的反射reflection來達成,以下是原始碼。

BeanUtils

public abstract class BeanUtils {
    ...
    private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
            @Nullable String... ignoreProperties) throws BeansException {

        Assert.notNull(source, "Source must not be null");
        Assert.notNull(target, "Target must not be null");

        Class<?> actualEditable = target.getClass();
        if (editable != null) {
            if (!editable.isInstance(target)) {
                throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
                        "] not assignable to Editable class [" + editable.getName() + "]");
            }
            actualEditable = editable;
        }
        PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
        List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);

        for (PropertyDescriptor targetPd : targetPds) {
            Method writeMethod = targetPd.getWriteMethod();
            if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
                PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName()); // 以目的對象(target)的屬性名稱來取得來源對象(source)的屬性
                if (sourcePd != null) {
                    Method readMethod = sourcePd.getReadMethod();
                    if (readMethod != null &&
                            ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                        try {
                            if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                readMethod.setAccessible(true);
                            }
                            Object value = readMethod.invoke(source); // 取得來源對象的屬性值
                            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                writeMethod.setAccessible(true);
                            }
                            writeMethod.invoke(target, value); // 把來源對象的屬性值寫入目的對象的同名屬性
                        }
                        catch (Throwable ex) {
                            throw new FatalBeanException(
                                    "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                        }
                    }
                }
            }
        }
    }
    ...
}

使用注意事項

  1. 由於是利用反射來達到複製的效果,因此效能上會比手動寫set方法來得差。
  2. 屬性的命名上必須統一,所以系統中各屬性的命名規則必須嚴格規範。
  3. 不好除錯,例如屬性中有Enum型態,但資料庫為字串等型態不一致,或是Entity類與DTO類命名不一致等也會造成屬性結果為null的情況。

專案中最好有專人專職負責維護整個系統的命名列表,中英文對照表,名詞解釋等,讓開發人員可以直接查找使用,如果找不到就寫信去申請由維護人員建立,否則就常看到同一種東西在程式中出現多種名稱,這樣BeanUtils.copyProperties()就派不上用場了,更嚴重的是維護上常令人困惑。
例如顧客編號customerId,customerID,custId,custID,clientId,clientId;
商品數量quantity, qty;
利息收入interestIncome,intstIncome,intrstIncome,intstRevenue;
一次性費用onetimeExpense,onceExpense,oneExpense等。

沒有留言:

張貼留言