大批量Excel文件導出實戰(EasyPOI)

業務需求

接觸了杭州市執法信息平臺歷史案卷的導出功能,因爲有個功能是要導出全部的案卷,10年的執法數據有100w+的數據量,怎麼樣快速導出成爲了棘手的問題。

傳統POI遇到的問題

  1. Excel寫入過慢;
  2. 每個Sheet僅支持65536(2003版)條數據;
  3. 容易導致OOM。
  4. 容易引起頁面奔潰
  5. 網絡傳輸數據太多

解決辦法

  1. 尋找合適的POI(集成easyExcel組件)框架減少內存消耗
  2. 儘量避免一次性加載所有的數據(分頁查詢)
  3. 採用多線程的方式
  4. 採用壓縮文件打包的方式
  5. 採用進度條的交互方式

具體實現核心代碼

依賴

 <!-- 集成easypoi組件 .導出excel http://easypoi.mydoc.io/ -->
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-base</artifactId>
            <version>4.1.0</version>
        </dependency>

        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-web</artifactId>
            <version>4.1.0</version>
        </dependency>

        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-annotation</artifactId>
            <version>4.1.0</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.0.5</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/net.lingala.zip4j/zip4j -->
        <dependency>
            <groupId>net.lingala.zip4j</groupId>
            <artifactId>zip4j</artifactId>
            <version>1.3.2</version>
        </dependency>

EasyExcelUtil

@Slf4j
public class EasyExcelUtil {
    private static Sheet initSheet;

    static {
        initSheet = new Sheet(1, 0);
        initSheet.setSheetName("sheet");
        //設置自適應寬度
        initSheet.setAutoWidth(Boolean.TRUE);
    }

    /**
     * 讀取少於1000行數據
     * @param filePath 文件絕對路徑
     * @return
     */
    public static List<Object> readLessThan1000Row(String filePath){
        return readLessThan1000RowBySheet(filePath,null);
    }

    /**
     * 讀小於1000行數據, 帶樣式
     * filePath 文件絕對路徑
     * initSheet :
     *      sheetNo: sheet頁碼,默認爲1
     *      headLineMun: 從第幾行開始讀取數據,默認爲0, 表示從第一行開始讀取
     *      clazz: 返回數據List<Object> 中Object的類名
     */
    public static List<Object> readLessThan1000RowBySheet(String filePath, Sheet sheet){
        if(!StringUtils.hasText(filePath)){
            return null;
        }

        sheet = sheet != null ? sheet : initSheet;

        InputStream fileStream = null;
        try {
            fileStream = new FileInputStream(filePath);
            return EasyExcelFactory.read(fileStream, sheet);
        } catch (FileNotFoundException e) {
            log.info("找不到文件或文件路徑錯誤, 文件:{}", filePath);
        }finally {
            try {
                if(fileStream != null){
                    fileStream.close();
                }
            } catch (IOException e) {
                log.info("excel文件讀取失敗, 失敗原因:{}", e);
            }
        }
        return null;
    }

    /**
     * 讀大於1000行數據
     * @param filePath 文件覺得路徑
     * @return
     */
    public static List<Object> readMoreThan1000Row(String filePath){
        return readMoreThan1000RowBySheet(filePath,null);
    }

    /**
     * 讀大於1000行數據, 帶樣式
     * @param filePath 文件覺得路徑
     * @return
     */
    public static List<Object> readMoreThan1000RowBySheet(String filePath, Sheet sheet){
        if(!StringUtils.hasText(filePath)){
            return null;
        }

        sheet = sheet != null ? sheet : initSheet;

        InputStream fileStream = null;
        try {
            fileStream = new FileInputStream(filePath);
            ExcelListener excelListener = new ExcelListener();
            EasyExcelFactory.readBySax(fileStream, sheet, excelListener);
            return excelListener.getDatas();
        } catch (FileNotFoundException e) {
            log.error("找不到文件或文件路徑錯誤, 文件:{}", filePath);
        }finally {
            try {
                if(fileStream != null){
                    fileStream.close();
                }
            } catch (IOException e) {
                log.error("excel文件讀取失敗, 失敗原因:{}", e);
            }
        }
        return null;
    }

