在博文 《java之的讀取文件大全》 中讀取csv文件後,須要本身將csv文件的對象轉爲本身的DO對象,那麼有沒有辦法我直接穿進去一個DO的class對象,內部實現生成對象,並利用
CSVRecord
對象對其進行初始化呢 ?html
本篇主要是爲了解決上面的這個問題,實現了一個很是初級轉換方法,而後會分析下大名鼎鼎的BeanUtils
是如何實現這種功能的java
CSVRecord
對象轉xxxBO
對象在作以前,先把csv的讀取相關代碼貼出來,具體的實現邏輯詳解能夠參考 《java之的讀取文件大全》spring
CsvUtil.java
apache
/** * 讀取文件 */ 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()); }
最直觀的作法是直接用String的內置方法
return str.substring(0,1).toUpperCase() + str.substring(1);
由於substring
內部實際上會新生成一個String對象,因此上面這行代碼實際上新生成了三個對象(+號又生成了一個),而咱們的代碼中, 則直接獲取String對象的字符數組,修改後從新生成一個String返回,實際只新生成了一個對象,稍微好一點
注意一下將String轉換爲基本的數據對象,封裝對象時, 須要對空的狀況進行特殊處理
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)
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, 主要是由於解析邏輯不一樣致使
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); }
實際看下屬性拷貝的代碼,
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 } }
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); }
上面經過內省獲取了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); }
apache的BeanUtils實現屬性拷貝的思路和咱們上面的設計相差很少,那麼差距在哪 ? 仔細看 BeaUtils
源碼,發現有不少優化點
BeanInfo
用了緩存,至關於一個class只用反射獲取一次便可,避免每次都這麼幹BeanUtils
使用的是專門作類型轉換的 Converter
來實現,全部你能夠本身定義各類類型的轉換,註冊進去後能夠實現各類鬼畜的場景了DynaBean
Map
Array
這幾個類型單獨進行處理,上面也沒有分析