java之經過反射生成並初始化對象

java之經過反射生成並初始化對象

在博文 《java之的讀取文件大全》 中讀取csv文件後,須要本身將csv文件的對象轉爲本身的DO對象,那麼有沒有辦法我直接穿進去一個DO的class對象,內部實現生成對象,並利用 CSVRecord 對象對其進行初始化呢 ?html

本篇主要是爲了解決上面的這個問題,實現了一個很是初級轉換方法,而後會分析下大名鼎鼎的BeanUtils是如何實現這種功能的java

1. CSVRecord對象轉xxxBO對象

在作以前,先把csv的讀取相關代碼貼出來,具體的實現邏輯詳解能夠參考 《java之的讀取文件大全》spring

CsvUtil.javaapache

/**
 * 讀取文件
 */
public static InputStream getStreamByFileName(String fileName) throws IOException {
        if (fileName == null) {
            throw new IllegalArgumentException("fileName should not be null!");
        }

        if (fileName.startsWith("http")) { // 網絡地址
            URL url = new URL(fileName);
            return url.openStream();
        } else if (fileName.startsWith("/")) { // 絕對路徑
            Path path = Paths.get(fileName);
            return Files.newInputStream(path);
        } else  { // 相對路徑
            return FileUtil.class.getClassLoader().getResourceAsStream(fileName);
        }
    }

/**
* 讀取csv文件, 返回結構話的對象
* @param filename csv 路徑 + 文件名, 支持絕對路徑 + 相對路徑 + 網絡文件
* @param headers  csv 每列的數據
* @return
* @throws IOException
*/
public static List<CSVRecord> read(String filename, String[] headers) throws IOException {
   try (Reader reader = new InputStreamReader(getStreamByFileName(fileName), Charset.forName("UTF-8"))) {
       CSVParser csvParser = new CSVParser(reader,
               CSVFormat.INFORMIX_UNLOAD_CSV.withHeader(headers)
       );

       return csvParser.getRecords();
   }
}

word.csv 文件json

dicId,"name",rootWord,weight
1,"質量",true,0.1
2,"服務",true,0.2
3,"發貨",,0.1
4,"性價比",false,0.4
5,"尺碼",true,0.4

測試用例數組

@Getter
@Setter
@ToString
static class WordDO {
   long dicId;

   String name;

   Boolean rootWord;

   Float weight;

   public WordDO() {
   }
}

@Test
public void testCsvRead() throws IOException {
   String fileName = "word.csv";
   List<CSVRecord> list = CsvUtil.read(fileName, new String[]{"dicId", "name", "rootWord", "weight"});
   Assert.assertTrue(list != null && list.size() > 0);

   List<WordDO> words = list.stream()
           .filter(csvRecord -> !"dicId".equals(csvRecord.get("dicId")))
           .map(this::parseDO).collect(Collectors.toList());
   logger.info("the csv words: {}", words);
}


private WordDO parseDO(CSVRecord csvRecord) {
   WordDO wordDO = new WordDO();
   wordDO.dicId = Integer.parseInt(csvRecord.get("dicId"));
   wordDO.name = csvRecord.get("name");
   wordDO.rootWord = Boolean.valueOf(csvRecord.get("rootWord"));
   wordDO.weight = Float.valueOf(csvRecord.get("weight"));
   return wordDO;
}

輸出結果緩存

16:17:27.145 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=1, name=質量, rootWord=true, weight=0.1)
16:17:27.153 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=2, name=服務, rootWord=true, weight=0.2)
16:17:27.154 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=3, name=發貨, rootWord=false, weight=0.1)
16:17:27.154 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=4, name=性價比, rootWord=false, weight=0.4)
16:17:27.154 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=5, name=尺碼, rootWord=true, weight=0.4)

從上面的使用來看,每次都要本身對解析出來的 CsvRecord 進行對象轉換, 咱們的目標就是把這個集成在 CsvUtil 內部去實現網絡

設計思路

