SpringBoot 實(shí)現(xiàn) excel 全自由導(dǎo)入導(dǎo)出充蓝,性能強(qiáng)的離譜隧枫,用起來(lái)還特優(yōu)雅

今天我給大家推薦一款性能更好的 Excel 導(dǎo)入導(dǎo)出工具:EasyExcel,希望對(duì)大家有所幫助谓苟!

easyexcel 是阿里開(kāi)源的一款 Excel導(dǎo)入導(dǎo)出工具官脓,具有處理速度快、占用內(nèi)存小涝焙、使用方便的特點(diǎn)卑笨,底層邏輯也是基于 apache poi 進(jìn)行二次開(kāi)發(fā)的,目前的應(yīng)用也是非常廣仑撞!


相比 EasyPoi赤兴,EasyExcel 的處理數(shù)據(jù)性能非常高,讀取 75M (46W行25列) 的Excel隧哮,僅需使用 64M 內(nèi)存桶良,耗時(shí) 20s,極速模式還可以更快沮翔!


廢話也不多說(shuō)了陨帆,下面直奔主題!

二、實(shí)踐

在 SpringBoot 項(xiàng)目中集成 EasyExcel 其實(shí)非常簡(jiǎn)單疲牵,僅需一個(gè)依賴(lài)即可岸浑。

<!--EasyExcel相關(guān)依賴(lài)--><dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.0.5</version></dependency>

EasyExcel 的導(dǎo)出導(dǎo)入支持兩種方式進(jìn)行處理

第一種是通過(guò)實(shí)體類(lèi)注解方式來(lái)生成文件和反解析文件數(shù)據(jù)映射成對(duì)象

第二種是通過(guò)動(dòng)態(tài)參數(shù)化生成文件和反解析文件數(shù)據(jù)

下面我們以用戶信息的導(dǎo)出導(dǎo)入為例,分別介紹兩種處理方式瑰步。

簡(jiǎn)單導(dǎo)出

首先矢洲,我們只需要?jiǎng)?chuàng)建一個(gè)UserEntity用戶實(shí)體類(lèi),然后添加對(duì)應(yīng)的注解字段即可缩焦,示例代碼如下:

