文章标題
-
-
-
-
-
- 一: 使用場景
- 二: 技術選型
- 三: 常用API介紹
- 四: 測試
- 五: 總結
-
-
-
-
溫馨提示: 本文總共6334字,閱讀完大概需要6-8分鐘,希望您能耐心看完,倘若你對該知識點已經比較熟悉,你可以直接通過目錄跳轉到你感興趣的地方,希望閱讀本文能夠對您有所幫助,如果閱讀過程中有什麼好的建議、看法,歡迎在文章下方留言或者私信我,您的意見對我非常寶貴,再次感謝你閱讀本文。
一: 使用場景 |
一: 使用場景
在日常的系統開發中,系統支援批量資料的操作是一個很常見的功能,其中,最常用的方式是使用excel表格對資料進行批量添加、删除,如:批量建立訂單、批量添加商品等。
二: 技術選型 |
二: 技術選型
現在市面上有很多技術實作來支援excel資料解析如:POI、JXL等,但是,這些技術或多或少都存在着一些問題,下面進行具體分析:
(一): POI
POI是目前使用最多的用來做excel解析的架構,但這個架構還存在在這個許多問題。現在使用POI技術來解析excel檔案的,大多數都是使用到它的userMode模式,好處是上手比較簡單,而且網上比較多封裝好的代碼,雖然複制一下就可以運作,這個對于資料量不大的檔案的時候是可以使用,但是當資料量大的時候會存在巨大隐患。
1、userMode模式存在着一個巨大的問題就是記憶體消耗很大,一個幾兆的檔案解析需要上百兆的記憶體,當并發量大的時候就會容易出現OOM(記憶體溢出)或者頻繁進行full GC回收),導緻程式執行緩慢甚至崩潰。
2、如果有深入了解過POI的會發現,其他它針對這個情況提供了一種叫SAX的模式,但是,這種模式相對複雜,且對excel 03版本和07版本不相容,兩個版本的資料存儲方式不一樣,是以解析也不一樣,這樣需要同個功能需要進行兩套代碼開發,時間周期長,且不易于維護。
3、在大并發情況下,POI還存在着一些未知的錯誤,如果需要POI團隊修複,周期不确定。
(二)JXL
它是純javaAPI,在跨平台上表現的非常完美,代碼可以再windows或者Linux上運作而無需重新編寫,但是它也存在着許多缺點。
1、效率低,格式支援比POI還少。
2、支援Excel 95-2000的所有版本,但是excel2007以後的版本暫時不支援。
.
(三)EasyExcel(推薦使用)
阿裡巴巴出的産品,相信看到這裡很多人應該更有信心(畢竟阿裡出的東西很是很有品質保障滴)。它是一個基于Java的簡單、省記憶體的讀寫Excel的開源項目。在盡可能節約記憶體的情況下支援讀寫百M的Excel,選擇使用它有以下原因:
1、開源,代碼放在github上,有問題随時issue
2、解決了POI解析excel非常耗費記憶體的問題,它是通過磁盤存儲,一行一傳回,最大程度解決了記憶體占用大的問題。
3、社群活躍度量大,網上的相關文檔也比較多。
(四)POI解析模式和EasyExcel解析模型圖
![]()
操作Excel,除了使用POI你還會其他的? ![]()
操作Excel,除了使用POI你還會其他的?
三: 常用API介紹 |
三: 常用API介紹
(一) 螢幕(不能被Spring容器管理,每次讀取Excel都需要新new一個,如果需要使用Spring容器對象,則通過構造函數傳入):
由于預設一行行的讀取excel,是以需要建立excel一行一行的回調監聽器(這個是必須實作的,是以我們要相容所有的對象,監聽器的泛型使用Object類型)
![]()
操作Excel,除了使用POI你還會其他的? (二) 讀Excel:
1、EasyExcel.read(…) —》它有三個重載的方法
2、sheet() --》指定讀取的sheet,doRead --》執行讀取資料操作![]()
操作Excel,除了使用POI你還會其他的? 3、ExcelReader.readAll() --》 執行讀取Excel檔案中的所有sheet![]()
操作Excel,除了使用POI你還會其他的? ![]()
操作Excel,除了使用POI你還會其他的? 4、ExcelReader執行個體.finish() –》完成讀取操作,并關閉流(一定要注意關閉流,因為easyExcel是使用磁盤的方式進行資料解析,是以解析過程中會建立臨時檔案,如果不關閉,最後可能會導緻磁盤崩潰)
(三) 寫Excel:
1、EasyExcel.write(…) —》它有六個重載的方法
2、writeSheet() —》向excel檔案中的sheet寫入資料![]()
操作Excel,除了使用POI你還會其他的? ![]()
操作Excel,除了使用POI你還會其他的? 3、ExcelWriter.write(…) —》插入sheet到excel檔案中,這樣就完成了資料寫入,實際上就是嵌套一樣,現将資料寫入到sheet,再将 sheet插入到excel中
4、ExcelWriter執行個體.finish() --》完成寫入操作,并關閉流(一定要注意關閉流,因為easyExcel是使用磁盤的方式進行資料解析,是以解析過程中會建立臨時檔案,如果不關閉,最後可能會導緻磁盤崩潰)
(四) 常用注解
1、@ExcelProperty: 作用在excel表資料對應的JAVA實體上,有以下屬性:
(1) : value – 指定導出時該字段對應的标題名稱,或者是讀取時比對excel表格中表頭的名稱,符合則将表頭中對應的資料填充到此處,如果這個名稱存在多個,隻能讀取到一個。
(2) : index – 指定該字段和excel檔案的哪一列對應,預設是0,不推薦和value屬性同時指定,如果需要制定,那麼value的值最好指定為導出資料對應表頭的标題名,index的值則指定為讀取excel檔案時該字段屬性對應的列的位置。
(3) : converter屬性則是指定對應的轉換器,可以自己書寫一個轉換器,在讀取資料的時候進行對資料的格式化,如:給每一列資料都加上自己自定義的東西
2、@ExcelIgnoreUnannotated:預設情況下Java類中的所有屬性都添加讀寫,在類上面加入@ExcelIgnoreUnannotated注解,加入這個注解後隻有加了@ExcelProperty才會參與讀寫。
3、@ExcelIgnore: 被标注的屬性不參加Excel的讀寫,相當于直接省略。
四: 測試 |
四: 測試
(一):添加依賴
// easyExcel坐标
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.7</version>
</dependency>
(二): JAVA映射實體
package com.elvis.easyexcel.model;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Demo implements Serializable {
private static final long serialVersionUID = -920481620956257604L;
@ExcelIgnore
@ExcelProperty(value = "姓名", index = 0)
private String stringType;
@ExcelProperty(value = "姓名2", index = 1)
private Integer integerType;
// 這裡使用String類型接收才能格式化,如果使用Date類型則無法格式化
@ExcelProperty(value = "姓名3", index = 2)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private String dateType;
@ExcelProperty(value = "姓名4", index = 3)
private Double doubleType;
@ExcelProperty(value = "姓名5", index = 4)
private Long longType;
@ExcelProperty(value = "姓名6", index = 5)
private Float floatType;
@ExcelProperty(value = "姓名7", index = 6)
private Boolean booleanType;
@ExcelProperty(value = "姓名8", index = 7)
private Short shortType;
}
(三): 添加監聽器
package com.elvis.easyexcel.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelDataConvertException;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Slf4j
public class ObjectListener extends AnalysisEventListener<Object> {
// 讀取到的資料
private List<Object> readData = new ArrayList<>();
/**
* 解析資料進入的方法
* @param o 本次讀到的資料
* @param analysisContext
*/
@Override
public void invoke(Object o, AnalysisContext analysisContext) {
JSONObject jsonObject = new JSONObject(o);
log.info("讀取到的資料:{}", jsonObject.toString());
if(Objects.nonNull(o)){
readData.add(o);
}
}
/**
* 所有資料解析完成了 都會來調用
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
log.info("所有資料解析完成了 都會來調用");
}
/**
* 在轉換異常 擷取其他異常下會調用本接口。抛出異常則停止讀取。如果這裡不抛出異常則 繼續讀取下一行。
* 如果你的程式在讀取解析時即使有異常也不想後面的解析失敗的,在此處打出解析錯誤日志即可
* 如果你的程式隻有解析過程出錯就解析解析的話,這在此處手動抛出異常即可
* @param exception
* @param context
* @throws Exception
*/
@Override
public void onException(Exception exception, AnalysisContext context) {
log.error("解析失敗,但是繼續解析下一行:{}", exception.getMessage());
// 如果是某一個單元格的轉換異常 能擷取到具體行号
// 如果要擷取頭的資訊 配合invokeHeadMap使用
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
log.error("第{}行,第{}列解析異常", excelDataConvertException.getRowIndex(),
excelDataConvertException.getColumnIndex());
}
}
// 回報解析完成的資料
public List<Object> getReadData(){
return readData;
}
}
(四): 書寫工具類(這個工具類可以直接使用,如果有需要的,直接複制就可以)
package com.elvis.easyexcel.utils;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.elvis.easyexcel.listener.ObjectListener;
import com.elvis.easyexcel.model.Demo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.List;
@Slf4j
public class EasyExcelUtils {
/**
* @param in 讀取的檔案流
* @param model 與excel檔案資料對應的實體
* @param type 通用的資料讀取解析監聽器
* @return
*/
public static List<Object> readExcelFile(InputStream in, Object model, ObjectListener type) {
ExcelReader reader = null;
try {
reader = EasyExcel.read(in, model.getClass(), type).build();
reader.readAll();
} catch (Exception e) {
log.error("讀取excel檔案錯誤:" + e.getMessage());
return null;
} finally {
// 關閉流,讀的時候會建立臨時檔案,不關閉到時磁盤會崩的
if (reader != null) {
reader.finish();
}
}
return type.getReadData();
}
/**
* 儲存資料到excel檔案
*
* @param data 資料(支援多個sheet寫入,根據資料的個數寫入對應個sheet,預設多個sheet寫入的資料是同一個實體的)
* @param savePath 儲存的路徑
* @return 是否儲存成功
*/
public static Boolean writeExcelFileWithCommonEntity(List<List<Object>> data, String savePath) {
if (CollectionUtils.isNotEmpty(data)) {
ExcelWriter excelWriter = null;
// 輸出流放到try的小括号中,方法結束時會自動關閉流,這個是jdk1.8的新特性,對于經常忘記關流的小夥伴很友好哦
try {
// 擷取到操作寫入excel的操作對象,第二個參數是導出的excel檔案的标題名對應的實體
// 擷取寫入資料中的第一個元素的類類型
excelWriter = EasyExcel.write(savePath).build();
// 設定每個sheet的名稱
for (List<Object> objectList : data) {
Object item = objectList.get(0);
WriteSheet writeSheet = EasyExcel.writerSheet(1, "模闆").head(item.getClass()).build();
excelWriter.write(objectList, writeSheet);
}
} catch (Exception e) {
log.error("儲存資料到excel錯誤:{}", e.getMessage());
return false;
} finally {
if (excelWriter != null) {
excelWriter.finish();
}
}
} else {
return false;
}
return true;
}
}
(五): 測試demo
package com.elvis;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.elvis.easyexcel.listener.ObjectListener;
import com.elvis.easyexcel.model.Demo;
import com.elvis.easyexcel.utils.EasyExcelUtils;
import org.json.JSONArray;
import org.junit.Test;
import javax.jws.Oneway;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class demo {
@Test
public void demo1() throws Exception{
// 讀取檔案
System.out.println("開始讀取檔案------------------------------------");
String fileName = "E:\\newpath\\excelutils\\build\\classes\\小明.xlsx";
InputStream in = new FileInputStream(new File(fileName));
List<Object> objects = EasyExcelUtils.readExcelFile(in, new Demo(), new ObjectListener());
JSONArray array = new JSONArray(objects);
System.out.println(array);
System.out.println("--------------------------------------------------------------");
System.out.println("開始儲存檔案------------------------------------");
String savePath = "E:\\newpath\\excelutils\\build\\classes\\儲存檔案測試.xlsx";
List<List<Object>> data = new ArrayList<>();
List<Object> item = new ArrayList<>();
Demo abc = new Demo("abc",12,"2020-12-12 19:10:10",12.2,12l,12f,false,Short.parseShort("12"));
item.add(abc);
data.add(item); EasyExcelUtils.writeExcelFileWithCommonEntity(data,savePath);
}
}
(六): 效果展示
五: 總結 |
五: 總結
通過親自測試發現EasyExcel的API很簡潔,使用也很容易上手,而且速度也很快(在公司裡使用了20多萬資料測試,如果使用多線程的話,幾秒就可以實作導出,非常的快,如果實作單線程的話,也隻需要十秒左右),是以,快動手學起來吧,這個架構現在越來越流行了,面試隻要涉及到導出的,基本不是POI的就是EasyExcel。
由于篇幅限制,是以本文講解的也隻是EasyExcel常使用到的知識,其實它還有許多東西值的我們去研究,更加詳細的請到EasyExcel官方手冊進行檢視,非常感謝你閱讀本文,如果有什麼疑問或者建議,歡迎在文章下方留言或者私信我,如果你覺的文字對你有幫助,請給我點贊和關注,後面還會書寫更多的文章跟大家分享其他的知識,我已經将代碼上傳到github上,如果你覺的需要看更詳細的代碼,請點選:跳轉(它的代碼在spring_parent項目的spring-easyexcel-demo服務中)