要實現Excel動態解析,實現解析與業務代碼邏輯相解耦;那麼我們不難會想起一個Java的一個關鍵技術-Reflection(反射原理),Python、Ruby等是動態語言而理論上Java是一門靜態語言,但是Java引入了Reflection技術實現了動態性。反射原理我們都比較熟悉,就是在運行期間動態... ...
目錄樹
- 背景
- 技術選型
- 問題分析
- 技術要點及難點分析
- 源碼分析
- 測試用例
背景
Tip:因為產品提的需求我都開發完了,進行了項目提測;前天老大走過來說:你用spring-boot開發一個解析Excel的jar包.....詳細對話如下:
A:世生,你用spring-boot開發一個解析Excel的jar包。
B:為什麼不在原來的項目上進行開發呢?(很納悶,這個東西不是一般用於前端上傳數據的嘛,這是後端層,咋搞這個)
A:因為xxxx這個需求有比較多的數據需要初始化,這個jar是作為一個上線的數據初始化腳本
B:嗯,好的
技術選型
畢竟是第一次寫解析Excel代碼,問了度娘說是有兩種方式可以做到。一是利用wso2的jxl解析,二是利用apache的poi解析;我去maven repository官網搜索了這兩個jar包,對比了下jml的最新版本是2.6.12竟然是2011.05月更新的,而poi的最新版本是4.0.x是2018.08更新的;個人覺得jml最新版本都是2011.05更新的,相對於apache的poi包來說更不靠譜;不斷持續更新的開源項目或者開源jar包不管是bug還是相容性相對來說是越來越好。所以最終選定用apache大佬的poi進行開發。
問題分析
解析Excel的關鍵點是在於從Excel表格中讀取數據到記憶體(解析Excel),然後可能是校驗數據、通過業務邏輯分析數據、最終持久化到資料庫中;其實這其中最重要的不是解析Excel,而是將解析出的數據持久化到資料庫中以備有用之需。而解析Excel的這塊功能只能算是一個Util類,不能與業務代碼耦合一起;然後我看到很多的Excel解析相關的代碼都是在解析數據中混淆業務代碼邏輯,其實這些都是不可取的,這會導致解析Excel邏輯與業務邏輯相耦合也就是冗雜、代碼重用率低、可擴展性低等問題。因為之前在做項目的時候遇到過一個問題:我負責的模塊是一個中間模塊(通訊採用Dubbo),其他系統要依賴我這個介面進行請求的轉發可我調用其他系統返回的結果對象卻各個都不同,我叫其他系統負責人說要統一調用介面返回的對象,但是其他系統的負責人都不是同一個人執行起來效率太低了,在歷經一個星期都無果的情況下我只能撒下自己的殺手鐧了;在這種極端條件下最終我不管其他數據的介面返回的對象是什麼,我直接用Object接收返回類型,通過反射獲取決定請求成功與否的屬性值(欣慰的是當時必傳屬性值倒是一樣的)。通過這種方法我可以少些很多的代碼(當時其他系統有15+),不然的話每調用不同系統的介面我都需要進行邏輯判斷或者是乾脆對於調用他們不同的系統我採用不同介面進行轉發,但選用這種方法卻便利多了。
以上問題分析及一個場景的描述很好理解,但是通過Object接收返回信息得這個場景事實上卻有所欠佳;返回對象不同的這個問題最好的處理方案就是統一介面,我那個方案是在需求推動但別人無法及時配合的極端條件下使用的,是沒辦法中的辦法,但這也是一個沒有辦法中的一個最優的處理方案,相容性比較強。以下我就用圖來分析這兩種情況的比較:
1.非動態模式:將Excel數據載入到記憶體與業務代碼邏輯混合,數據在解析期間交叉傳遞。弊端:每新增一個需要解析的Excel,解析Excel代碼塊就需要重新開發,代碼復用率底下、可擴展性也低。
2.動態模式:將Excel數據載入到記憶體與業務代碼邏輯分開;Excel數據載入到記憶體之後才將數據傳遞給業務代碼邏輯處理,解析Excel與業務代碼之間分開;優點:將解析Excel的這部分代碼封裝為一個ExcelUtil,代碼復用率明顯提高,而且解析與業務代碼間實行解耦,可擴展性增強。
技術要點及難點分析
要實現動態解析,實現解析與業務代碼邏輯相解耦;那麼我們不難會想起一個Java的一個關鍵技術-Reflection(反射原理),Python、Ruby等是動態語言而理論上Java是一門靜態語言,但是Java引入了Reflection技術實現了動態性。反射原理我們都比較熟悉,就是在運行期間動態獲取類的所有屬性及其方法,可以對這些數據進行相關的操作。以上動態解析Excel的實現就需要用到Java這項的高級技術了,通過這項技術可以實現動態解析、解析與業務邏輯解耦等。為了實現動態解析的目的我應用了Java反射技術,但是在開發的過程我發現反射執行一行數據結束的時候如何保存呢?換句話說就是:解析的時候一行的Excel數據封裝後就是一個bean,一個Excel表格就是多個bean 即“beans”;如果我們直接將反射執行後的結果保存至List中,當解析整個Excel結束後我們會發現,整個List裡面的對象的值完全一樣的?what?這是什麼原因導致的呢?這就是類似於:Object obj=new Object(),我們每次解析都只是把 obj 放在List中,List中的每一個對象都是同一個 obj(引用不變,實例也不變),所以自然也就相同了;因為當一個類執行反射的時候其實它的運行時狀態只有一個,也就是類似於只有一個實例,而傳統的解析Excel是解析出一條數據就new一個對象進行封裝數據,然後將bean存放至List。然而有什麼方法能夠解決這一類問題呢?那就是Object 的native clone()方法了,clone()這個大佬是比較牛逼的一個人物,在不創建對象的情況下將屬性值複製給另一個對象,具體實現需要實現Cloneable介面並重寫clone()。而解決這個問題的方式就是在每解析完一行Excel數據的時候,反射調用該對象的clone方法。動態解析具體實現應用了Apache POI、 LRUCache(LRU緩存)、Reflection(反射)、java的Clone等技術。如果以上技術沒有瞭解過的朋友可以去自行瞭解,這裡不加贅述。
前提條件:因為要實現動態解析,動態設置值,那麼我們在反射執行set操作的時候就需要知道相應的setMethod(),那麼我們可以在Excel規定第一行就是屬性欄位,並且欄位名稱跟bean的名稱一樣,讀取的時候先把第一行的數據放在一個String []數組中。具體實現請參照以下源碼。我已經把相關代碼打包成Jar,需要的朋友可以自行下載;Jar包下載鏈接:https://pan.baidu.com/s/1fKCCh54S3ZtHfv66T2pk2w 密碼:nur8
使用方法:新建bean用於存儲Excel數據信息,每個屬性需要有get、set操作,屬性與Excel首行相同,最重要的一點是要實現Clonesble介面重寫clone方法。Excel使用Office編輯,親測Wps編輯的Excel某些屬性值有問題。在new ReadExcelUtil 的時候只需要將對象類型與Excel文件路徑傳入構造函數即可,然後調用 ReadExcelUtil的getObjectList即可得到解析後的所有對象。至於這個對象你可以用任何的對象,你可以換成Teacher、OrderInfo、UserInfo......但是前面提到的:Excel第一行的屬性欄位需要與bean的屬性欄位一致,否則無法調用目標方法,具體可參見ReflectionInitValue的方法。具體實現請參見:文章末尾的Test類測試。
源碼分析
- 前提條件:引入Apache POI 的Maven倉庫坐標,我這裡使用的是3.25版本的。
1 <!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
2 <dependency> 3 <groupId>org.apache.poi</groupId> 4 <artifactId>poi</artifactId> 5 <version>3.15</version> 6 </dependency> 7 <dependency> 8 <groupId>org.apache.poi</groupId> 9 <artifactId>poi-ooxml</artifactId> 10 <version>3.15</version> 11 </dependency>
- 主要類:Common.java、LRUCache.java、LRUCacheException.java、ResolveFileException.java、ReadExcelUtil.java、ReflectionInitValue.java、Student、Test
- Common.java:基礎常量池,主要用於反射執行Method方法時判斷Method的參數類型的常量。
1 package com.hdbs.common; 2 3 /** 4 * @author :cnblogs-WindsJune 5 * @version :1.1.0 6 * @date :2018年9月20日 下午6:33:54 7 * @comments :解析Excel公共類常量類 8 */ 9 10 public class Common { 11 12 public static final String OFFICE_EXCEL_2003_POSTFIX_xls = "xls"; 13 public static final String OFFICE_EXCEL_2010_POSTFIX_xlsx = "xlsx"; 14 public static final String DATA_TYPE_long ="long"; 15 public static final String DATA_TYPE_boolean ="boolean"; 16 public static final String DATA_TYPE_int ="int"; 17 public static final String DATA_TYPE_float ="float"; 18 public static final String DATA_TYPE_double ="double"; 19 public static final String DATA_TYPE_Long ="class java.lang.Long"; 20 public static final String DATA_TYPE_Integer ="class java.lang.Integer"; 21 22 23 }
- LRUCacheException.java;LRU緩存自定義異常類。
1 package com.hdbs.exceptions; 2 3 /** 4 * Creater: cnblogs-WindsJune 5 * Date: 2018/9/21 6 * Time: 10:04 7 * Description: No Description 8 */ 9 public class LRUCacheException extends Exception{ 10 /** 11 * 錯誤碼 12 */ 13 private String errorCode; 14 15 /** 16 * 錯誤描述 17 */ 18 private String errorMessage; 19 20 public LRUCacheException(String errorCode, String errorMessage) { 21 this.errorCode = errorCode; 22 this.errorMessage = errorMessage; 23 } 24 25 public LRUCacheException(String message) { 26 super(message); 27 this.errorMessage = errorMessage; 28 } 29 30 public String getErrorCode() { 31 return errorCode; 32 } 33 34 public void setErrorCode(String errorCode) { 35 this.errorCode = errorCode; 36 } 37 38 public String getErrorMessage() { 39 return errorMessage; 40 } 41 42 public void setErrorMessage(String errorMessage) { 43 this.errorMessage = errorMessage; 44 } 45 }
- ResolveFileException.java;解析Excel自定義異常類。
1 package com.hdbs.exceptions; 2 /** 3 * Creater: cnblogs-WindsJune 4 * Date: 2018/9/20 5 * Time: 19:44 6 * Description: 解析Excel的公共異常類 7 */ 8 9 public class ResolveFileException extends RuntimeException{ 10 11 /** 12 * 錯誤碼 13 */ 14 private String errorCode; 15 16 /** 17 * 錯誤描述 18 */ 19 private String errorMessage; 20 21 public ResolveFileException(String errorCode, String errorMessage) { 22 this.errorCode = errorCode; 23 this.errorMessage = errorMessage; 24 } 25 26 public ResolveFileException(String message) { 27 super(message); 28 this.errorMessage = errorMessage; 29 } 30 31 public String getErrorCode() { 32 return errorCode; 33 } 34 35 public void setErrorCode(String errorCode) { 36 this.errorCode = errorCode; 37 } 38 39 public String getErrorMessage() { 40 return errorMessage; 41 } 42 43 public void setErrorMessage(String errorMessage) { 44 this.errorMessage = errorMessage; 45 } 46 }
- LRUCache.java:LRU緩存池,主要用於不同線程反射獲取的Methods,減少相同線程反射執行次數,減輕應用的負載、提高執行效率。我這裡是基於LinkedHashMap實現的LRU緩存,你也可以用數組或者其他方式實現該演算法。以下代碼邏輯如果不能理解的可以先去瞭解LinkedHashSet的源碼。
1 package com.hdbs.common; 2 3 import com.hdbs.exceptions.LRUCacheException; 4 import org.slf4j.Logger; 5 import org.slf4j.LoggerFactory; 6 7 import java.lang.reflect.Method; 8 import java.util.LinkedHashMap; 9 import java.util.Map; 10 11 /** 12 * Creater: cnblogs-WindsJune 13 * Date: 2018/9/20 14 * Time: 19:44 15 * Description: LinkedHashMap實現LRU緩存不同線程反射獲取的Method方法 16 */ 17 public class LRUCache { 18 private static final Logger LOGGER=LoggerFactory.getLogger(LRUCache.class); 19 //緩存容量 20 private static final int cacheSize = 10; 21 22 private static final Map<Integer,Method[]> cacheMap = new LinkedHashMap<Integer, Method[]>((int) Math.ceil(cacheSize / 0.75f) + 1, 0.75f, true){ 23 @Override 24 protected boolean removeEldestEntry(Map.Entry<Integer,Method[]> eldest){ 25 26 return size()> cacheSize; 27 28 } 29 }; 30 31 /** 32 * 設置緩存 33 * @param key 34 * @param methods 35 * @return boolean 36 */ 37 public static boolean set (Integer key,Method [] methods) throws LRUCacheException { 38 try { 39 cacheMap.put(key,methods); 40 return true; 41 } 42 catch ( Exception e ){ 43 throw new LRUCacheException("Set LRU緩存異常!"); 44 } 45 } 46 47 /** 48 * 獲取緩存的Method 49 * @param key 50 * @return Method 51 */ 52 public static Method[] get(Integer key) throws LRUCacheException { 53 Method[] methods=null; 54 try { 55 methods=cacheMap.get(key); 56 }catch ( Exception e ){ 57 throw new LRUCacheException("Get LRU緩存異常!{}"); 58 } 59 return methods; 60 } 61 }
- ReadExcelUtil.java;解析Excel數據工具類(將Excel載入到記憶體)
1 package com.hdbs.resolver; 2 3 import com.hdbs.common.Common; 4 import com.hdbs.exceptions.ResolveFileException; 5 import org.apache.commons.lang3.StringUtils; 6 import org.apache.poi.hssf.usermodel.HSSFCell; 7 import org.apache.poi.hssf.usermodel.HSSFRow; 8 import org.apache.poi.hssf.usermodel.HSSFSheet; 9 import org.apache.poi.hssf.usermodel.HSSFWorkbook; 10 import org.apache.poi.xssf.usermodel.XSSFCell; 11 import org.apache.poi.xssf.usermodel.XSSFRow; 12 import org.apache.poi.xssf.usermodel.XSSFSheet; 13 import org.apache.poi.xssf.usermodel.XSSFWorkbook; 14 import org.slf4j.Logger; 15 import org.slf4j.LoggerFactory; 16 17 import java.io.File; 18 import java.io.FileInputStream; 19 import java.io.IOException; 20 import java.io.InputStream; 21 import java.lang.reflect.InvocationTargetException; 22 import java.util.ArrayList; 23 import java.util.HashMap; 24 import java.util.List; 25 import java.util.Map; 26 27 /** 28 * @author :cnblogs-WindsJune 29 * @version :1.1.0 30 * @date :2018年9月20日 下午6:13:43 31 * @comments : 32 */ 33 34 public class ReadExcelUtil { 35 36 private static final Logger LOGGER = LoggerFactory.getLogger(ReadExcelUtil.class); 37 //存放屬性集 38 private Map<Integer,String []> fieldsMap=new HashMap<>(); 39 //存放解析後的對象List 40 private List<Object> objectsList = new ArrayList<>(); 41 //反射運行時對象 42 private Object object=null; 43 //Excel文件路徑 44 private String path =null; 45 //獲取解析後的對象集 46 public List<Object> getObjectsList() { 47 return this.objectsList; 48 } 49 50 public ReadExcelUtil(Object object,String path) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException { 51 this.object=object; 52 this.path=path; 53 readExcel(); 54 } 55 56 /** 57 * 添加Object到List中 58 * @param object 59 * @return 60 */ 61 public boolean addListObject(Object object){ 62 boolean isSucceed=this.objectsList.add(object); 63 return isSucceed; 64 } 65 66 /** 67 * 讀取excel,判斷是xls結尾(2010之前);還是xlsx結尾(2010以後)的Excel 68 * 69 * @return 70 * @throws IOException 71 */ 72 public boolean readExcel() throws IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { 73 if (StringUtils.isEmpty(path)) { 74 return false; 75 } else { 76 // 截取尾碼名,判斷是xls還是xlsx 77 String postfix = path.substring(path.lastIndexOf(".") + 1); 78 if (!StringUtils.isEmpty(postfix)) { 79 if (Common.OFFICE_EXCEL_2003_POSTFIX_xls.equals(postfix)) { 80 return readXls(); 81 } else if (Common.OFFICE_EXCEL_2010_POSTFIX_xlsx.equals(postfix)) { 82 return readXlsx(); 83 } 84 } else { 85 LOGGER.error("文件尾碼名有誤!"); 86 throw new ResolveFileException("文件尾碼名有誤!" + "[" + path + "]"); 87 } 88 } 89 return false; 90 } 91 92 /** 93 * 讀取xls(2010)之後的Excel 94 * 95 * @return 96 * @throws IOException 97 */ 98 public boolean readXlsx() throws IOException{ 99 File file = new File(path); 100 InputStream is = new FileInputStream(file); 101 XSSFWorkbook xssfWorkbook = new XSSFWorkbook(is); 102 // 遍歷sheet頁 103 for (int numSheet = 0; numSheet < xssfWorkbook.getNumberOfSheets(); numSheet++) { 104 XSSFSheet xssfSheet = xssfWorkbook.getSheetAt(numSheet); 105 String [] fields=null; 106 if (xssfSheet == null) { 107 continue; 108 } 109 // 迴圈行 110 for (int rowNum = 0; rowNum <= xssfSheet.getLastRowNum(); rowNum++) { 111 XSSFRow xssfRow = xssfSheet.getRow(rowNum); 112 int cloumns=xssfRow.getLastCellNum(); 113 int i=0; 114 //獲取第一行的所有屬性 115 if (rowNum == 0){ 116 fields=getFields(xssfRow,cloumns); 117 fieldsMap.put(numSheet,fields); 118 continue; 119 } 120 //遍曆數據,反射set值 121 while (i<cloumns){ 122 XSSFCell field=xssfRow.getCell(i); 123 String value=getValue(field); 124 try { 125 ReflectionInitValue.setValue(object,fields[i],value); 126 }catch ( Exception e ){ 127 throw new ResolveFileException(e.getMessage()); 128 } 129 i++; 130 } 131 //通過反射執行clone複製對象 132 Object result=ReflectionInitValue.invokeClone(object,"clone"); 133 this.addListObject(result); 134 // System.out.println(object.toString()); 135 } 136 } 137 return true; 138 } 139 140 /** 141 * 讀取xls(2010)之前的Excel 142 * 143 * @return 144 * @throws IOException 145 */ 146 public boolean readXls() throws IOException, ResolveFileException { 147 InputStream is = new FileInputStream(path); 148 HSSFWorkbook hssfWorkbook = new HSSFWorkbook(is); 149 // 遍歷sheet頁 150 for (int numSheet = 0; numSheet < hssfWorkbook.getNumberOfSheets(); numSheet++) { 151 HSSFSheet hssfSheet = hssfWorkbook.getSheetAt(numSheet); 152 String[] fields = null; 153 if (hssfSheet == null) { 154 continue; 155 } 156 // 迴圈行Row 157 for (int rowNum = 0; rowNum <= hssfSheet.getLastRowNum(); rowNum++) { 158 HSSFRow hssfRow = hssfSheet.getRow(rowNum); 159 int cloumns=hssfRow.getLastCellNum(); 160 int i=0; 161 //獲取第一行的所有屬性 162 if (rowNum == 0){ 163 //獲取屬性欄位 164 fields=getFields(hssfRow,cloumns); 165 fieldsMap.put(numSheet,fields); 166 continue; 167 } 168 //遍曆數據,反射set值 169 while (i<cloumns){ 170 HSSFCell field=hssfRow.getCell(i); 171 String value=getValue(field); 172 try { 173 ReflectionInitValue.setValue(object,fields[i],value); 174 }catch ( Exception e ){ 175 throw new ResolveFileException(e.getMessage()); 176 } 177 i++; 178 } 179 //通過反射執行clone複製對象 180 Object result=ReflectionInitValue.invokeClone(object,"clone"); 181 this.addListObject(result); 182 } 183 } 184 return true; 185 } 186 187 /** 188 * xlsx -根據數據類型,獲取單元格的值 189 * @param xssfRow 190 * @return 191 */ 192 @SuppressWarnings({ "static-access" }) 193 private static String getValue(XSSFCell xssfRow) { 194 String value=null; 195 try { 196 if (xssfRow.getCellType() == xssfRow.CELL_TYPE_BOOLEAN) { 197 // 返回布爾類型的值 198 value=String.valueOf(xssfRow.getBooleanCellValue()).replace(" ",""); 199 } else if (xssfRow.getCellType() == xssfRow.CELL_TYPE_NUMERIC) { 200 // 返回數值類型的值 201 value= String.valueOf(xssfRow.getNumericCellValue()).replace(" ",""); 202 } else { 203 // 返回字元串類型的值 204 value= String.valueOf(xssfRow.getStringCellValue()).replace(" ",""); 205 } 206 } catch (Exception e) { 207 //單元格為空,不處理 208 value=null; 209 LOGGER.error("單元格為空!"); 210 } 211 return value; 212 } 213 214 /** 215 * xls-根據數據類型,獲取單元格的值 216 * @param hssfCell 217 * @return 218 */ 219 @SuppressWarnings({ "static-access" }) 220 private static String getV