反射建立對象,獲取對象的全部屬性,而後在屬性前面加 set 表示設置屬性的方法(boolea類型的屬性多是 isXXX格式), 經過反射設置方法的屬性值數據結構

  • 建立對象: T obj = clz.newInstance();
  • 獲取全部屬性: Field[] fields = clz.getDeclaredFields();
  • 設置屬性值
    • 方法名: fieldSetMethodName = "set" + upperCase(field.getName());
    • 屬性值,須要轉換對應的類型: fieldValue = this.parseType(value, field.getType());
    • 獲取設置屬性方法 : Method method = clz.getDeclaredMethod(fieldSetMethodName, field.getType());
    • 設置屬性: method.invoke(obj, fieldValue);

實現代碼

基本結構如上,先貼出實現的代碼,並對其中的幾點作一下簡短的說明app

private <T> T parseBO(CSVRecord csvRecord, Class<T> clz) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {

        // 建立BO對象
        T obj = clz.newInstance();


        // 獲取聲明的全部成員變量
        Field[] fields = clz.getDeclaredFields();


        // 保存屬性對應的csvRecord中的值
        String value;
        String fieldSetMethodName;
        Object fieldValue;
        for (Field field : fields) {
            // 設置爲可訪問
            field.setAccessible(true);

            // 將value轉換爲目標類型
            value = csvRecord.get(field.getName());
            if (value == null) {
                continue;
            }
            fieldValue = this.parseType(value, field.getType());


            // 獲取屬性對應的設置方法名
            fieldSetMethodName = "set" + upperCase(field.getName());
            Method method = clz.getDeclaredMethod(fieldSetMethodName, field.getType());
            

            // 設置屬性值
            method.invoke(obj, fieldValue);
        }


        return obj;
    }


    // 首字母變大寫
    private String upperCase(String str) {
        char[] ch = str.toCharArray();
//      也能夠直接用下面的記性轉大寫
//      ch[0] = Character.toUpperCase(ch[0]);
        if (ch[0] >= 'a' && ch[0] <= 'z') {
            ch[0] = (char) (ch[0] - 32);
        }
        return new String(ch);
    }


    /**
     * 類型轉換
     * 
     * @param value 原始數據格式
     * @param type 期待轉換的類型
     * @return 轉換後的數據對象
     */
    private Object parseType(String value, Class type) {

        if (type == String.class) {
            return value;
        } else if (type == int.class) {
            return value == null ? 0 : Integer.parseInt(value);
        } else if (type == float.class) {
            return value == null ? 0f : Float.parseFloat(value);
        } else if (type == long.class) {
            return value == null ? 0L : Long.parseLong(value);
        } else if (type == double.class) {
            return value == null ? 0D : Double.parseDouble(value);
        } else if (type == boolean.class) {
            return value != null && Boolean.parseBoolean(value);
        } else if (type == byte.class) {
            return value == null || value.length() == 0 ? 0 : value.getBytes()[0];
        } else if (type == char.class) {
            if (value == null || value.length() == 0) {
                return 0;
            }

            char[] chars = new char[1];
            value.getChars(0, 1, chars, 0);
            return chars[0];
        }

        // 非基本類型,
        if (StringUtils.isEmpty(value)) {
            return null;
        }


        if (type == Integer.class) {
            return Integer.valueOf(value);
        } else if (type == Long.class) {
            return Long.valueOf(value);
        } else if (type == Float.class) {
            return Float.valueOf(value);
        } else if (type == Double.class) {
            return Double.valueOf(value);
        } else if (type == Boolean.class) {
            return Boolean.valueOf(value);
        } else if (type == Byte.class) {
            return value.getBytes()[0];
        } else if (type == Character.class) {
            char[] chars = new char[1];
            value.getChars(0, 1, chars, 0);
            return chars[0];
        }


        throw new IllegalStateException("argument not basic type! now type:" + type.getName());
    }

1. 字符串的首字母大寫

最直觀的作法是直接用String的內置方法

return str.substring(0,1).toUpperCase() + str.substring(1);

由於substring內部實際上會新生成一個String對象,因此上面這行代碼實際上新生成了三個對象(+號又生成了一個),而咱們的代碼中, 則直接獲取String對象的字符數組,修改後從新生成一個String返回,實際只新生成了一個對象,稍微好一點

2. string 轉基本數據類型

注意一下將String轉換爲基本的數據對象,封裝對象時, 須要對空的狀況進行特殊處理

3. 幾個限制

BO對象必須是可實例化的

舉一個反例, 下面的這個 WordBO對象就沒辦法經過反射建立對象