    /**
     * 生成excle
     * @param filePath  絕對路徑, 如:/home/chenmingjian/Downloads/aaa.xlsx
     * @param data 數據源
     * @param head 表頭
     */
    public static void writeBySimple(String filePath, List<List<Object>> data, List<String> head){
        writeSimpleBySheet(filePath,data,head,null);
    }

    /**
     * 生成excle
     * @param filePath 絕對路徑, 如:/home/chenmingjian/Downloads/aaa.xlsx
     * @param data 數據源
     * @param sheet excle頁面樣式
     * @param head 表頭
     */
    public static void writeSimpleBySheet(String filePath, List<List<Object>> data, List<String> head, Sheet sheet){
        sheet = (sheet != null) ? sheet : initSheet;

        if(head != null){
            List<List<String>> list = new ArrayList<>();
            head.forEach(h -> list.add(Collections.singletonList(h)));
            sheet.setHead(list);
        }

        OutputStream outputStream = null;
        ExcelWriter writer = null;
        try {
            outputStream = new FileOutputStream(filePath);
            writer = EasyExcelFactory.getWriter(outputStream);
            writer.write1(data,sheet);
        } catch (FileNotFoundException e) {
            log.error("找不到文件或文件路徑錯誤, 文件:{}", filePath);
        }finally {
            try {
                if(writer != null){
                    writer.finish();
                }

                if(outputStream != null){
                    outputStream.close();
                }

            } catch (IOException e) {
                log.error("excel文件導出失敗, 失敗原因:{}", e);
            }
        }

    }

    /**
     * 生成excle
     * @param filePath 絕對路徑, 如:/home/chenmingjian/Downloads/aaa.xlsx
     * @param data 數據源
     */
    public static void writeWithTemplate(String filePath, List<? extends BaseRowModel> data){
        writeWithTemplateAndSheet(filePath,data,null);
    }

    /**
     * 生成excle
     * @param filePath 絕對路徑, 如:/home/chenmingjian/Downloads/aaa.xlsx
     * @param data 數據源
     * @param sheet excle頁面樣式
     */
    public static void writeWithTemplateAndSheet(String filePath, List<? extends BaseRowModel> data, Sheet sheet){
        if(CollectionUtils.isEmpty(data)){
            return;
        }

        sheet = (sheet != null) ? sheet : initSheet;
        sheet.setClazz(data.get(0).getClass());

        OutputStream outputStream = null;
        ExcelWriter writer = null;
        try {
            outputStream = new FileOutputStream(filePath);
            writer = EasyExcelFactory.getWriter(outputStream);
            writer.write(data,sheet);
        } catch (FileNotFoundException e) {
            log.error("找不到文件或文件路徑錯誤, 文件:{}", filePath);
        }finally {
            try {
                if(writer != null){
                    writer.finish();
                }

                if(outputStream != null){
                    outputStream.close();
                }
            } catch (IOException e) {
                log.error("excel文件導出失敗, 失敗原因:{}", e);
            }
        }

    }

    /**
     * 生成多Sheet的excle
     * @param filePath 絕對路徑, 如:/home/chenmingjian/Downloads/aaa.xlsx
     * @param multipleSheelPropetys
     */
    public static void writeWithMultipleSheel(String filePath,List<MultipleSheelPropety> multipleSheelPropetys){
        if(CollectionUtils.isEmpty(multipleSheelPropetys)){
            return;
        }

        OutputStream outputStream = null;
        ExcelWriter writer = null;
        try {
            outputStream = new FileOutputStream(filePath);
            writer = EasyExcelFactory.getWriter(outputStream);
            for (MultipleSheelPropety multipleSheelPropety : multipleSheelPropetys) {
                Sheet sheet = multipleSheelPropety.getSheet() != null ? multipleSheelPropety.getSheet() : initSheet;
                if(!CollectionUtils.isEmpty(multipleSheelPropety.getData())){
                    sheet.setClazz(multipleSheelPropety.getData().get(0).getClass());
                }
                writer.write(multipleSheelPropety.getData(), sheet);
            }

        } catch (FileNotFoundException e) {
            log.error("找不到文件或文件路徑錯誤, 文件:{}", filePath);
        }finally {
            try {
                if(writer != null){
                    writer.finish();
                }

                if(outputStream != null){
                    outputStream.close();
                }
            } catch (IOException e) {
                log.error("excel文件導出失敗, 失敗原因:{}", e);
            }
        }

    }


