使用EasyExcel异步导出excel技术方案

发布时间 2023-07-27 10:07:19作者: 咖啡来一杯
  1. 主线程:处理请求响应,同时开启子线程,让子线程处理导出任务
  2. 子线程:将导出的文件写入到磁盘临时文件,临时文件上传到oss中获取上传文件的url路径,记录url路径到数据库中,最后再删除临时文件
  3. 通过单独一个页面查询导出文件流水的列表,进行下载文件

代码实现

线程池配置

@Component
public class ExcelExportThreadPoolConfig {

    private static final int CPU_SIZE = Runtime.getRuntime().availableProcessors();

    @Bean(value = "exportThreadPoolExecutor")
    public ThreadPoolExecutor poolExecutorSetting() {

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            CPU_SIZE * 2,
            CPU_SIZE * 2,
            3,
            TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(20),
            new CustomizableThreadFactory("文件导出线程池")
        );

        threadPoolExecutor.prestartAllCoreThreads();
        return threadPoolExecutor;
    }
}

controller层

/**
     * easyExcel异步导出通用案例2
     *
     * @param response
     * @return
     */
@GetMapping("asyncExportFileFlow")
public String asyncExportFileFlow(HttpServletResponse response) {

    CompletableFuture.runAsync(() -> {

        this.studentService.asyncExportFileFlow(response);

    }, exportThreadPoolExecutor).exceptionally(e -> {
        log.error("导出文件失败", e);
        return null;
    });

    return "ok";

}

service层

@Override
public void exportUserData(HttpServletResponse response) {

    List<Map<String, Object>> data = this.sysUserMapper.list();
    if (data.isEmpty()) {
        throw new RuntimeException("数据为空");
    }

    recordFileFlow(response, data, ExcelExportEnum.USER_EXCEL);

}

/**
     * 记录导出文件的流水
     *
     * @param response
     * @param data
     * @param excelExportEnum
     */
private void recordFileFlow(HttpServletResponse response,
                            List<Map<String, Object>> data,
                            ExcelExportEnum excelExportEnum) {

    ExportFileFlow exportFileFlow = new ExportFileFlow();
    exportFileFlow.setFileName(excelExportEnum.getFilename());
    exportFileFlow.setCreateTime(LocalDateTime.now());
    exportFileFlow.setOperateUser("系统用户");
    this.exportFileFlowMapper.insert(exportFileFlow);

    long beginTime = System.currentTimeMillis();
    try {
        String fileUrl = this.exportTemplate.uploadFileToOss(response, excelExportEnum, data);
        long endTime = System.currentTimeMillis();
        exportFileFlow.setExportStatus(1);
        exportFileFlow.setExportConsumeTime((endTime - beginTime) / 1000);
        exportFileFlow.setFileUrl(fileUrl);
    } catch (Exception e) {
        exportFileFlow.setExportStatus(2);
        exportFileFlow.setFailReason(e.toString());
        exportFileFlow.setCreateTime(null);
        throw new RuntimeException(e);
    } finally {
        this.exportFileFlowMapper.update(exportFileFlow);
    }
}
public String uploadFileToOss(HttpServletResponse response,
                              ExcelExportEnum excelExportEnum,
                              List<Map<String, Object>> writeData) throws IOException {

    BufferedOutputStream tempOutStream = null;
    BufferedInputStream tempInStream = null;
    Path newFilePath = null;
    try {

        // 1.加载类路径下的模板
        ClassPathResource classPathResource = new ClassPathResource(excelExportEnum.getFileClassPath());
        String originalFileName = classPathResource.getFilename();
        if (StringUtils.isEmpty(originalFileName)) {
            throw new RuntimeException("文件名不能为空");
        }

        // 2.创建临时目录
        File file = new File(DISK_TEMPORARY_LOCATION);
        // 目录不存在则新建,如果新建失败则终止
        boolean createDiskDirFail = !file.exists() && !file.mkdirs();
        if (createDiskDirFail) {
            throw new RuntimeException("临时文件目录创建失败");
        }
        String fileSuffix = originalFileName.substring(originalFileName.lastIndexOf("."));
        newFilePath = Paths.get(file.getPath() + UUID.randomUUID() + fileSuffix);
        // 使用缓冲流,加快读写
        tempOutStream = new BufferedOutputStream(Files.newOutputStream(newFilePath));

        // 3.往模板中写数据,并将写好的文件写入到磁盘临时文件
        ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream())
            // 用于指定生成 Excel 文件的路径
            .file(tempOutStream)
            // 指定模板位置
            .withTemplate(classPathResource.getInputStream()).build();
        WriteSheet writeSheet = EasyExcel.writerSheet().build();
        FillConfig fillConfig = FillConfig.builder().build();
        excelWriter.fill(new FillWrapper("data", writeData), fillConfig, writeSheet);
        excelWriter.finish();

        // 4.上传oss
        tempInStream = new BufferedInputStream(Files.newInputStream(newFilePath));
        String fileUrl = this.aliOssTemplate.uploadFile(tempInStream, classPathResource.getFilename());
        log.info("文件上传成功,地址:{}", fileUrl);
        return fileUrl;

    } finally {
        // 5.最后再删掉磁盘上临时文件
        assert newFilePath != null;
        Files.delete(newFilePath);
        assert tempOutStream != null;
        tempOutStream.close();
        assert tempInStream != null;
        tempInStream.close();
    }

}
public String uploadFile(InputStream inputStream, String originalFileName) {

    if (StringUtils.isEmpty(originalFileName)) {
        throw new RuntimeException("文件名不能为空");
    }

    // 1.重命名文件,oss目录是 2021-9-10/uuid.文件后缀
    String fileSuffix = originalFileName.substring(originalFileName.lastIndexOf("."));
    String newFileName = UUID.randomUUID().toString().replace("-", "");
    String renameFileName = LocalDate.now() + "/" + newFileName + fileSuffix;

    // 2.文件上传
    OSS ossClient = null;
    try {
        ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, renameFileName, inputStream);
        ossClient.putObject(putObjectRequest);
    } finally {
        assert ossClient != null;
        ossClient.shutdown();
    }

    // 3.组装获取返回的url
    //https://classroomsys.oss-cn-beijing.aliyuncs.com/objectdir/a.png
    return "https://" + bucketName + "." + endpoint + "/" + renameFileName;
}
CREATE TABLE `export_file_flow`  (
    `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT 'id',
    `file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '文件名',
    `file_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件url',
    `export_status` int(0) NULL DEFAULT 2 COMMENT '导出状态:1处理中,2处理成功,3处理失败',
    `fail_reason` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '导出失败原因',
    `export_consume_time` bigint(0) NULL DEFAULT NULL COMMENT '导出耗时(秒)',
    `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
    `operate_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '操作人',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 244 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '文件下载列表' ROW_FORMAT = Dynamic;