前言
Java解析、生成Excel比較有名的框架有Apache poi、jxl。但他們都存在一個嚴重的問題就是很是的耗內存,poi有一套SAX模式的API能夠必定程度的解決一些內存溢出的問題,但POI仍是有一些缺陷,好比07版Excel解壓縮以及解壓後存儲都是在內存中完成的,內存消耗依然很大。easyexcel重寫了poi對07版Excel的解析,可以本來一個3M的excel用POI sax依然須要100M左右內存下降到幾M,而且再大的excel不會出現內存溢出,03版依賴POI的sax模式。在上層作了模型轉換的封裝,讓使用者更加簡單方便。html
<p align="right">——<a href="https://github.com/alibaba/easyexcel" target="_blank">easyexcel</a></p>前端
起步
- maven or gradle
- springboot
- api or blog
快速上手
EasyExcelApijava
簡單需求demo
- demo地址
喜歡直接看項目的能夠直接 >> demo-easy-excelgithub
-
內容大體瀏覽web
-
引入easyexcelspring
引入easyexcel (maven爲例),引入easyexcel數據庫
<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.1.4</version> </dependency>
- 自定義註解
/** * @author quaint * @since 17 February 2020 */ @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ExcelPropertyNotNull { /** * @return 開啓校驗 */ boolean open() default true; /** * @return 提示消息 */ String message() default ""; /** * @return 列號 */ int col() default -1; }
- 建立對應Excel的Dto
業務中有各類類型,這裏基於java8經常使用的類型進行測試。api
/** * 父類 可能業務須要繼承 * @author quaint * @date 2020-01-14 11:23 */ @Data public class DemoParentDto { @ExcelProperty(index = 0,value = {"序號"}) private Integer num; } /** * 子類 通常業務一個子類便可 * @author quaint * @date 2020-01-14 11:20 */ @EqualsAndHashCode(callSuper = true) @AllArgsConstructor @NoArgsConstructor @Data public class DemoUserDto extends DemoParentDto{ @ExcelProperty(value = {"姓名"}) private String name; @ExcelProperty(value = {"性別"}) private String sex; /** * @see LocalDateConverter (時間格式轉換器)LocalDateTime同理,代碼也會貼出來 */ @ExcelProperty(value = "生日",converter = LocalDateConverter.class) @DateTimeFormat("yyyy-MM-dd") private LocalDate birthday; @ExcelProperty(value = {"存款"}) @ExcelPropertyNotNull(message = "不可爲空", col = 4) private BigDecimal money; /** * 獲取6個測試數據 * @return 6個 */ public static List<DemoUserDto> getUserDtoTest6(String search){ List<DemoUserDto> list = new ArrayList<>(); list.add(new DemoUserDto("quaint","男",LocalDate.of(2011,11,11),BigDecimal.ONE)); list.add(new DemoUserDto("quaint2","女",LocalDate.of(2001,11,1),BigDecimal.TEN)); list.add(new DemoUserDto("quaint3","男",LocalDate.of(2010,2,7),new BigDecimal(11.11))); list.add(new DemoUserDto("quaint4","男",LocalDate.of(2011,1,11),new BigDecimal(10.24))); list.add(new DemoUserDto("quaint5","女",LocalDate.of(2021,5,12),BigDecimal.ZERO)); list.add(new DemoUserDto(search,"男",LocalDate.of(2010,7,11),BigDecimal.TEN)); return list; } }
- 建立converter(導入導出時自定義轉換對應字段)
/** * LocalDate and string converter * @author quait */ public class LocalDateConverter implements Converter<LocalDate> { @Override public Class supportJavaTypeKey() { return LocalDate.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } @Override public LocalDate convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration){ // 將excel 中的 數據 轉換爲 LocalDate if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { return LocalDate.parse(cellData.getStringValue(), DateTimeFormatter.ISO_LOCAL_DATE); } else { // 獲取註解的 format 注意,註解須要導入這個 excel.annotation.format.DateTimeFormat; return LocalDate.parse(cellData.getStringValue(), DateTimeFormatter.ofPattern(contentProperty.getDateTimeFormatProperty().getFormat())); } } @Override public CellData convertToExcelData(LocalDate value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { // 將 LocalDateTime 轉換爲 String if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { return new CellData(value.toString()); } else { return new CellData(value.format(DateTimeFormatter.ofPattern(contentProperty.getDateTimeFormatProperty().getFormat()))); } } } /** * LocalDateTime and string converter * * @author quait */ public class LocalDateTimeConverter implements Converter<LocalDateTime> { @Override public Class supportJavaTypeKey() { return LocalDateTime.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } @Override public LocalDateTime convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration){ // 將excel 中的 數據 轉換爲 LocalDateTime if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { return LocalDateTime.parse(cellData.getStringValue(), DateTimeFormatter.ISO_LOCAL_DATE_TIME); } else { // 獲取註解的 format 注意,註解須要導入這個 excel.annotation.format.DateTimeFormat; DateTimeFormatter formatter = DateTimeFormatter.ofPattern(contentProperty.getDateTimeFormatProperty().getFormat()); return LocalDateTime.parse(cellData.getStringValue(), formatter); } } @Override public CellData convertToExcelData(LocalDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { // 將 LocalDateTime 轉換爲 String if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { return new CellData(value.toString()); } else { DateTimeFormatter formatter = DateTimeFormatter.ofPattern(contentProperty.getDateTimeFormatProperty().getFormat()); return new CellData(value.format(formatter)); } } }
- 建立Listener(監聽Excel導入)
/** * 官方提示:有個很重要的點 DemoDataListener 不能被spring管理,要每次讀取excel都要new,而後裏面用到spring能夠構造方法傳進去 * * 若是想被spring 管理的話, 改成原型模式, Controller 以 getBean 形式獲取 本博客展現被spring管理 * @author quaint */ @EqualsAndHashCode(callSuper = true) @Slf4j @Data @Scope(SCOPE_PROTOTYPE) @Component public class DemoUserListener extends AnalysisEventListener<DemoUserDto> { /** * 每隔5條存儲數據庫,實際使用中能夠3000條,而後清理list ,方便內存回收 */ private static final int BATCH_COUNT = 5; private List<DemoUserDto> list = new ArrayList<>(); /** * 方式一 * 能夠換成 @Autowired 注入 service 或者mapper * 不被spring管理的話 使用構造函數 接收外面被spring管理的mapper -->constructor * @Autowired * DemoUserMapper demoUserMapper; */ private List<DemoUserDto> virtualDataBase = new ArrayList<>(); /** * 方式二 * 假設 virtualDataBase 是 mapper, 這裏就在外面new該類的時候傳進來 調用方注入過得mapper * 而且 把Scope、Component註解去掉 */ // public DemoUserListener(List<DemoUserDto> virtualDataBase) { // this.virtualDataBase = virtualDataBase; // } /** * 這個每一條數據解析都會來調用 */ @Override public void invoke(DemoUserDto data, AnalysisContext context) { log.info("解析到一條數據:{}", JSONObject.toJSONString(data)); // 校驗非空 Field[] fields = DemoUserDto.class.getDeclaredFields(); for (Field f: fields) { f.setAccessible(true); ExcelPropertyNotNull ann = f.getAnnotation(ExcelPropertyNotNull.class); if (null != ann && ann.open()){ try { if(null == f.get(data)){ log.info("有一條數據未經過校驗,message[{}]",ann.message()); log.info("列號:[{}]",ann.col()); return; } } catch (IllegalAccessException e) { e.printStackTrace(); } } } list.add(data); // 達到BATCH_COUNT了,須要去存儲一次數據庫,防止數據幾萬條數據在內存,容易OOM if (list.size() >= BATCH_COUNT) { saveData(); // 存儲完成清理 list list.clear(); } } /** * 全部數據解析完成了 會來調用 */ @Override public void doAfterAllAnalysed(AnalysisContext context) { // 這裏也要保存數據,確保最後遺留的數據也存儲到數據庫 saveData(); log.info("全部數據解析完成!"); } /** * 在轉換異常 獲取其餘異常下會調用本接口。拋出異常則中止讀取。若是這裏不拋出異常則 繼續讀取下一行。 * @param exception exception * @param context context * @throws Exception e */ @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()); } } /** * 加上存儲數據庫 */ private void saveData() { log.info("{}條數據,開始存儲數據庫!", list.size()); virtualDataBase.addAll(list); log.info("存儲數據庫成功!"); } }
- 建立Handler
/** * 自定義攔截器。對第一行第一列的頭超連接到:https://github.com/alibaba/easyexcel * 這裏沒有采用 spring 管理 * @author Jiaju Zhuang */ @Slf4j public class CustomCellWriteHandler implements CellWriteHandler { @Override public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) { log.info("cell 建立以前"); } @Override public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { log.info("cell 建立後"); } @Override public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { // 這裏能夠對cell進行任何操做 log.info("第{}行,第{}列寫入完成。", cell.getRowIndex(), cell.getColumnIndex()); } }
- 建立handler控制單元格樣式
/** * 自定義寫Excel handler 實現style 策略。 * @author quaint * @date 14 February 2020 * @since 1.30 */ public class ProductWriteErrHandler extends AbstractCellStyleStrategy { /** * 存儲解析失敗的行號和列號 */ private Map<Integer, Integer> failureRowCol; /** * 能夠這麼理解: 外部定義樣式 */ private WriteCellStyle writeErrCellStyle; /** * 單元格樣式 */ private CellStyle errCellStyle; /** * 在這裏自定義樣式, 或者在外面定義樣式 */ public ProductWriteErrHandler(WriteCellStyle writeCellStyle,Map<Integer, Integer> failureRowCol) { this.writeErrCellStyle = writeCellStyle; this.failureRowCol = failureRowCol; } /** * 單元格樣式初始化方法 * @param workbook */ @Override protected void initCellStyle(Workbook workbook) { // 初始化 if (writeErrCellStyle!=null){ errCellStyle = StyleUtil.buildContentCellStyle(workbook, writeErrCellStyle); } } /** * 寫頭部樣式 * @param cell * @param head * @param relativeRowIndex */ @Override protected void setHeadCellStyle(Cell cell, Head head, Integer relativeRowIndex) { } /** * 寫內容樣式 * @param cell * @param head * @param relativeRowIndex */ @Override protected void setContentCellStyle(Cell cell, Head head, Integer relativeRowIndex) { // 判斷 是否傳入 錯誤的 map if (!CollectionUtils.isEmpty(failureRowCol)){ // 若是錯誤 的行 和列 對應成功 --> 染色 if (failureRowCol.containsKey(cell.getRowIndex()) && failureRowCol.get(cell.getRowIndex()).equals(cell.getColumnIndex())){ cell.setCellStyle(errCellStyle); } } } }
- 控制層Controller
/** * @author quaint * @date 2020-01-14 11:13 */ @Controller @Slf4j public class DemoEasyExcelSpi implements ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @PostMapping("/in/excel") public String inExcel(@RequestParam("inExcel") MultipartFile inExcel, Model model){ DemoUserListener demoUserListener = applicationContext.getBean(DemoUserListener.class); log.info("demoUserListener 在 spi 調用以前 hashCode爲 [{}]", demoUserListener.hashCode()); if (inExcel.isEmpty()){ // 讀取 local 指定文件 List<DemoUserDto> demoUserList; String filePath = System.getProperty("user.dir")+"/demo-easy-excel/src/main/resources/ExcelTest.xlsx"; try { // 這裏 須要指定讀用哪一個class去讀,而後讀取第一個sheet 文件流會自動關閉 EasyExcel.read(filePath, DemoUserDto.class, demoUserListener).sheet().doRead(); demoUserList = demoUserListener.getVirtualDataBase(); } catch (Exception e) { e.printStackTrace(); return null; } model.addAttribute("users", demoUserList); } else { // 讀取 web 上傳的文件 List<DemoUserDto> demoUserList; try { EasyExcel.read(inExcel.getInputStream(), DemoUserDto.class, demoUserListener).sheet().doRead(); demoUserList = demoUserListener.getVirtualDataBase(); } catch (IOException e) { e.printStackTrace(); return null; } model.addAttribute("users", demoUserList); } log.info("demoUserListener 在 spi 調用以後 hashCode爲 [{}]", demoUserListener.hashCode()); return "index"; } @PostMapping("/out/excel") public void export(HttpServletResponse response){ String search = "@RequestBody Object search"; // 根據前端傳入的查詢條件 去庫裏查到要導出的dto List<DemoUserDto> userDto = DemoUserDto.getUserDtoTest6(search); // 要忽略的 字段 List<String> ignoreIndices = Collections.singletonList("性別"); // 根據類型獲取要反射的對象 Class clazz = DemoUserDto.class; // 遍歷全部字段, 找到忽略的字段 Set<String> excludeFiledNames = new HashSet<>(); while (clazz != Object.class){ Arrays.stream(clazz.getDeclaredFields()).forEach(field -> { ExcelProperty ann = field.getAnnotation(ExcelProperty.class); if (ann!=null && ignoreIndices.contains(ann.value()[0])){ // 忽略 該字段 excludeFiledNames.add(field.getName()); } }); clazz = clazz.getSuperclass(); } // 設置序號 AtomicInteger i = new AtomicInteger(1); userDto.forEach(u-> u.setNum(i.getAndIncrement())); // 建立本地文件 EasyExcelUtils.exportLocalExcel(userDto,DemoUserDto.class,"ExcelTest",excludeFiledNames); // 建立web文件 EasyExcelUtils.exportWebExcel(response,userDto,DemoUserDto.class,"ExcelTest",null); } }
- 導出工具類
/** * EasyExcelUtils * @author quaint * @date 2020-01-14 14:26 */ public abstract class EasyExcelUtils { /** * 導出excel * @param response http下載 * @param dataList 導出的數據 * @param clazz 導出的模板類 * @param fileName 導出的文件名 * @param excludeFiledNames 要排除的filed * @param <T> 模板 */ public static <T> void exportWebExcel(HttpServletResponse response, List<T> dataList, Class<T> clazz, String fileName, Set<String> excludeFiledNames) { // 這裏注意 有同窗反應使用swagger 會致使各類問題,請直接用瀏覽器或者用postman response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); // 單元格樣式策略 定義 WriteCellStyle style = new WriteCellStyle(); // 這裏須要指定 FillPatternType 爲FillPatternType.SOLID_FOREGROUND 否則沒法顯示背景顏色.頭默認了 FillPatternType因此能夠不指定 style.setFillPatternType(FillPatternType.SOLID_FOREGROUND); style.setFillForegroundColor(IndexedColors.RED.getIndex()); Map<Integer,Integer> errRecord = new HashMap<>(); errRecord.put(1,1); errRecord.put(2,2); ProductWriteErrHandler handler = new ProductWriteErrHandler(style,errRecord); try { // 導出excel EasyExcel.write(response.getOutputStream(), clazz) // 設置過濾字段策略 .excludeColumnFiledNames(excludeFiledNames) // 選擇導入時的 handler .registerWriteHandler(handler) .sheet("fileName") .doWrite(dataList); } catch (IOException e) { System.err.println("建立文件異常!"); } } /** * 導出excel * @param dataList 導出的數據 * @param clazz 導出的模板類 * @param fileName 導出的文件名 * @param excludeFiledNames 要排除的filed * @param <T> 模板 */ public static <T> void exportLocalExcel(List<T> dataList, Class<T> clazz, String fileName, Set<String> excludeFiledNames){ //建立本地文件 test 使用 String filePath = System.getProperty("user.dir")+"/demo-easy-excel/src/main/resources/"+fileName+".xlsx"; File dbfFile = new File(filePath); if (!dbfFile.exists() || dbfFile.isDirectory()) { try { dbfFile.createNewFile(); } catch (IOException e) { System.err.println("建立文件異常!"); return; } } // 導出excel EasyExcel.write(filePath, clazz) .registerWriteHandler(new CustomCellWriteHandler()) .excludeColumnFiledNames(excludeFiledNames) .sheet("SheetName").doWrite(dataList); } }
- 前端代碼
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <style> .data-local{ border: 1px black; } </style> <body> <form th:action="@{/in/excel}" method="post" enctype="multipart/form-data"> <input name="inExcel" type="file" value="上傳文件"/> <input type="submit" value="導入excel"/> </form> <h2>導入的數據展現位置:</h2> <div class="data-local" th:each="user : ${users}"> <span th:text="${user}"></span> </div> <form th:action="@{/out/excel}" method="post"> <input type="submit" value="導出下載文件"/> </form> </body> </html>
- 導入效果圖
- 導出效果圖
總結
Listener和Handler的自定義寫法能夠知足絕大多數需求,大佬設計的代碼用起來就是舒服。就是@ExcelProperty註解的index屬性的排序混合使用,還須要看源碼是如何排序的。這裏知識匱乏,望之後能夠補充。瀏覽器