    /*********************匿名內部類開始,可以提取出去******************************/

    @Data
    public static class MultipleSheelPropety{

        private List<? extends BaseRowModel> data;

        private Sheet sheet;
    }

    /**
     * 解析監聽器,
     * 每解析一行會回調invoke()方法。
     * 整個excel解析結束會執行doAfterAllAnalysed()方法
     *
     */
    @Getter
    @Setter
    public static class ExcelListener extends AnalysisEventListener {

        private List<Object> datas = new ArrayList<>();

        /**
         * 逐行解析
         * object : 當前行的數據
         */
        @Override
        public void invoke(Object object, AnalysisContext context) {
            //當前行
            // context.getCurrentRowNum()
            if (object != null) {
                datas.add(object);
            }
        }


        /**
         * 解析完所有數據後會調用該方法
         */
        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            //解析結束銷燬不用的資源
        }

    }

    /************************匿名內部類結束,可以提取出去***************************/


工具類:ZIP壓縮文件操作工具類

@Slf4j
public class CompressUtil {

    /**
     * 使用給定密碼解壓指定的ZIP壓縮文件到指定目錄
     * <p>
     * 如果指定目錄不存在,可以自動創建,不合法的路徑將導致異常被拋出
     * @param zip 指定的ZIP壓縮文件
     * @param dest 解壓目錄
     * @param passwd ZIP文件的密碼
     * @return 解壓後文件數組
     * @throws ZipException 壓縮文件有損壞或者解壓縮失敗拋出
     */
    public static File [] unzip(String zip, String dest, String passwd) throws ZipException {
        File zipFile = new File(zip);
        return unzip(zipFile, dest, passwd);
    }

    /**
     * 使用給定密碼解壓指定的ZIP壓縮文件到當前目錄
     * @param zip 指定的ZIP壓縮文件
     * @param passwd ZIP文件的密碼
     * @return  解壓後文件數組
     * @throws ZipException 壓縮文件有損壞或者解壓縮失敗拋出
     */
    public static File [] unzip(String zip, String passwd) throws ZipException {
        File zipFile = new File(zip);
        File parentDir = zipFile.getParentFile();
        return unzip(zipFile, parentDir.getAbsolutePath(), passwd);
    }

    /**
     * 使用給定密碼解壓指定的ZIP壓縮文件到指定目錄
     * <p>
     * 如果指定目錄不存在,可以自動創建,不合法的路徑將導致異常被拋出
     * @param dest 解壓目錄
     * @param passwd ZIP文件的密碼
     * @return  解壓後文件數組
     * @throws ZipException 壓縮文件有損壞或者解壓縮失敗拋出
     */
    public static File [] unzip(File zipFile, String dest, String passwd) throws ZipException {
        ZipFile zFile = new ZipFile(zipFile);
        zFile.setFileNameCharset("GBK");
        if (!zFile.isValidZipFile()) {
            throw new ZipException("壓縮文件不合法,可能被損壞.");
        }
        File destDir = new File(dest);
        if (destDir.isDirectory() && !destDir.exists()) {
            destDir.mkdir();
        }
        if (zFile.isEncrypted()) {
            zFile.setPassword(passwd.toCharArray());
        }
        zFile.extractAll(dest);

        List<FileHeader> headerList = zFile.getFileHeaders();
        List<File> extractedFileList = new ArrayList<File>();
        for(FileHeader fileHeader : headerList) {
            if (!fileHeader.isDirectory()) {
                extractedFileList.add(new File(destDir,fileHeader.getFileName()));
            }
        }
        File [] extractedFiles = new File[extractedFileList.size()];
        extractedFileList.toArray(extractedFiles);
        return extractedFiles;
    }

    /**
     * 壓縮指定文件到當前文件夾
     * @param src 要壓縮的指定文件
     * @return 最終的壓縮文件存放的絕對路徑,如果爲null則說明壓縮失敗.
     */
    public static String zip(String src) {
        return zip(src,null);
    }