public class UserWriteEntity { @ExcelProperty(value = "姓名") private String name; @ExcelProperty(value = "年齡") private int age; @DateTimeFormat("yyyy-MM-dd HH:mm:ss") @ExcelProperty(value = "操作時(shí)間") private Date time; //set读虏、get...}

然后,使用 EasyExcel 提供的EasyExcel工具類(lèi)袁滥,即可實(shí)現(xiàn)文件的導(dǎo)出盖桥。

public static void main(String[] args) throws FileNotFoundException { List<UserWriteEntity> dataList = new ArrayList<>(); for (int i = 0; i < 10; i++) { UserWriteEntity userEntity = new UserWriteEntity(); userEntity.setName("張三" + i); userEntity.setAge(20 + i); userEntity.setTime(new Date(System.currentTimeMillis() + i)); dataList.add(userEntity); } //定義文件輸出位置 FileOutputStream outputStream = new FileOutputStream(new File("/Users/panzhi/Documents/easyexcel-export-user1.xlsx")); EasyExcel.write(outputStream, UserWriteEntity.class).sheet("用戶信息").doWrite(dataList);}

運(yùn)行程序,打開(kāi)文件內(nèi)容結(jié)果题翻!


簡(jiǎn)單導(dǎo)入

這種簡(jiǎn)單固定表頭的 Excel 文件揩徊,如果想要讀取文件數(shù)據(jù),操作也很簡(jiǎn)單嵌赠。

以上面的導(dǎo)出文件為例塑荒,使用 EasyExcel 提供的EasyExcel工具類(lèi),即可來(lái)實(shí)現(xiàn)文件內(nèi)容數(shù)據(jù)的快速讀取姜挺,示例代碼如下:

首先創(chuàng)建讀取實(shí)體類(lèi)

/** * 讀取實(shí)體類(lèi) */public class UserReadEntity { @ExcelProperty(value = "姓名") private String name; /** * 強(qiáng)制讀取第三個(gè) 這里不建議 index 和 name 同時(shí)用齿税,要么一個(gè)對(duì)象只用index,要么一個(gè)對(duì)象只用name去匹配 */ @ExcelProperty(index = 1) private int age; @DateTimeFormat("yyyy-MM-dd HH:mm:ss") @ExcelProperty(value = "操作時(shí)間") private Date time; //set炊豪、get...}

然后讀取文件數(shù)據(jù)凌箕,并封裝到對(duì)象里面

public static void main(String[] args) throws FileNotFoundException { //同步讀取文件內(nèi)容 FileInputStream inputStream = new FileInputStream(new File("/Users/panzhi/Documents/easyexcel-user1.xls")); List<UserReadEntity> list = EasyExcel.read(inputStream).head(UserReadEntity.class).sheet().doReadSync(); System.out.println(JSONArray.toJSONString(list));}

運(yùn)行程序,輸出結(jié)果如下:

[{"age":20,"name":"張三0","time":1616920360000},{"age":21,"name":"張三1","time":1616920360000},{"age":22,"name":"張三2","time":1616920360000},{"age":23,"name":"張三3","time":1616920360000},{"age":24,"name":"張三4","time":1616920360000},{"age":25,"name":"張三5","time":1616920360000},{"age":26,"name":"張三6","time":1616920360000},{"age":27,"name":"張三7","time":1616920360000},{"age":28,"name":"張三8","time":1616920360000},{"age":29,"name":"張三9","time":1616920360000}]

動(dòng)態(tài)自由導(dǎo)出導(dǎo)入

在實(shí)際使用開(kāi)發(fā)中词渤,我們不可能每來(lái)一個(gè) excel 導(dǎo)入導(dǎo)出需求牵舱,就編寫(xiě)一個(gè)實(shí)體類(lèi),很多業(yè)務(wù)需求需要根據(jù)不同的字段來(lái)動(dòng)態(tài)導(dǎo)入導(dǎo)出缺虐,沒(méi)辦法基于實(shí)體類(lèi)注解的方式來(lái)讀取文件或者寫(xiě)入文件芜壁。

因此,基于EasyExcel提供的動(dòng)態(tài)參數(shù)化生成文件和動(dòng)態(tài)監(jiān)聽(tīng)器讀取文件方法志笼,我們可以單獨(dú)封裝一套動(dòng)態(tài)導(dǎo)出導(dǎo)出工具類(lèi)沿盅,省的我們每次都需要重新編寫(xiě)大量重復(fù)工作,以下就是小編我在實(shí)際使用過(guò)程纫溃,封裝出來(lái)的工具類(lèi)腰涧,在此分享給大家!

首先紊浩,我們可以編寫(xiě)一個(gè)動(dòng)態(tài)導(dǎo)出工具類(lèi)

public class DynamicEasyExcelExportUtils { private static final Logger log = LoggerFactory.getLogger(DynamicEasyExcelExportUtils.class); private static final String DEFAULT_SHEET_NAME = "sheet1"; /** * 動(dòng)態(tài)生成導(dǎo)出模版(單表頭) * @param headColumns 列名稱(chēng) * @return excel文件流 */ public static byte[] exportTemplateExcelFile(List<String> headColumns){ List<List<String>> excelHead = Lists.newArrayList(); headColumns.forEach(columnName -> { excelHead.add(Lists.newArrayList(columnName)); }); byte[] stream = createExcelFile(excelHead, new ArrayList<>()); return stream; } /** * 動(dòng)態(tài)生成模版(復(fù)雜表頭) * @param excelHead 列名稱(chēng) * @return */ public static byte[] exportTemplateExcelFileCustomHead(List<List<String>> excelHead){ byte[] stream = createExcelFile(excelHead, new ArrayList<>()); return stream; } /** * 動(dòng)態(tài)導(dǎo)出文件(通過(guò)map方式計(jì)算) * @param headColumnMap 有序列頭部 * @param dataList 數(shù)據(jù)體 * @return */ public static byte[] exportExcelFile(LinkedHashMap<String, String> headColumnMap, List<Map<String, Object>> dataList){ //獲取列名稱(chēng) List<List<String>> excelHead = new ArrayList<>(); if(MapUtils.isNotEmpty(headColumnMap)){ //key為匹配符窖铡,value為列名疗锐,如果多級(jí)列名用逗號(hào)隔開(kāi) headColumnMap.entrySet().forEach(entry -> { excelHead.add(Lists.newArrayList(entry.getValue().split(","))); }); } List<List<Object>> excelRows = new ArrayList<>(); if(MapUtils.isNotEmpty(headColumnMap) && CollectionUtils.isNotEmpty(dataList)){ for (Map<String, Object> dataMap : dataList) { List<Object> rows = new ArrayList<>(); headColumnMap.entrySet().forEach(headColumnEntry -> { if(dataMap.containsKey(headColumnEntry.getKey())){ Object data = dataMap.get(headColumnEntry.getKey()); rows.add(data); } }); excelRows.add(rows); } } byte[] stream = createExcelFile(excelHead, excelRows); return stream; } /** * 生成文件(自定義頭部排列) * @param rowHeads * @param excelRows * @return */ public static byte[] customerExportExcelFile(List<List<String>> rowHeads, List<List<Object>> excelRows){ //將行頭部轉(zhuǎn)成easyexcel能識(shí)別的部分 List<List<String>> excelHead = transferHead(rowHeads); return createExcelFile(excelHead, excelRows); } /** * 生成文件 * @param excelHead * @param excelRows * @return */ private static byte[] createExcelFile(List<List<String>> excelHead, List<List<Object>> excelRows){ try { if(CollectionUtils.isNotEmpty(excelHead)){ ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); EasyExcel.write(outputStream).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .head(excelHead) .sheet(DEFAULT_SHEET_NAME) .doWrite(excelRows); return outputStream.toByteArray(); } } catch (Exception e) { log.error("動(dòng)態(tài)生成excel文件失敗,headColumns:" + JSONArray.toJSONString(excelHead) + "费彼,excelRows:" + JSONArray.toJSONString(excelRows), e); } return null; } /** * 將行頭部轉(zhuǎn)成easyexcel能識(shí)別的部分 * @param rowHeads * @return */ public static List<List<String>> transferHead(List<List<String>> rowHeads){ //將頭部列進(jìn)行反轉(zhuǎn) List<List<String>> realHead = new ArrayList<>(); if(CollectionUtils.isNotEmpty(rowHeads)){ Map<Integer, List<String>> cellMap = new LinkedHashMap<>(); //遍歷行 for (List<String> cells : rowHeads) { //遍歷列 for (int i = 0; i < cells.size(); i++) { if(cellMap.containsKey(i)){ cellMap.get(i).add(cells.get(i)); } else { cellMap.put(i, Lists.newArrayList(cells.get(i))); } } } //將列一行一行加入realHead cellMap.entrySet().forEach(item -> realHead.add(item.getValue())); } return realHead; } /** * 導(dǎo)出文件測(cè)試 * @param args * @throws IOException */ public static void main(String[] args) throws IOException { //導(dǎo)出包含數(shù)據(jù)內(nèi)容的文件(方式一) LinkedHashMap<String, String> headColumnMap = Maps.newLinkedHashMap(); headColumnMap.put("className","班級(jí)"); headColumnMap.put("name","學(xué)生信息,姓名"); headColumnMap.put("sex","學(xué)生信息,性別"); List<Map<String, Object>> dataList = new ArrayList<>(); for (int i = 0; i < 5; i++) { Map<String, Object> dataMap = Maps.newHashMap(); dataMap.put("className", "一年級(jí)"); dataMap.put("name", "張三" + i); dataMap.put("sex", "男"); dataList.add(dataMap); } byte[] stream1 = exportExcelFile(headColumnMap, dataList); FileOutputStream outputStream1 = new FileOutputStream(new File("/Users/panzhi/Documents/easyexcel-export-user5.xlsx")); outputStream1.write(stream1); outputStream1.close(); //導(dǎo)出包含數(shù)據(jù)內(nèi)容的文件(方式二) //頭部滑臊,第一層 List<String> head1 = new ArrayList<>(); head1.add("第一行頭部列1"); head1.add("第一行頭部列1"); head1.add("第一行頭部列1"); head1.add("第一行頭部列1"); //頭部,第二層 List<String> head2 = new ArrayList<>(); head2.add("第二行頭部列1"); head2.add("第二行頭部列1"); head2.add("第二行頭部列2"); head2.add("第二行頭部列2"); //頭部箍铲,第三層 List<String> head3 = new ArrayList<>(); head3.add("第三行頭部列1"); head3.add("第三行頭部列2"); head3.add("第三行頭部列3"); head3.add("第三行頭部列4"); //封裝頭部 List<List<String>> allHead = new ArrayList<>(); allHead.add(head1); allHead.add(head2); allHead.add(head3); //封裝數(shù)據(jù)體 //第一行數(shù)據(jù) List<Object> data1 = Lists.newArrayList(1,1,1,1); //第二行數(shù)據(jù) List<Object> data2 = Lists.newArrayList(2,2,2,2); List<List<Object>> allData = Lists.newArrayList(data1, data2); byte[] stream2 = customerExportExcelFile(allHead, allData); FileOutputStream outputStream2 = new FileOutputStream(new File("/Users/panzhi/Documents/easyexcel-export-user6.xlsx")); outputStream2.write(stream2); outputStream2.close(); }}

然后雇卷,編寫(xiě)一個(gè)動(dòng)態(tài)導(dǎo)入工具類(lèi)

/** * 創(chuàng)建一個(gè)文件讀取監(jiān)聽(tīng)器 */public class DynamicEasyExcelListener extends AnalysisEventListener<Map<Integer, String>> { private static final Logger LOGGER = LoggerFactory.getLogger(UserDataListener.class); /** * 表頭數(shù)據(jù)(存儲(chǔ)所有的表頭數(shù)據(jù)) */ private List<Map<Integer, String>> headList = new ArrayList<>(); /** * 數(shù)據(jù)體 */ private List<Map<Integer, String>> dataList = new ArrayList<>(); /** * 這里會(huì)一行行的返回頭 * * @param headMap * @param context */ @Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { LOGGER.info("解析到一條頭數(shù)據(jù):{}", JSON.toJSONString(headMap)); //存儲(chǔ)全部表頭數(shù)據(jù) headList.add(headMap); } /** * 這個(gè)每一條數(shù)據(jù)解析都會(huì)來(lái)調(diào)用 * * @param data * one row value. Is is same as {@link AnalysisContext#readRowHolder()} * @param context */ @Override public void invoke(Map<Integer, String> data, AnalysisContext context) { LOGGER.info("解析到一條數(shù)據(jù):{}", JSON.toJSONString(data)); dataList.add(data); } /** * 所有數(shù)據(jù)解析完成了 都會(huì)來(lái)調(diào)用 * * @param context */ @Override public void doAfterAllAnalysed(AnalysisContext context) { // 這里也要保存數(shù)據(jù),確保最后遺留的數(shù)據(jù)也存儲(chǔ)到數(shù)據(jù)庫(kù) LOGGER.info("所有數(shù)據(jù)解析完成颠猴!"); } public List<Map<Integer, String>> getHeadList() { return headList; } public List<Map<Integer, String>> getDataList() { return dataList; }}

動(dòng)態(tài)導(dǎo)入工具類(lèi)

/** * 編寫(xiě)導(dǎo)入工具類(lèi) */public class DynamicEasyExcelImportUtils { /** * 動(dòng)態(tài)獲取全部列和數(shù)據(jù)體关划,默認(rèn)從第一行開(kāi)始解析數(shù)據(jù) * @param stream * @return */ public static List<Map<String,String>> parseExcelToView(byte[] stream) { return parseExcelToView(stream, 1); } /** * 動(dòng)態(tài)獲取全部列和數(shù)據(jù)體 * @param stream excel文件流 * @param parseRowNumber 指定讀取行 * @return */ public static List<Map<String,String>> parseExcelToView(byte[] stream, Integer parseRowNumber) { DynamicEasyExcelListener readListener = new DynamicEasyExcelListener(); EasyExcelFactory.read(new ByteArrayInputStream(stream)).registerReadListener(readListener).headRowNumber(parseRowNumber).sheet(0).doRead(); List<Map<Integer, String>> headList = readListener.getHeadList(); if(CollectionUtils.isEmpty(headList)){ throw new RuntimeException("Excel未包含表頭"); } List<Map<Integer, String>> dataList = readListener.getDataList(); if(CollectionUtils.isEmpty(dataList)){ throw new RuntimeException("Excel未包含數(shù)據(jù)"); } //獲取頭部,取最后一次解析的列頭數(shù)據(jù) Map<Integer, String> excelHeadIdxNameMap = headList.get(headList.size() -1); //封裝數(shù)據(jù)體 List<Map<String,String>> excelDataList = Lists.newArrayList(); for (Map<Integer, String> dataRow : dataList) { Map<String,String> rowData = new LinkedHashMap<>(); excelHeadIdxNameMap.entrySet().forEach(columnHead -> { rowData.put(columnHead.getValue(), dataRow.get(columnHead.getKey())); }); excelDataList.add(rowData); } return excelDataList; } /** * 文件導(dǎo)入測(cè)試 * @param args * @throws IOException */ public static void main(String[] args) throws IOException { FileInputStream inputStream = new FileInputStream(new File("/Users/panzhi/Documents/easyexcel-export-user5.xlsx")); byte[] stream = IoUtils.toByteArray(inputStream); List<Map<String,String>> dataList = parseExcelToView(stream, 2); System.out.println(JSONArray.toJSONString(dataList)); inputStream.close(); }}

為了方便后續(xù)的操作流程,在解析數(shù)據(jù)的時(shí)候翘瓮,會(huì)將列名作為key贮折!

三、小結(jié)

在實(shí)際的業(yè)務(wù)開(kāi)發(fā)過(guò)程中资盅,根據(jù)參數(shù)動(dòng)態(tài)實(shí)現(xiàn) Excel 的導(dǎo)出導(dǎo)入還是非常廣的调榄。

當(dāng)然,EasyExcel 的功能還不只上面介紹的那些內(nèi)容呵扛,還有基于模版進(jìn)行 excel的填充每庆,web 端 restful 的導(dǎo)出導(dǎo)出,使用方法大致都差不多择份,更多的功能扣孟,會(huì)在后續(xù)的文章中再次介紹烫堤,如果有描述不對(duì)的地方荣赶,歡迎網(wǎng)友批評(píng)吐槽!

更多關(guān)于Java技術(shù)問(wèn)題鸽斟,歡迎大家關(guān)注我的微信:https://docs.qq.com/doc/DQ2Z0eE1aUmlITnNz? 備注【666】有我準(zhǔn)備的一線程序必備計(jì)算機(jī)書(shū)籍拔创,JAVA 課件,源碼富蓄,安裝包等資料希望可以幫助大家提升技術(shù)和能力

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末剩燥,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子立倍,更是在濱河造成了極大的恐慌灭红,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件口注,死亡現(xiàn)場(chǎng)離奇詭異变擒,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)寝志,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)娇斑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)策添,“玉大人,你說(shuō)我怎么就攤上這事毫缆∥ㄖ瘢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵苦丁,是天一觀的道長(zhǎng)浸颓。 經(jīng)常有香客問(wèn)我,道長(zhǎng)旺拉,這世上最難降的妖魔是什么猾愿? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮账阻,結(jié)果婚禮上蒂秘,老公的妹妹穿的比我還像新娘。我一直安慰自己淘太,他們只是感情好姻僧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著蒲牧,像睡著了一般撇贺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上冰抢,一...
    開(kāi)封第一講書(shū)人閱讀 49,071評(píng)論 1 285
  • 那天松嘶,我揣著相機(jī)與錄音,去河邊找鬼挎扰。 笑死翠订,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的遵倦。 我是一名探鬼主播尽超,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼梧躺!你這毒婦竟也來(lái)了似谁?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤掠哥,失蹤者是張志新(化名)和其女友劉穎巩踏,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體续搀,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡塞琼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了目代。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屈梁。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嗤练,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出在讶,到底是詐尸還是另有隱情煞抬,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布构哺,位于F島的核電站革答,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏曙强。R本人自食惡果不足惜残拐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望碟嘴。 院中可真熱鬧溪食,春花似錦、人聲如沸娜扇。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)雀瓢。三九已至枢析,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間刃麸,已是汗流浹背醒叁。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留泊业,地道東北人把沼。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像脱吱,于是被迫代替她去往敵國(guó)和親智政。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容