【基礎設計】Spring整合EasyExcel

前言

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

EasyExcelGitHubUrlgit

簡單需求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屬性的排序混合使用,還須要看源碼是如何排序的。這裏知識匱乏,望之後能夠補充。瀏覽器

相關文章
相關標籤/搜索