1. 程式人生 > 實用技巧 >Java常用屬性拷貝工具類使用總結

Java常用屬性拷貝工具類使用總結

開頭聊幾句

  • 1、網上很多的技術文章和資料是有問題的,要學會辨證的看待,不能隨便就拿來用,起碼要自己驗證一下
  • 2、關注當下,關注此刻,如果你真正閱讀本篇文章,請花幾分鐘時間的注意力閱讀,相信你會有收穫的
  • 3、文中程式碼沒有使用圖片展示,可能存在閱讀排版錯亂問題,請見諒,因為可能考慮到有其他夥伴需要拷貝程式碼,這樣比較方便

Java常用屬性拷貝工具類使用總結

對專案中經常使用的屬性拷貝工具類進行總結:

  • org.apache.commons.beanutils.BeanUtils
  • org.apache.commons.beanutils.PropertyUtils
  • org.springframework.beans.BeanUtils

本文使用的工具類對應的版本:
commons-beanutils:1.9.4
spring-beans:5.0.7.RELEASE

欄位和屬性

首先明確下在Java中欄位和屬性的區別。


屬性是不是類裡最上邊的那些全域性變數嗎?比如:

public class UserTest{
    private String userName;
    private String password;


    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getHello() {
        return "hello";
    }

    public void setHello(String str) {
    }
}


上面 private String userName;private String password;。準確的來說它們應該稱為:欄位,而不是本次要講的屬性。


下面簡述一下:什麼是Java中的屬性


Java中的屬性(property),通常可以理解為get和set方法,而欄位(field),通常叫做“類成員”,或“類成員變數”,有時也叫“域”,理解為“資料成員”,用來承載資料的。

直白點就是Java中的屬性是指:設定和讀取欄位的方法,也就是平常見到的set和get方法。只要是set和get開頭的方法在Java裡都認為它是屬性(請注意這句話,等下後邊會寫程式碼做驗證)


屬性名稱:就是set和get方法名 去掉"set"和"get"後的內容

比如:

public void setUserName(String userName) {
	this.userName = userName;
}

它的屬性名稱是:userName(也就是方法名稱”setUserName”去掉“set”)


當然 setUserName和 getUserName 方法是指同一個屬性 UserName,


這裡再次提醒:欄位和屬性不是同一個東西。


程式碼驗證屬性
上面程式碼中還有一個 getHello  和 setHello  , JDK 中有個API Introspector






獲取的是java.beans.BeanInfo 類。這個類可以通過

java.beans.BeanInfo#getPropertyDescriptors : 獲取java bean 所有的屬性。

public static void main(String[] args) throws IntrospectionException {
    BeanInfo beanInfo = Introspector.getBeanInfo(UserTest.class);
    // 得到類中的所有的屬性描述器
    PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
    System.out.println("屬性的個數:" + pds.length);
    for (PropertyDescriptor pd : pds) {
        System.out.println("屬性:" + pd.getName());
    }
}

結果:

屬性的個數:4
屬性:class
屬性:hello
屬性:password
屬性:userName


上面多了一個 class ,原因很簡單,因為Object類是所有類的父類,Object類裡有個方法叫 getClass();
所以這也驗證了咱們剛才說的: “只要是set或者get開頭的方法都叫屬性”

使用說明

default (即預設,什麼也不寫): 在同一包內可見,不使用任何修飾符。使用物件:類、介面、變數、方法。
public : 對所有類可見。使用物件:類、介面、變數、方法
private : 在同一類內可見。使用物件:變數、方法。 注意:不能修飾類(外部類)
protected : 對同一包內的類和所有子類可見。使用物件:變數、方法。 注意:不能修飾類(外部類)

org.springframework.beans.BeanUtils#copyProperties

1.基本型別和包裝型別會自動轉換, 方法名稱相同,返回值型別和引數型別不同,不進行復制,也不報錯_

2.支援指定忽略某些屬性不復制

3、支援類的修飾符 default 、 public

org.apache.commons.beanutils.PropertyUtils#copyProperties

1.基本型別和包裝型別會自動轉換

2.方法名稱相同,返回值型別和引數型別不同,複製失敗,會報錯,如下:

_argument type mismatch - had objects of type "java.lang.Double" but expected signature "java.lang.String"

3.只支援類的修飾符 public,如果是default 則直接不會進行轉換(注意內部類複製也要加public)

org.apache.commons.beanutils.BeanUtils#_copyProperties

1.基本型別和包裝型別會自動轉換

2.方法名稱相同,返回值型別和_ _引數型別不同,不復制,不報錯

3.只支援類的修飾符 public,如果是default 則直接不會進行轉換(注意內部類複製也要加public)


tips: Spring和apache的_copyProperties_屬性的方法源和目的引數的位置正好相反,所以導包和呼叫的時候都要注意一下。

// Apache
public static void 
  copyProperties(final Object dest, final Object orig)
    
// Spring
public static void
  copyProperties(Object source, Object target)

