SpringBoot 實(shí)現(xiàn) Excel 導(dǎo)入導(dǎo)出,性能爆表,用起來夠優(yōu)雅!
操作Excel實(shí)現(xiàn)導(dǎo)入導(dǎo)出是個(gè)非常常見的需求,之前介紹了一款非常好用的工具EasyPoi 。有讀者提出在數(shù)據(jù)量大的情況下,EasyPoi占用內(nèi)存大,性能不夠好。今天給大家推薦一款性能更好的Excel導(dǎo)入導(dǎo)出工具
EasyExcel,希望對(duì)大家有所幫助!
EasyExcel簡介

EasyExcel是一款阿里開源的Excel導(dǎo)入導(dǎo)出工具,具有處理快速、占用內(nèi)存小、使用方便的特點(diǎn),在Github上已有22k+Star,可見其非常流行。
EasyExcel讀取75M(46W行25列)的Excel,僅需使用64M內(nèi)存,耗時(shí)20s,極速模式還可以更快!

集成
在SpringBoot中集成EasyExcel非常簡單,僅需一個(gè)依賴即可。
<dependency>
????<groupId>com.alibabagroupId>
????<artifactId>easyexcelartifactId>
????<version>3.0.5version>
dependency>
使用
EasyExcel和EasyPoi的使用非常類似,都是通過注解來控制導(dǎo)入導(dǎo)出。接下來我們以會(huì)員信息和訂單信息的導(dǎo)入導(dǎo)出為例,分別實(shí)現(xiàn)下簡單的單表導(dǎo)出和具有一對(duì)多關(guān)系的復(fù)雜導(dǎo)出。
簡單導(dǎo)出
我們以會(huì)員信息的導(dǎo)出為例,來體驗(yàn)下EasyExcel的導(dǎo)出功能。
首先創(chuàng)建一個(gè)會(huì)員對(duì)象 Member,封裝會(huì)員信息,這里使用了EasyExcel的注解;
/**
?*?購物會(huì)員
?*?Created?by?macro?on?2021/10/12.
?*/
@Data
@EqualsAndHashCode(callSuper?=?false)
public?class?Member?{
????@ExcelProperty("ID")
????@ColumnWidth(10)
????private?Long?id;
????@ExcelProperty("用戶名")
????@ColumnWidth(20)
????private?String?username;
????@ExcelIgnore
????private?String?password;
????@ExcelProperty("昵稱")
????@ColumnWidth(20)
????private?String?nickname;
????@ExcelProperty("出生日期")
????@ColumnWidth(20)
????@DateTimeFormat("yyyy-MM-dd")
????private?Date?birthday;
????@ExcelProperty("手機(jī)號(hào)")
????@ColumnWidth(20)
????private?String?phone;
????@ExcelIgnore
????private?String?icon;
????@ExcelProperty(value?=?"性別",?converter?=?GenderConverter.class)
????@ColumnWidth(10)
????private?Integer?gender;
}
上面代碼使用到了EasyExcel的核心注解,我們分別來了解下:
@ExcelProperty:核心注解, value屬性可用來設(shè)置表頭名稱,converter屬性可以用來設(shè)置類型轉(zhuǎn)換器;@ColumnWidth:用于設(shè)置表格列的寬度; @DateTimeFormat:用于設(shè)置日期轉(zhuǎn)換格式。 在EasyExcel中,如果你想實(shí)現(xiàn)枚舉類型到字符串的轉(zhuǎn)換(比如gender屬性中,
0->男,1->女),需要自定義轉(zhuǎn)換器,下面為自定義的GenderConverter代碼實(shí)現(xiàn);
/**
?*?excel性別轉(zhuǎn)換器
?*?Created?by?macro?on?2021/12/29.
?*/
public?class?GenderConverter?implements?Converter<Integer>?{
????@Override
????public?Class>?supportJavaTypeKey()?{
????????//對(duì)象屬性類型
????????return?Integer.class;
????}
????@Override
????public?CellDataTypeEnum?supportExcelTypeKey()?{
????????//CellData屬性類型
????????return?CellDataTypeEnum.STRING;
????}
????@Override
????public?Integer?convertToJavaData(ReadConverterContext>?context)?throws?Exception?{
????????//CellData轉(zhuǎn)對(duì)象屬性
????????String?cellStr?=?context.getReadCellData().getStringValue();
????????if?(StrUtil.isEmpty(cellStr))?return?null;
????????if?("男".equals(cellStr))?{
????????????return?0;
????????}?else?if?("女".equals(cellStr))?{
????????????return?1;
????????}?else?{
????????????return?null;
????????}
????}
????@Override
????public?WriteCellData>?convertToExcelData(WriteConverterContext?context)?throws?Exception?{
????????//對(duì)象屬性轉(zhuǎn)CellData
????????Integer?cellValue?=?context.getValue();
????????if?(cellValue?==?null)?{
????????????return?new?WriteCellData<>("");
????????}
????????if?(cellValue?==?0)?{
????????????return?new?WriteCellData<>("男");
????????}?else?if?(cellValue?==?1)?{
????????????return?new?WriteCellData<>("女");
????????}?else?{
????????????return?new?WriteCellData<>("");
????????}
????}
}
接下來我們?cè)贑ontroller中添加一個(gè)接口,用于導(dǎo)出會(huì)員列表到Excel,還需給響應(yīng)頭設(shè)置下載excel的屬性,具體代碼如下;
/**
?*?EasyExcel導(dǎo)入導(dǎo)出測試Controller
?*?Created?by?macro?on?2021/10/12.
?*/
@Controller
@Api(tags?=?"EasyExcelController",?description?=?"EasyExcel導(dǎo)入導(dǎo)出測試")
@RequestMapping("/easyExcel")
public?class?EasyExcelController?{
????@SneakyThrows(IOException.class)
????@ApiOperation(value?=?"導(dǎo)出會(huì)員列表Excel")
????@RequestMapping(value?=?"/exportMemberList",?method?=?RequestMethod.GET)
????public?void?exportMemberList(HttpServletResponse?response)?{
????????setExcelRespProp(response,?"會(huì)員列表");
????????List?memberList?=?LocalJsonUtil.getListFromJson("json/members.json",?Member.class);
????????EasyExcel.write(response.getOutputStream())
????????????????.head(Member.class)
????????????????.excelType(ExcelTypeEnum.XLSX)
????????????????.sheet("會(huì)員列表")
????????????????.doWrite(memberList);
????}
????
??/**
???*?設(shè)置excel下載響應(yīng)頭屬性
???*/
??private?void?setExcelRespProp(HttpServletResponse?response,?String?rawFileName)?throws?UnsupportedEncodingException?{
????response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
????response.setCharacterEncoding("utf-8");
????String?fileName?=?URLEncoder.encode(rawFileName,?"UTF-8").replaceAll("\\+",?"%20");
????response.setHeader("Content-disposition",?"attachment;filename*=utf-8''"?+?fileName?+?".xlsx");
??}
}
運(yùn)行項(xiàng)目,通過Swagger測試接口,注意在Swagger中訪問接口無法直接下載,需要點(diǎn)擊返回結(jié)果中的 下載按鈕才行,訪問地址:http://localhost:8088/swagger-ui/