public class CsvUtilTest {
    @Getter
    @Setter
    @ToString
    private static class WordBO {
        long dicId;

        String name;

        Boolean rootWord;

        Float weight;

//        public WordDO() {
//        }
    }
}

解決辦法是加一個默認的無參構造方法便可


BO對象要求

  • 顯示聲明無參構造方法
  • 屬性 abc 的設置方法命名爲 setAbc(xxx)
  • 屬性都是基本的數據結構 (若對象是以json字符串格式存csv文件時,可利用json工具進行反序列化,這樣可能會更加簡單)
  • BO對象的屬性名與CsvRecord中的對象名相同

測試一發

@Test
public void testCsvReadV2() throws IOException {
   String fileName = "word.csv";
   List<CSVRecord> list = CsvUtil.read(fileName, new String[]{"dicId", "name", "rootWord", "weight"});
   Assert.assertTrue(list != null && list.size() > 0);
   
   try {
       List<WordDO> words = new ArrayList<>(list.size() - 1);
       for (int i = 1; i < list.size(); i++) {
           words.add(parseDO(list.get(i), WordDO.class));
       }

       words.stream().forEach(
               word -> logger.info("the csv words: {}", word)
       );
   } catch (Exception e) {
       logger.error("parse DO error! e: {}", e);
   }
}

輸出結果

17:17:14.640 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=1, name=質量, rootWord=true, weight=0.1)
17:17:14.658 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=2, name=服務, rootWord=true, weight=0.2)
17:17:14.658 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=3, name=發貨, rootWord=null, weight=0.1)
17:17:14.659 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=4, name=性價比, rootWord=false, weight=0.4)
17:17:14.659 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=5, name=尺碼, rootWord=true, weight=0.4)

注意這裏發貨這一個輸出的 rootWord爲null, 而上面的是輸出false, 主要是由於解析邏輯不一樣致使


2. BeanUtils 分析

頂頂大名的BeanUtils, 目前流行的就有好多個 Apache的兩個版本:(反射機制) org.apache.commons.beanutils.PropertyUtils.copyProperties(Object dest, Object orig) org.apache.commons.beanutils.BeanUtils.copyProperties(Object dest, Object orig) Spring版本:(反射機制) org.springframework.beans.BeanUtils.copyProperties(Object source, Object target, Class editable, String[] ignoreProperties) cglib版本:(使用動態代理,效率高) net.sf.cglib.beans.BeanCopier.copy(Object paramObject1, Object paramObject2, Converter paramConverter)

本篇分析的目標放在 BeanUtils.copyProperties

先看一個使用的case

DoA.java

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class DoA {

    private String name;

    private long phone;
}

DoB.java

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class DoB {
    private String name;

    private long phone;
}

測試case

@Test
public void testBeanCopy() throws InvocationTargetException, IllegalAccessException {
   DoA doA = new DoA();
   doA.setName("yihui");
   doA.setPhone(1234234L);

   DoB doB = new DoB();
   BeanUtils.copyProperties(doB, doA);
   log.info("doB: {}", doB);

   BeanUtils.setProperty(doB, "name", doA.getName());
   BeanUtils.setProperty(doB, "phone", doB.getPhone());
   log.info("doB: {}", doB);
}

1, 屬性拷貝邏輯

實際看下屬性拷貝的代碼,

  • 獲取對象的屬性描述類 PropertyDescriptor,
  • 而後遍歷能夠進行賦值的屬性 getPropertyUtils().isReadable(orig, name) && getPropertyUtils().isWriteable(dest, name)
  • 獲取orgi屬性名 + 屬性值,執行賦值 copyProperty(dest, name, value);
PropertyDescriptor[] origDescriptors =
      getPropertyUtils().getPropertyDescriptors(orig);
for (int i = 0; i < origDescriptors.length; i++) {
 String name = origDescriptors[i].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 {
         Object value =
             getPropertyUtils().getSimpleProperty(orig, name);
        // 獲取源對象的 屬性名 + 屬性值, 調用 copyProperty方法實現賦值
         copyProperty(dest, name, value);
     } catch (NoSuchMethodException e) {
         // Should not happen
     }
 }

2. PropertyDescriptor

jdk說明: A PropertyDescriptor describes one property that a Java Bean exports via a pair of accessor methods.

根據class獲得這個屬性以後,基本上就get到各類屬性,以及屬性的設置方法了

