• 如何在SpringBoot中使用异步方式结合easyExcel实现并行导出Excel功能?
  • 发布于 6天前
  • 43 热度
    0 评论
在SpringBoot应用中,采用同步方式导出Excel文件会导致服务器在生成文件期间阻塞,特别是在处理大量数据时,这种效率较低的方法会严重影响性能。为了解决这个问题,可以采用以下改进措施:首先将导出的数据进行拆分,然后利用CompletableFuture将导出任务异步化。通过easyExcel工具类并行导出多个Excel文件,最后将这些导出完成的文件压缩成ZIP格式,便于用户下载。

相比之下,使用FutureTask实现的多线程导出方法也能达到类似的效果,但CompletableFuture提供了更简洁的API和更强大的功能,更适合现代并发编程的需求。

具体实现:
@RestController
@RequestMapping("/export")
@RequiredArgsConstructor(onConstructor_ = {@Lazy, @Autowired})
public class ExportController {
    private final ExcelExportService excelExportService;

    @GetMapping("/zip")
    public ResponseEntity<byte[]> exportToZip() throws Exception {
        //  创建临时数据
        List<Users> dataList = createDataList(100);
        List<List<Users>> dataSets = ListUtil.partition(dataList, 10);
        List<CompletableFuture<String>> futures = new ArrayList<>();
        //  异步导出所有Excel文件
        String outputDir = "D:\\data";
        for (List<Users> dataSet : dataSets) {
            futures.add(excelExportService.exportDataToExcel(dataSet, outputDir));
        }

        //  等待所有导出任务完成
        CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).get(10, TimeUnit.MINUTES);
        // 堆代码 duidaima.com
        //  收集Excel文件路径
        List<String> excelFilePaths = futures.stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList());

        //  压缩文件
        File zipFile = new File("D:\\data\\output.zip");
        try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile))) {
            for (String filePath : excelFilePaths) {
                zipFile(new File(filePath), zipOut, new File(filePath).getName());
            }
        }
        // 删除临时文件
        for (String filePath : excelFilePaths) {
            File file = new File(filePath);
            file.delete();
        }

        //  返回ZIP文件
        byte[] data = Files.readAllBytes(zipFile.toPath());
        return ResponseEntity.ok()
                .header("Content-Disposition", "attachment;  filename=\"" + zipFile.getName() + "\"")
                .contentType(MediaType.parseMediaType("application/zip"))
                .body(data);
    }

    //  将文件添加到ZIP输出流中
    private void zipFile(File file, ZipOutputStream zipOut, String entryName) throws IOException {
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
            ZipEntry zipEntry = new ZipEntry(entryName);
            zipOut.putNextEntry(zipEntry);
            byte[] bytesIn = new byte[4096];
            int read;
            while ((read = bis.read(bytesIn)) != -1) {
                zipOut.write(bytesIn, 0, read);
            }
            zipOut.closeEntry();
        }
    }

    /**
     * 创建一个包含指定数量随机用户数据的列表。
     *
     * @param count 用户的数量
     * @return 包含指定数量用户的列表
     */
    public static List<Users> createDataList(int count) {
        List<Users> dataList = new ArrayList<>();
        Random random = new Random();
        for (int i = 0; i < count; i++) {
            String userId = "U" + String.format("%03d", i);
            String userName = "用户" + String.format("%03d", i);
            String userAge = String.valueOf(random.nextInt(60) + 18); // 年龄随机在18到77之间
            String userSex = random.nextBoolean() ? "男" : "女";
            String userAddress = "地址" + String.format("%03d", i);
            String userEmail = userName.toLowerCase().replace("用户", "") + "@example.com";
            Users user = new Users(userId, userName, userAge, userSex, userAddress, userEmail);
            dataList.add(user);
        }

        return dataList;
    }
}
异步并行生成excel文件,利用EasyExcel库来简化Excel的生成过程:
@Service
@RequiredArgsConstructor(onConstructor_ = {@Lazy, @Autowired})
public class ExcelExportServiceImpl implements ExcelExportService {

    private final ThreadPoolTaskExecutor taskExecutor;
    @Override
    public CompletableFuture<String> exportDataToExcel(List<Users> dataList, String outputDir) {
        try {
            Path temproaryFilePath = Files.createTempFile(Paths.get(outputDir), "excelFilePre", ".xlsx");
            return CompletableFuture.supplyAsync(() -> {
                try (OutputStream outputStream = new FileOutputStream(temproaryFilePath.toFile())) {
                    EasyExcel.write(outputStream, Users.class).sheet("用户信息").doWrite(dataList);
                    return temproaryFilePath.toString();
                } catch (IOException e) {
                    throw new RuntimeException("Failed to export Excel file", e);
                }
            }, taskExecutor);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
线程池配置:
@Configuration
public class ThreadPoolConfig {
    private static final int corePoolSize = 20;//线程池维护线程的最少数量
    private static final int maximumPoolSize = 50;//线程池维护线程的最大数量

    /**
     * 默认线程池线程池
     *
     * @return Executor
     */
    @Bean("taskExecutor")
    public ThreadPoolTaskExecutor defaultThreadPool() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //核心线程数目
        executor.setCorePoolSize(corePoolSize);
        //指定最大线程数
        executor.setMaxPoolSize(maximumPoolSize);
        //队列中最大的数目
        executor.setQueueCapacity(50);
        //线程名称前缀
        executor.setThreadNamePrefix("defaultThreadPool_");
        //rejection-policy:当pool已经达到max size的时候,如何处理新任务
        //CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
        //对拒绝task的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //线程空闲后的最大存活时间
        executor.setKeepAliveSeconds(60);
        //加载
        executor.initialize();
        return executor;
    }
}

用户评论