下載完成后,查看下文件,一個(gè)標(biāo)準(zhǔn)的Excel文件已經(jīng)被導(dǎo)出了。

簡單導(dǎo)入
接下來我們以會(huì)員信息的導(dǎo)入為例,來體驗(yàn)下EasyExcel的導(dǎo)入功能。
在Controller中添加會(huì)員信息導(dǎo)入的接口,這里需要注意的是使用 @RequestPart注解修飾文件上傳參數(shù),否則在Swagger中就沒法顯示上傳按鈕了;
/**
?*?EasyExcel導(dǎo)入導(dǎo)出測試Controller
?*?Created?by?macro?on?2021/10/12.
?*/
@Controller
@Api(tags?=?"EasyExcelController",?description?=?"EasyExcel導(dǎo)入導(dǎo)出測試")
@RequestMapping("/easyExcel")
public?class?EasyExcelController?{
????
????@SneakyThrows
????@ApiOperation("從Excel導(dǎo)入會(huì)員列表")
????@RequestMapping(value?=?"/importMemberList",?method?=?RequestMethod.POST)
????@ResponseBody
????public?CommonResult?importMemberList(@RequestPart("file")?MultipartFile?file)?{
????????List?memberList?=?EasyExcel.read(file.getInputStream())
????????????????.head(Member.class)
????????????????.sheet()
????????????????.doReadSync();
????????return?CommonResult.success(memberList);
????}
}
然后在Swagger中測試接口,選擇之前導(dǎo)出的Excel文件即可,導(dǎo)入成功后會(huì)返回解析到的數(shù)據(jù)。