內部的幾個關鍵屬性

// bean 的成員類型
private Reference<? extends Class<?>> propertyTypeRef;
// bean 的成員讀方法
private final MethodRef readMethodRef = new MethodRef();
// bean 的成員寫方法
private final MethodRef writeMethodRef = new MethodRef();

MethodRef.java, 包含了方法的引用

final class MethodRef {
    // 方法簽名 , 如 : public void com.hust.hui.quicksilver.file.test.dos.DoA.setName(java.lang.String)
    private String signature;
    private SoftReference<Method> methodRef;
    // 方法所在的類對應的class
    private WeakReference<Class<?>> typeRef;
}

一個實例的截圖以下

如何獲取 PropertyDescriptor 對象呢 ? 經過 java.beans.BeanInfo#getPropertyDescriptors 便可, 順着 PropertyDescriptor[] origDescriptors = getPropertyUtils().getPropertyDescriptors(orig); , 一路摸到如何根據 class 獲取 BeanInfo對象, 貼一下幾個重要的節點

  • org.apache.commons.beanutils.PropertyUtilsBean#getPropertyDescriptors(java.lang.Class<?>) <--

  • org.apache.commons.beanutils.PropertyUtilsBean#getIntrospectionData <--

  • org.apache.commons.beanutils.PropertyUtilsBean#fetchIntrospectionData <--

  • org.apache.commons.beanutils.DefaultBeanIntrospector#introspect <--

  • java.beans.Introspector#getBeanInfo(java.lang.Class<?>)

    beanInfo = new Introspector(beanClass, null, USE_ALL_BEANINFO).getBeanInfo();
    
    在建立 `Introspector` 對象時, 會遞歸獲取class的超類,也就是說超類中的屬性也會包含進來, 構造方法中,調用了下面的方法 `findExplicitBeanInfo` , 這裏實際上借用的是jdk的 `BeanInfoFinder#find()` 方法
    
    /**
     * 
     */
    private static BeanInfo findExplicitBeanInfo(Class<?> beanClass) {
        return ThreadGroupContext.getContext().getBeanInfoFinder().find(beanClass);
    }

3. 屬性拷貝

上面經過內省獲取了Bean對象的基本信息(成員變量 + 讀寫方法), 剩下的一個點就是源碼中的 copyProperty(dest, name, value); 實際的屬性值設置

看代碼中,用了不少看似高大上的東西,排除掉一些不關心的,主要乾的就是這麼幾件事情

  • 屬性描述對象 descriptor = getPropertyUtils().getPropertyDescriptor(target, name);
  • 參數類型 type = descriptor.getPropertyType();
  • 屬性值的類型轉換 value = convertForCopy(value, type);
  • 屬性值設置 getPropertyUtils().setSimpleProperty(target, propName, value);

最後屬性設置的源碼以下, 刪了不少不關心的代碼,基本上和咱們上面的實現相差不大

public void setSimpleProperty(Object bean,
                                         String name, Object value)
            throws IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {

        // Retrieve the property setter method for the specified property
        PropertyDescriptor descriptor =
                getPropertyDescriptor(bean, name);

        Method writeMethod = getWriteMethod(bean.getClass(), descriptor);

        // Call the property setter method
        Object[] values = new Object[1];
        values[0] = value;

        invokeMethod(writeMethod, bean, values);

    }

4. 小結

apache的BeanUtils實現屬性拷貝的思路和咱們上面的設計相差很少,那麼差距在哪 ? 仔細看 BeaUtils 源碼,發現有不少優化點

  • 獲取 clas對應的 BeanInfo 用了緩存,至關於一個class只用反射獲取一次便可,避免每次都這麼幹
  • 類型轉換,相比較咱們上面原始到爆的簡陋方案,BeanUtils使用的是專門作類型轉換的 Converter 來實現,全部你能夠本身定義各類類型的轉換,註冊進去後能夠實現各類鬼畜的場景了
  • 各類異常邊界的處理 (單反一個開源的成熟產品,這一塊真心沒話說)
  • DynaBean Map Array 這幾個類型單獨進行處理,上面也沒有分析
  • 用內省來操做JavaBean對象,而非使用反射 參考博文《深刻理解Java:內省(Introspector)》
相關文章
相關標籤/搜索