    /**
     * 使用給定密碼壓縮指定文件或文件夾到當前目錄
     * @param src 要壓縮的文件
     * @param passwd 壓縮使用的密碼
     * @return 最終的壓縮文件存放的絕對路徑,如果爲null則說明壓縮失敗.
     */
//    public static String zip(String src, String passwd) {
//        return zip(src, null, passwd);
//    }

    /**
     * 使用給定密碼壓縮指定文件或文件夾到當前目錄
     * @param src 要壓縮的文件
     * @param dest 壓縮文件存放路徑
     * @return 最終的壓縮文件存放的絕對路徑,如果爲null則說明壓縮失敗.
     */
    public static String zip(String src, String dest) {
        return zip(src, dest, false, null);
    }

    /**
     * 使用給定密碼壓縮指定文件或文件夾到當前目錄
     * @param src 要壓縮的文件
     * @param dest 壓縮文件存放路徑
     * @param passwd 壓縮使用的密碼
     * @return 最終的壓縮文件存放的絕對路徑,如果爲null則說明壓縮失敗.
     */
    public static String zip(String src, String dest, String passwd) {
        return zip(src, dest, true, passwd);
    }

    /**
     * 使用給定密碼壓縮指定文件或文件夾到指定位置.
     * <p>
     * dest可傳最終壓縮文件存放的絕對路徑,也可以傳存放目錄,也可以傳null或者"".<br />
     * 如果傳null或者""則將壓縮文件存放在當前目錄,即跟源文件同目錄,壓縮文件名取源文件名,以.zip爲後綴;<br />
     * 如果以路徑分隔符(File.separator)結尾,則視爲目錄,壓縮文件名取源文件名,以.zip爲後綴,否則視爲文件名.
     * @param src 要壓縮的文件或文件夾路徑
     * @param dest 壓縮文件存放路徑
     * @param isCreateDir 是否在壓縮文件裏創建目錄,僅在壓縮文件爲目錄時有效.<br />
     * 如果爲false,將直接壓縮目錄下文件到壓縮文件.
     * @param passwd 壓縮使用的密碼
     * @return 最終的壓縮文件存放的絕對路徑,如果爲null則說明壓縮失敗.
     */
    public static String zip(String src, String dest, boolean isCreateDir, String passwd) {
        if(Files.exists(Paths.get(dest))){
            log.error("已經存在壓縮文件[{}],不能執行壓縮過程!", dest);
            return null;
        }
        File srcFile = new File(src);
        dest = buildDestinationZipFilePath(srcFile, dest);
        ZipParameters parameters = new ZipParameters();
        parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE);			// 壓縮方式
        parameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL);	// 壓縮級別
        if (!StringUtils.isEmpty(passwd)) {
            parameters.setEncryptFiles(true);
            parameters.setEncryptionMethod(Zip4jConstants.ENC_METHOD_STANDARD);	// 加密方式
            parameters.setPassword(passwd.toCharArray());
        }
        try {
            ZipFile zipFile = new ZipFile(dest);

            if (srcFile.isDirectory()) {
                // 如果不創建目錄的話,將直接把給定目錄下的文件壓縮到壓縮文件,即沒有目錄結構
                if (!isCreateDir) {
                    File [] subFiles = srcFile.listFiles();
                    ArrayList<File> temp = new ArrayList<File>();
                    Collections.addAll(temp, subFiles);
                    zipFile.addFiles(temp, parameters);
                    return dest;
                }
                zipFile.addFolder(srcFile, parameters);
            } else {
                zipFile.addFile(srcFile, parameters);
            }
            return dest;
        } catch (ZipException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 構建壓縮文件存放路徑,如果不存在將會創建
     * 傳入的可能是文件名或者目錄,也可能不傳,此方法用以轉換最終壓縮文件的存放路徑
     * @param srcFile 源文件
     * @param destParam 壓縮目標路徑
     * @return 正確的壓縮文件存放路徑
     */
    private static String buildDestinationZipFilePath(File srcFile,String destParam) {
        if (StringUtils.isEmpty(destParam)) {
            if (srcFile.isDirectory()) {
                destParam = srcFile.getParent() + File.separator + srcFile.getName() + ".zip";
            } else {
                String fileName = srcFile.getName().substring(0, srcFile.getName().lastIndexOf("."));
                destParam = srcFile.getParent() + File.separator + fileName + ".zip";
            }
        } else {
            createDestDirectoryIfNecessary(destParam);	// 在指定路徑不存在的情況下將其創建出來
            if (destParam.endsWith(File.separator)) {
                String fileName = "";
                if (srcFile.isDirectory()) {
                    fileName = srcFile.getName();
                } else {
                    fileName = srcFile.getName().substring(0, srcFile.getName().lastIndexOf("."));
                }
                destParam += fileName + ".zip";
            }
        }
        return destParam;
    }

    /**
     * 在必要的情況下創建壓縮文件存放目錄,比如指定的存放路徑並沒有被創建
     * @param destParam 指定的存放路徑,有可能該路徑並沒有被創建
     */
    private static void createDestDirectoryIfNecessary(String destParam) {
        File destDir = null;
        if (destParam.endsWith(File.separator)) {
            destDir = new File(destParam);
        } else {
            destDir = new File(destParam.substring(0, destParam.lastIndexOf(File.separator)));
        }
        if (!destDir.exists()) {
            destDir.mkdirs();
        }
    }

    public static void main(String[] args) {
        zip("D:\\tmp\\export\\82a7734ef75a4fda890320973fcda5c5", "D:\\tmp\\export\\82a7734ef75a4fda890320973fcda5c5\\export.zip");
//		try {
//			File[] files = unzip("d:\\test\\漢字.zip", "aa");
//			for (int i = 0; i < files.length; i++) {
//				System.out.println(files[i]);
//			}
//		} catch (ZipException e) {
//			e.printStackTrace();
//		}
    }
}
    /*
     * 執行excel導出的線程池
     */
    private ExecutorService execPool = new ThreadPoolExecutor(28, 56, 60L,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<Runnable>(256),
            new ThreadFactoryBuilder().setNameFormat("export-thread-%d").build(),
            new ThreadPoolExecutor.AbortPolicy());


  public void export(CaseInfoBo caseInfoBo, int pageSize, HttpServletResponse response) {
        log.debug("====> 開始導出excel,查詢參數:[{}]", caseInfoBo);
        //查詢最終記錄數量
        Integer count = caseInfoMapper.countCaseList(caseInfoBo);
        //計算需要總共多少頁
        int pageCount = calcPageCount(count, pageSize);
        long startTime = System.currentTimeMillis();
        //創建本次導出的臨時目錄
        String uuid =  UUID.randomUUID().toString().replaceAll("-","");
        String tmpDir = TEMP_ROOT_FOLDER +File.separator+ uuid;
        if(!ensureFolderExist(tmpDir)){
            log.error("創建臨時目錄[{}]失敗!", tmpDir);
            return;
        }
        log.debug("需要導出[{}]條記錄, 分爲[{}]個線程分別導出excel!", count, pageCount);
        //按頁數分配線程執行
        CountDownLatch latch = new CountDownLatch(pageCount);
        for(int i = 0; i < pageCount; i++) {
            execPool.execute(new tcase(i, pageSize, tmpDir, caseInfoBo, latch));
        }
        try {
            latch.await();
        } catch (InterruptedException e) {
            log.error("主線程同步等待線程完成失敗!", e);
            //若出現異常,則清除臨時文件
            clearTempDir(tmpDir);
            return;
        }
        //打壓縮包
        String zipPath = CompressUtil.zip(tmpDir, getZipPath(tmpDir));
        if(zipPath == null) return;
        //發送文件流
        File file = new File(zipPath);
        if (file.exists()) {
            response.setHeader("content-type", "application/octet-stream");
            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition", "attachment;filename=" + "HistoryCase"+uuid+".zip");

            //實現文件流輸出
            byte[] buffer = new byte[1024];
            FileInputStream fis = null;
            BufferedInputStream bis = null;
            try {
                fis = new FileInputStream(file);
                bis = new BufferedInputStream(fis);
                OutputStream os = response.getOutputStream();
                int i = bis.read(buffer);
                while (i != -1) {
                    os.write(buffer, 0, i);
                    i = bis.read(buffer);
                }
                log.info("輸出歷史案件[{}]的文件流[{}]到客戶端成功!", caseInfoBo, zipPath);
            }
            catch (Exception e) {
                log.error("輸出歷史案件[{}]的文件流[{}]到客戶端出現異常!", caseInfoBo, zipPath, e);
            } finally {
                if (bis != null) {
                    try {
                        bis.close();
                    } catch (IOException e) {
                        //
                    }
                }
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException e) {
                        //
                    }
                }
            }
        }
        //清除文件
        clearTempDir(tmpDir);
        long endTime = System.currentTimeMillis();
        log.info("<====執行導出[{}]成功,總時長爲:{}", caseInfoBo, (endTime - startTime)/1000);
    }

    /*
     * 臨時目錄中壓縮文件路徑名
     */
    private String getZipPath(String tmpDir){
        return tmpDir + File.separator + "archive.zip";
    }

    /*
     * 創建下級目錄,已存在或創建成功爲true,不存在且創建不成功爲false
     */
    private boolean ensureFolderExist(String strFolder) {
        File file = new File(strFolder);
        //如果不存在,則創建文件夾
        if (!file.exists()) {
            if (file.mkdirs()) {
                // 創建成功
                return true;
            } else {
                //創建不成功
                return false;
            }
        }
        //目錄已存在返回false
        return false;
    }

    /*
     * 清除臨時目錄
     */
    private void clearTempDir(String dirPath){
        execPool.execute(() -> {
            Logger log = LoggerFactory.getLogger(Thread.currentThread().getName());
            File localDir = new File(dirPath);
            try {
                // 確保存在空的project 文件夾
                if (!localDir.exists()) {
                    return;
                } else {
                    // 清空文件夾
                    // Files.walk - return all files/directories below rootPath including
                    // .sorted - sort the list in reverse order, so the directory itself comes after the including
                    // subdirectories and files
                    // .map - map the Path to File
                    // .peek - is there only to show which entry is processed
                    // .forEach - calls the .delete() method on every File object
                    log.debug("開始清空目錄:{}", dirPath);
                    Files.walk(Paths.get(dirPath)).sorted(Comparator.reverseOrder()).map(Path::toFile)
                            .peek(f -> {log.debug(f.getAbsolutePath());}).forEach(File::delete);
                    log.debug("清空目錄:{} 成功!", dirPath);
                }
            } catch (Exception e) {
                log.error("清空目錄:{}時發生異常!", dirPath, e);
            }
        });
    }

    /**
     * 計算總頁數
     */
    private int calcPageCount(int total, int row){
        return (total-1)/row+1;
    }

    /**
     * 執行查詢記錄和導出excel的方法體
     */
    private class tcase implements Runnable{

        private int page;

        private int size;

        private String dir;

        private CaseInfoBo model;

        private CountDownLatch latch;

        public tcase(int pageNum, int pageSize, String tmpDir, CaseInfoBo _model, CountDownLatch _latch){
            page = pageNum;
            size = pageSize;
            this.dir = tmpDir;
            model = _model;
            latch = _latch;
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            log.debug("==> Thread:{} query and export page:[{}] start", threadName, page);
            Logger log = LoggerFactory.getLogger(threadName);
            try {
                CaseQueryParams finalModel = new CaseQueryParams();
                BeanUtils.copyProperties(finalModel, model);

                finalModel.setPageNo(page);
                finalModel.setPageSize(size);

                List<ExcelCaseInfo> pageList = caseInfoMapper.selectCasePage(finalModel);
                //excel
                String filePath = dir + File.separator + page + EXPORT_SUFFIX;
                EasyExcelUtil.writeWithTemplate(filePath,pageList);

            } catch (Exception e) {
                log.error("查詢[{}]導出頁[{}]出現異常!", model, page, e);
            } finally {
                latch.countDown();
            }
            log.debug("<== Thread:{} query and export page:[{}] end", threadName, page);
        }
    }

總結

選用解決了同時多sheet導入問題,分頁查詢獲取每個sheet的內容,減少了內存處理的數據完美的解決了OOM問題,多文件壓縮打包節省了前端等待的時間,進度條的交互方式完美的解決了用戶體驗感。

備註:不懂EasyExcel的參考https://blog.csdn.net/zhongzk69/article/details/92057268

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章