復(fù)雜導(dǎo)出
當(dāng)然EasyExcel也可以實(shí)現(xiàn)更加復(fù)雜的導(dǎo)出,比如導(dǎo)出一個(gè)嵌套了商品信息的訂單列表,下面我們來實(shí)現(xiàn)下!
使用EasyPoi實(shí)現(xiàn)
之前我們使用過EasyPoi實(shí)現(xiàn)該功能,由于EasyPoi本來就支持嵌套對(duì)象的導(dǎo)出,直接使用內(nèi)置的@ExcelCollection注解即可實(shí)現(xiàn),非常方便也符合面向?qū)ο蟮乃枷搿?/p>
尋找方案
由于EasyExcel本身并不支持這種一對(duì)多的信息導(dǎo)出,所以我們得自行實(shí)現(xiàn)下,這里分享一個(gè)我平時(shí)常用的
快速查找解決方案的辦法。
我們可以直接從開源項(xiàng)目的issues里面去搜索,比如搜索下一對(duì)多,會(huì)直接找到有無一對(duì)多導(dǎo)出比較優(yōu)雅的方案這個(gè)issue。

從此issue的回復(fù)我們可以發(fā)現(xiàn),項(xiàng)目維護(hù)者建議創(chuàng)建自定義合并策略來實(shí)現(xiàn),有位回復(fù)的老哥已經(jīng)給出了實(shí)現(xiàn)代碼,接下來我們就用這個(gè)方案來實(shí)現(xiàn)下。

解決思路
為什么自定義單元格合并策略能實(shí)現(xiàn)一對(duì)多的列表信息的導(dǎo)出呢?首先我們來看下將嵌套數(shù)據(jù)平鋪,不進(jìn)行合并導(dǎo)出的Excel。