效能參考:
Bean複製的幾種框架效能比較(Apache BeanUtils、PropertyUtils,Spring BeanUtils,Cglib BeanCopier)


摘要總結:Spring是在次數增多的情況下,效能較好,在資料較少的時候,效能比PropertyUtils的效能差一些。PropertyUtils的效能相對穩定,表現是呈現線性增長的趨勢。而Apache的BeanUtil的效能最差,無論是單次Copy還是大數量的多次Copy效能都不是很好。

使用的壓測工具備忘:Java使用JMH進行簡單的基準測試Benchmark : http://irfen.me/java-jmh-simple-microbenchmark/

根據上面的具體的分析還是使用 :org.springframework.beans.BeanUtils#copyProperties
原因

1.這個方法在複製的時候不會因為屬性的不同而報錯,影響程式碼執行

2.效能方面也相對較好


其他Apache的兩個,
1、org.apache.commons.beanutils.PropertyUtils#copyProperties 複製會直接報錯

2、org.apache.commons.beanutils.BeanUtils#copyProperties 效能相對較差

原理探索

核心本質都是使用反射實現。具體的實現程式碼稍有不同。

Spring#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());
            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、獲取 目標物件 所有的屬性 targetPds

PropertyDescriptor_[] _targetPds = getPropertyDescriptors(actualEditable);

2、迴圈 targetPds ,並在源物件取出對應的屬性

PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName_())_;


3、r如果不是修飾不是public,暴力反射 ,然後使用對屬性進行設值

setAccessible_(true);// 暴力反射
writeMethod.invoke
(target, value)_;


### apache.commons#BeanUtils
  • org.apache.commons.beanutils.BeanUtilsBean#copyProperties

簡單擷取核心程式碼:

// org.apache.commons.beanutils.BeanUtilsBean#copyProperties

final PropertyDescriptor[] origDescriptors =
    getPropertyUtils().getPropertyDescriptors(orig);
for (PropertyDescriptor origDescriptor : origDescriptors) {
    final String name = origDescriptor.getName();
    if ("class".equals(name)) {
        continue; // No point in trying to set an object's class
    }
    if (getPropertyUtils().isReadable(orig, name) &&
        getPropertyUtils().isWriteable(dest, name)) {
        try {
            final Object value =
                getPropertyUtils().getSimpleProperty(orig, name);
            copyProperty(dest, name, value);
        } catch (final NoSuchMethodException e) {
            // Should not happen
        }
    }
}
// org.apache.commons.beanutils.BeanUtilsBean#copyProperty
getPropertyUtils().setSimpleProperty(target, propName, value);

// org.apache.commons.beanutils.PropertyUtilsBean#setSimpleProperty
 invokeMethod(writeMethod, bean, values);

1、 獲取的是源物件的所有的屬性

final PropertyDescriptor[] origDescriptors =  getPropertyDescriptors(orig);

2、如果屬性是class,不復制

if ("class".equals_(name)) {    continue; // No point in trying to set an object's class}_

3、迴圈源物件的屬性,做一些檢驗

copyProperty(dest, name, value);
1、會檢驗目標物件是否有源物件的屬性,沒有跳過
2、獲取屬性的名稱型別

4、然後給目標物件設定,最終還是使用反射

method.invoke_(bean, values)_;

apache.commons#PropertyUtils

  • org.apache.commons.beanutils.PropertyUtilsBean#copyProperties

簡單擷取核心程式碼:

// org.apache.commons.beanutils.PropertyUtilsBean#copyProperties
final PropertyDescriptor[] origDescriptors =
    getPropertyDescriptors(orig);
for (PropertyDescriptor origDescriptor : origDescriptors) {
    final String name = origDescriptor.getName();
    if (isReadable(orig, name) && isWriteable(dest, name)) {
        try {
            final Object value = getSimpleProperty(orig, name);
            if (dest instanceof DynaBean) {
                ((DynaBean) dest).set(name, value);
            } else {
                setSimpleProperty(dest, name, value);
            }
        } catch (final NoSuchMethodException e) {
            if (log.isDebugEnabled()) {
                log.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", e);
            }
        }
    }
}
// org.apache.commons.beanutils.PropertyUtilsBean#invokeMethod
method.invoke(bean, values);

1、 獲取的是源物件的所有的屬性

final PropertyDescriptor[] origDescriptors =  getPropertyDescriptors(orig);

2、迴圈源物件的屬性,然後給目標物件設定,最終還是使用反射

總結

結合使用說明以及相關的效能和原理分析,建議使用 org.springframework.beans.BeanUtils#copyPropertie

參考資料

https://www.cnblogs.com/kaka/archive/2013/03/06/2945514.html


Java程式設計技術樂園:分享乾貨技術,每天進步一點點,小的積累,帶來大的改變!

掃描關注,後臺回覆【祕籍】,獲取珍藏乾貨! 99.9%的夥伴都很喜歡