SpringBoot 集成 EasyExcel 3.x 優(yōu)雅實(shí)現(xiàn) Excel 導(dǎo)入導(dǎo)出
介紹
EasyExcel 是一個(gè)基于 Java 的、快速、簡潔、解決大文件內(nèi)存溢出的 Excel 處理工具。它能讓你在不用考慮性能、內(nèi)存的等因素的情況下,快速完成 Excel 的讀、寫等功能。
EasyExcel文檔地址:https://easyexcel.opensource.alibaba.com/
快速開始
引入依賴
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.1.3</version></dependency>
簡單導(dǎo)出
以導(dǎo)出用戶信息為例,接下來手把手教大家如何使用EasyExcel實(shí)現(xiàn)導(dǎo)出功能!
定義實(shí)體類
在EasyExcel中,以面向?qū)ο笏枷雭韺?shí)現(xiàn)導(dǎo)入導(dǎo)出,無論是導(dǎo)入數(shù)據(jù)還是導(dǎo)出數(shù)據(jù)都可以想象成具體某個(gè)對象的集合,所以為了實(shí)現(xiàn)導(dǎo)出用戶信息功能,首先創(chuàng)建一個(gè)用戶對象UserDO實(shí)體類,用于封裝用戶信息:
/*** 用戶信息** @author william@StarImmortal*/public class UserDO {private Long id;private String username;private String password;private String nickname;private Date birthday;private String phone;private Double height;private Integer gender;}
上面代碼中類屬性上使用了EasyExcel核心注解:
@ExcelProperty:核心注解,value屬性可用來設(shè)置表頭名稱,converter屬性可以用來設(shè)置類型轉(zhuǎn)換器;
@ColumnWidth:用于設(shè)置表格列的寬度;
@DateTimeFormat:用于設(shè)置日期轉(zhuǎn)換格式;
@NumberFormat:用于設(shè)置數(shù)字轉(zhuǎn)換格式。
自定義轉(zhuǎn)換器
在EasyExcel中,如果想實(shí)現(xiàn)枚舉類型到字符串類型轉(zhuǎn)換(例如gender屬性:1 -> 男,2 -> 女),需實(shí)現(xiàn)Converter接口來自定義轉(zhuǎn)換器,下面為自定義GenderConverter性別轉(zhuǎn)換器代碼實(shí)現(xiàn):
/*** Excel 性別轉(zhuǎn)換器** @author william@StarImmortal*/public class GenderConverter implements Converter<Integer> {public Class<?> supportJavaTypeKey() {return Integer.class;}public CellDataTypeEnum supportExcelTypeKey() {return CellDataTypeEnum.STRING;}public Integer convertToJavaData(ReadConverterContext<?> context) {return GenderEnum.convert(context.getReadCellData().getStringValue()).getValue();}public WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) {return new WriteCellData<>(GenderEnum.convert(context.getValue()).getDescription());}}
/*** 性別枚舉** @author william@StarImmortal*/@Getter@AllArgsConstructorpublic enum GenderEnum {/*** 未知*/UNKNOWN(0, "未知"),/*** 男性*/MALE(1, "男性"),/*** 女性*/FEMALE(2, "女性");private final Integer value;@JsonFormatprivate final String description;public static GenderEnum convert(Integer value) {return Stream.of(values()).filter(bean -> bean.value.equals(value)).findAny().orElse(UNKNOWN);}public static GenderEnum convert(String description) {return Stream.of(values()).filter(bean -> bean.description.equals(description)).findAny().orElse(UNKNOWN);}}
定義接口
/*** EasyExcel導(dǎo)入導(dǎo)出** @author william@StarImmortal*/("/excel")public class ExcelController {("/export/user")public void exportUserExcel(HttpServletResponse response) {try {this.setExcelResponseProp(response, "用戶列表");List<UserDO> userList = this.getUserList();EasyExcel.write(response.getOutputStream()).head(UserDO.class).excelType(ExcelTypeEnum.XLSX).sheet("用戶列表").doWrite(userList);} catch (IOException e) {throw new RuntimeException(e);}}/*** 設(shè)置響應(yīng)結(jié)果** @param response 響應(yīng)結(jié)果對象* @param rawFileName 文件名* @throws UnsupportedEncodingException 不支持編碼異常*/private void setExcelResponseProp(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");}/*** 讀取用戶列表數(shù)據(jù)** @return 用戶列表數(shù)據(jù)* @throws IOException IO異常*/private List<UserDO> getUserList() throws IOException {ObjectMapper objectMapper = new ObjectMapper();ClassPathResource classPathResource = new ClassPathResource("mock/users.json");InputStream inputStream = classPathResource.getInputStream();return objectMapper.readValue(inputStream, new TypeReference<List<UserDO>>() {});}}
測試接口
運(yùn)行項(xiàng)目,通過 Postman 或者 Apifox 工具來進(jìn)行接口測試
注意:在 Apifox 中訪問接口后無法直接下載,需要點(diǎn)擊返回結(jié)果中的下載圖標(biāo)才行,點(diǎn)擊之后方可對Excel文件進(jìn)行保存。
接口地址:http://localhost:8080/excel/export/user


復(fù)雜導(dǎo)出
由于 EasyPoi 支持嵌套對象導(dǎo)出,直接使用內(nèi)置 @ExcelCollection 注解即可實(shí)現(xiàn),遺憾的是 EasyExcel 不支持一對多導(dǎo)出,只能自行實(shí)現(xiàn),通過此issues了解到,項(xiàng)目維護(hù)者建議通過自定義合并策略方式來實(shí)現(xiàn)一對多導(dǎo)出。

解決思路:只需把訂單主鍵相同的列中需要合并的列給合并了,就可以實(shí)現(xiàn)這種一對多嵌套信息的導(dǎo)出
自定義注解
創(chuàng)建一個(gè)自定義注解,用于標(biāo)記哪些屬性需要合并單元格,哪個(gè)屬性是主鍵:
/*** 用于判斷是否需要合并以及合并的主鍵** @author william@StarImmortal*/({ElementType.FIELD})(RetentionPolicy.RUNTIME)public ExcelMerge {/*** 是否合并單元格** @return true || false*/boolean merge() default true;/*** 是否為主鍵(即該字段相同的行合并)** @return true || false*/boolean isPrimaryKey() default false;}
定義實(shí)體類
在需要合并單元格的屬性上設(shè)置 @ExcelMerge 注解,二級(jí)表頭通過設(shè)置 @ExcelProperty 注解中 value 值為數(shù)組形式來實(shí)現(xiàn)該效果:
/*** @author william@StarImmortal*/public class OrderBO {private String id;private String orderId;private String address;private Date createTime;private String productId;private String name;private String subtitle;private String brandName;private BigDecimal price;private Integer count;}
數(shù)據(jù)映射與平鋪
導(dǎo)出之前,需要對數(shù)據(jù)進(jìn)行處理,將訂單數(shù)據(jù)進(jìn)行平鋪,orderList為平鋪前格式,exportData為平鋪后格式:

自定義單元格合并策略
當(dāng) Excel 中兩列主鍵相同時(shí),合并被標(biāo)記需要合并的列:
/*** 自定義單元格合并策略** @author william@StarImmortal*/public class ExcelMergeStrategy implements RowWriteHandler {/*** 主鍵下標(biāo)*/private Integer primaryKeyIndex;/*** 需要合并的列的下標(biāo)集合*/private final List<Integer> mergeColumnIndexList = new ArrayList<>();/*** 數(shù)據(jù)類型*/private final Class<?> elementType;public ExcelMergeStrategy(Class<?> elementType) {this.elementType = elementType;}public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {// 判斷是否為標(biāo)題if (isHead) {return;}// 獲取當(dāng)前工作表Sheet sheet = writeSheetHolder.getSheet();// 初始化主鍵下標(biāo)和需要合并字段的下標(biāo)if (primaryKeyIndex == null) {this.initPrimaryIndexAndMergeIndex(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(primaryKeyIndex).getStringCellValue().equalsIgnoreCase(row.getCell(primaryKeyIndex).getStringCellValue())) {for (Integer mergeIndex : mergeColumnIndexList) {CellRangeAddress cellRangeAddress = new CellRangeAddress(row.getRowNum() - 1, row.getRowNum(), mergeIndex, mergeIndex);sheet.addMergedRegionUnsafe(cellRangeAddress);}}}/*** 初始化主鍵下標(biāo)和需要合并字段的下標(biāo)** @param writeSheetHolder WriteSheetHolder*/private void initPrimaryIndexAndMergeIndex(WriteSheetHolder writeSheetHolder) {// 獲取當(dāng)前工作表Sheet sheet = writeSheetHolder.getSheet();// 獲取標(biāo)題行Row titleRow = sheet.getRow(0);// 獲取所有屬性字段Field[] fields = this.elementType.getDeclaredFields();// 遍歷所有字段for (Field field : fields) {// 獲取@ExcelProperty注解,用于獲取該字段對應(yīng)列的下標(biāo)ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);// 判斷是否為空if (null == excelProperty) {continue;}// 獲取自定義注解,用于合并單元格ExcelMerge excelMerge = field.getAnnotation(ExcelMerge.class);// 判斷是否需要合并if (null == excelMerge) {continue;}for (int i = 0; i < fields.length; i++) {Cell cell = titleRow.getCell(i);if (null == cell) {continue;}// 將字段和表頭匹配上if (excelProperty.value()[0].equalsIgnoreCase(cell.getStringCellValue())) {if (excelMerge.isPrimaryKey()) {primaryKeyIndex = i;}if (excelMerge.merge()) {mergeColumnIndexList.add(i);}}}}// 沒有指定主鍵,則異常if (null == this.primaryKeyIndex) {throw new IllegalStateException("使用@ExcelMerge注解必須指定主鍵");}}}
定義接口
將自定義合并策略 ExcelMergeStrategy 通過 registerWriteHandler 注冊上去:
/*** EasyExcel導(dǎo)入導(dǎo)出** @author william@StarImmortal*/public class ExcelController {public void exportOrderExcel(HttpServletResponse response) {try {this.setExcelResponseProp(response, "訂單列表");List<OrderDO> orderList = this.getOrderList();List<OrderBO> exportData = this.convert(orderList);EasyExcel.write(response.getOutputStream()).head(OrderBO.class).registerWriteHandler(new ExcelMergeStrategy(OrderBO.class)).excelType(ExcelTypeEnum.XLSX).sheet("訂單列表").doWrite(exportData);} catch (IOException e) {throw new RuntimeException(e);}}/*** 設(shè)置響應(yīng)結(jié)果** @param response 響應(yīng)結(jié)果對象* @param rawFileName 文件名* @throws UnsupportedEncodingException 不支持編碼異常*/private void setExcelResponseProp(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)目,通過 Postman 或者 Apifox 工具來進(jìn)行接口測試
注意:在 Apifox 中訪問接口后無法直接下載,需要點(diǎn)擊返回結(jié)果中的下載圖標(biāo)才行,點(diǎn)擊之后方可對Excel文件進(jìn)行保存。
接口地址:http://localhost:8080/excel/export/order


簡單導(dǎo)入
以導(dǎo)入用戶信息為例,接下來手把手教大家如何使用EasyExcel實(shí)現(xiàn)導(dǎo)入功能!
/*** EasyExcel導(dǎo)入導(dǎo)出** @author william@StarImmortal*/public class ExcelController {public ResponseVO importUserExcel( MultipartFile file) {try {List<UserDO> userList = EasyExcel.read(file.getInputStream()).head(UserDO.class).sheet().doReadSync();return ResponseVO.success(userList);} catch (IOException e) {return ResponseVO.error();}}}

參考資料
項(xiàng)目地址:https://github.com/alibaba/easyexcel
官方文檔:https://www.yuque.com/easyexcel/doc/easyexcel
一對多導(dǎo)出優(yōu)雅方案:https://github.com/alibaba/easyexcel/issues/1780