看完之后我們很容易理解解決思路,只要把訂單ID相同的列中需要合并的列給合并了,就可以實(shí)現(xiàn)這種一對(duì)多嵌套信息的導(dǎo)出了。
實(shí)現(xiàn)過程
首先我們得把原來嵌套的訂單商品信息給平鋪了,創(chuàng)建一個(gè)專門的導(dǎo)出對(duì)象 OrderData,包含訂單和商品信息,二級(jí)表頭可以通過設(shè)置@ExcelProperty的value為數(shù)組來實(shí)現(xiàn);
/**
?*?訂單導(dǎo)出
?*?Created?by?macro?on?2021/12/30.
?*/
@Data
@EqualsAndHashCode(callSuper?=?false)
public?class?OrderData?{
????@ExcelProperty(value?=?"訂單ID")
????@ColumnWidth(10)
????@CustomMerge(needMerge?=?true,?isPk?=?true)
????private?String?id;
????@ExcelProperty(value?=?"訂單編碼")
????@ColumnWidth(20)
????@CustomMerge(needMerge?=?true)
????private?String?orderSn;
????@ExcelProperty(value?=?"創(chuàng)建時(shí)間")
????@ColumnWidth(20)
????@DateTimeFormat("yyyy-MM-dd")
????@CustomMerge(needMerge?=?true)
????private?Date?createTime;
????@ExcelProperty(value?=?"收貨地址")
????@CustomMerge(needMerge?=?true)
????@ColumnWidth(20)
????private?String?receiverAddress;
????@ExcelProperty(value?=?{"商品信息",?"商品編碼"})
????@ColumnWidth(20)
????private?String?productSn;
????@ExcelProperty(value?=?{"商品信息",?"商品名稱"})
????@ColumnWidth(20)
????private?String?name;
????@ExcelProperty(value?=?{"商品信息",?"商品標(biāo)題"})
????@ColumnWidth(30)
????private?String?subTitle;
????@ExcelProperty(value?=?{"商品信息",?"品牌名稱"})
????@ColumnWidth(20)
????private?String?brandName;
????@ExcelProperty(value?=?{"商品信息",?"商品價(jià)格"})
????@ColumnWidth(20)
????private?BigDecimal?price;
????@ExcelProperty(value?=?{"商品信息",?"商品數(shù)量"})
????@ColumnWidth(20)
????private?Integer?count;
}
然后將原來嵌套的 Order對(duì)象列表轉(zhuǎn)換為OrderData對(duì)象列表;
/**
?*?EasyExcel導(dǎo)入導(dǎo)出測試Controller
?*?Created?by?macro?on?2021/10/12.
?*/
@Controller
@Api(tags?=?"EasyExcelController",?description?=?"EasyExcel導(dǎo)入導(dǎo)出測試")
@RequestMapping("/easyExcel")
public?class?EasyExcelController?{
????private?List?convert(List?orderList) ? {
????????List?result?=?new?ArrayList<>();
????????for?(Order?order?:?orderList)?{
????????????List?productList?=?order.getProductList();
????????????for?(Product?product?:?productList)?{
????????????????OrderData?orderData?=?new?OrderData();
????????????????BeanUtil.copyProperties(product,orderData);
????????????????BeanUtil.copyProperties(order,orderData);
????????????????result.add(orderData);
????????????}
????????}
????????return?result;
????}
}
再創(chuàng)建一個(gè)自定義注解 CustomMerge,用于標(biāo)記哪些屬性需要合并,哪個(gè)是主鍵;
/**
?*?自定義注解,用于判斷是否需要合并以及合并的主鍵
?*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public?@interface?CustomMerge?{
????/**
?????*?是否需要合并單元格
?????*/
????boolean?needMerge()?default?false;
????/**
?????*?是否是主鍵,即該字段相同的行合并
?????*/
????boolean?isPk()?default?false;
}
再創(chuàng)建自定義單元格合并策略類 CustomMergeStrategy,當(dāng)Excel中兩列主鍵相同時(shí),合并被標(biāo)記需要合并的列;
/**
?*?自定義單元格合并策略
?*/
public?class?CustomMergeStrategy?implements?RowWriteHandler?{
????/**
?????*?主鍵下標(biāo)
?????*/
????private?Integer?pkIndex;
????/**
?????*?需要合并的列的下標(biāo)集合
?????*/
????private?List?needMergeColumnIndex?=?new?ArrayList<>();
????/**
?????*?DTO數(shù)據(jù)類型
?????*/
????private?Class>?elementType;
????public?CustomMergeStrategy(Class>?elementType)?{
????????this.elementType?=?elementType;
????}
????@Override
????public?void?afterRowDispose(WriteSheetHolder?writeSheetHolder,?WriteTableHolder?writeTableHolder,?Row?row,?Integer?relativeRowIndex,?Boolean?isHead)?{
????????//?如果是標(biāo)題,則直接返回
????????if?(isHead)?{
????????????return;
????????}
????????//?獲取當(dāng)前sheet
????????Sheet?sheet?=?writeSheetHolder.getSheet();
????????//?獲取標(biāo)題行
????????Row?titleRow?=?sheet.getRow(0);
????????if?(null?==?pkIndex)?{
????????????this.lazyInit(writeSheetHolder);
????????}
????????//?判斷是否需要和上一行進(jìn)行合并
????????//?不能和標(biāo)題合并,只能數(shù)據(jù)行之間合并
????????if?(row.getRowNum()?<=?1)?{
????????????return;
????????}
????????//?獲取上一行數(shù)據(jù)
????????Row?lastRow?=?sheet.getRow(row.getRowNum()?-?1);
????????//?將本行和上一行是同一類型的數(shù)據(jù)(通過主鍵字段進(jìn)行判斷),則需要合并
????????if?(lastRow.getCell(pkIndex).getStringCellValue().equalsIgnoreCase(row.getCell(pkIndex).getStringCellValue()))?{
????????????for?(Integer?needMerIndex?:?needMergeColumnIndex)?{
????????????????CellRangeAddress?cellRangeAddress?=?new?CellRangeAddress(row.getRowNum()?-?1,?row.getRowNum(),
????????????????????????needMerIndex,?needMerIndex);
????????????????sheet.addMergedRegionUnsafe(cellRangeAddress);
????????????}
????????}
????}
????/**
?????*?初始化主鍵下標(biāo)和需要合并字段的下標(biāo)
?????*/
????private?void?lazyInit(WriteSheetHolder?writeSheetHolder)?{
????????//?獲取當(dāng)前sheet
????????Sheet?sheet?=?writeSheetHolder.getSheet();
????????//?獲取標(biāo)題行
????????Row?titleRow?=?sheet.getRow(0);
????????//?獲取DTO的類型
????????Class>?eleType?=?this.elementType;
????????//?獲取DTO所有的屬性
????????Field[]?fields?=?eleType.getDeclaredFields();
????????//?遍歷所有的字段,因?yàn)槭腔贒TO的字段來構(gòu)建excel,所以字段數(shù)?>=?excel的列數(shù)
????????for?(Field?theField?:?fields)?{
????????????//?獲取@ExcelProperty注解,用于獲取該字段對(duì)應(yīng)在excel中的列的下標(biāo)
????????????ExcelProperty?easyExcelAnno?=?theField.getAnnotation(ExcelProperty.class);
????????????//?為空,則表示該字段不需要導(dǎo)入到excel,直接處理下一個(gè)字段
????????????if?(null?==?easyExcelAnno)?{
????????????????continue;
????????????}
????????????//?獲取自定義的注解,用于合并單元格
????????????CustomMerge?customMerge?=?theField.getAnnotation(CustomMerge.class);
????????????//?沒有@CustomMerge注解的默認(rèn)不合并
????????????if?(null?==?customMerge)?{
????????????????continue;
????????????}
????????????for?(int?index?=?0;?index?????????????????Cell?theCell?=?titleRow.getCell(index);
????????????????//?當(dāng)配置為不需要導(dǎo)出時(shí),返回的為null,這里作一下判斷,防止NPE
????????????????if?(null?==?theCell)?{
????????????????????continue;
????????????????}
????????????????//?將字段和excel的表頭匹配上
????????????????if?(easyExcelAnno.value()[0].equalsIgnoreCase(theCell.getStringCellValue()))?{
????????????????????if?(customMerge.isPk())?{
????????????????????????pkIndex?=?index;
????????????????????}
????????????????????if?(customMerge.needMerge())?{
????????????????????????needMergeColumnIndex.add(index);
????????????????????}
????????????????}
????????????}
????????}
????????//?沒有指定主鍵,則異常
????????if?(null?==?this.pkIndex)?{
????????????throw?new?IllegalStateException("使用@CustomMerge注解必須指定主鍵");
????????}
????}
}
接下來在Controller中添加導(dǎo)出訂單列表的接口,將我們自定義的合并策略 CustomMergeStrategy給注冊(cè)上去;
/**
?*?EasyExcel導(dǎo)入導(dǎo)出測試Controller
?*?Created?by?macro?on?2021/10/12.
?*/
@Controller
@Api(tags?=?"EasyExcelController",?description?=?"EasyExcel導(dǎo)入導(dǎo)出測試")
@RequestMapping("/easyExcel")
public?class?EasyExcelController?{
????
????@SneakyThrows
????@ApiOperation(value?=?"導(dǎo)出訂單列表Excel")
????@RequestMapping(value?=?"/exportOrderList",?method?=?RequestMethod.GET)
????public?void?exportOrderList(HttpServletResponse?response)?{
????????List?orderList?=?getOrderList();
????????List?orderDataList?=?convert(orderList);
????????setExcelRespProp(response,?"訂單列表");
????????EasyExcel.write(response.getOutputStream())
????????????????.head(OrderData.class)
????????????????.registerWriteHandler(new?CustomMergeStrategy(OrderData.class))
????????????????.excelType(ExcelTypeEnum.XLSX)
????????????????.sheet("訂單列表")
????????????????.doWrite(orderDataList);
????}
}
在Swagger中訪問接口測試,導(dǎo)出訂單列表對(duì)應(yīng)Excel;

下載完成后,查看下文件,由于EasyExcel需要自己來實(shí)現(xiàn),對(duì)比之前使用EasyPoi來實(shí)現(xiàn)麻煩了不少。

其他使用
由于EasyExcel的官方文檔介紹的比較簡單,如果你想要更深入地進(jìn)行使用的話,建議大家看下官方Demo。

總結(jié)
體驗(yàn)了一把EasyExcel,使用還是挺方便的,性能也很優(yōu)秀。但是比較常見的一對(duì)多導(dǎo)出實(shí)現(xiàn)比較復(fù)雜,而且功能也不如EasyPoi 強(qiáng)大。如果你的Excel導(dǎo)出數(shù)據(jù)量不大的話,可以使用EasyPoi,如果數(shù)據(jù)量大,比較在意性能的話,還是使用EasyExcel吧。
參考資料
項(xiàng)目地址:https://github.com/alibaba/easyexcel 官方文檔:https://www.yuque.com/easyexcel/doc/easyexcel
項(xiàng)目源碼地址
https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-easyexcel
各種Excel處理歷史文章鏈接
Spring Boot + EasyExcel 導(dǎo)入導(dǎo)出,好用到爆!
史上最全的Excel導(dǎo)入導(dǎo)出(easyexcel版)
Java實(shí)現(xiàn)pdf和Excel的生成及數(shù)據(jù)動(dòng)態(tài)插入、導(dǎo)出
Excel大批量數(shù)據(jù)的導(dǎo)入和導(dǎo)出,如何做優(yōu)化?
10w行級(jí)別數(shù)據(jù)的Excel導(dǎo)入,4版完整優(yōu)化記錄,效果太明顯了!
程序汪資料鏈接
程序汪接的7個(gè)私活都在這里,經(jīng)驗(yàn)整理
Java項(xiàng)目分享 最新整理全集,找項(xiàng)目不累啦 06版
堪稱神級(jí)的Spring Boot手冊(cè),從基礎(chǔ)入門到實(shí)戰(zhàn)進(jìn)階
臥槽!字節(jié)跳動(dòng)《算法中文手冊(cè)》火了,完整版 PDF 開放下載!
臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開放下載!
字節(jié)跳動(dòng)總結(jié)的設(shè)計(jì)模式 PDF 火了,完整版開放下載!
歡迎添加程序汪個(gè)人微信 itwang007? 進(jìn)粉絲群或圍觀朋友圈
