update 增加大数据量Excel导出功能,支持多线程处理并打包为ZIP
This commit is contained in:
+145
-7
@@ -1,8 +1,11 @@
|
||||
package org.dromara.common.excel.utils;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import cn.hutool.core.io.resource.ClassPathResource;
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.ZipUtil;
|
||||
import jakarta.servlet.ServletOutputStream;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AccessLevel;
|
||||
@@ -20,14 +23,17 @@ import org.dromara.common.excel.convert.ExcelBigNumberConvert;
|
||||
import org.dromara.common.excel.core.*;
|
||||
import org.dromara.common.excel.handler.DataWriteHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.io.*;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* Excel相关处理
|
||||
@@ -93,6 +99,138 @@ public class ExcelUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 大数据量Excel导出
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
*/
|
||||
public static <T> void exportExcelZip(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response) {
|
||||
exportExcelZip(list, sheetName, clazz, response, 999);
|
||||
}
|
||||
|
||||
/**
|
||||
* 大数据量Excel导出
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param pageSize 每页条数
|
||||
*/
|
||||
public static <T> void exportExcelZip(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response, int pageSize) {
|
||||
// 数据分页
|
||||
List<List<T>> pageList = ListUtil.partition(list, pageSize);
|
||||
// 只有一页,直接导出普通Excel
|
||||
if (pageList.size() <= 1) {
|
||||
exportSingleExcel(list, sheetName, clazz, response);
|
||||
return;
|
||||
}
|
||||
// 多线程生成所有Excel文件(字节数组)
|
||||
Map<String, byte[]> excelMap = buildExcelZipData(pageList, sheetName, clazz);
|
||||
// 写入ZIP并下载
|
||||
writeExcelZipResponse(sheetName, response, excelMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出【单文件】Excel
|
||||
*/
|
||||
private static <T> void exportSingleExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response) {
|
||||
try {
|
||||
resetResponse(sheetName, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportExcel(list, sheetName, clazz, false, os, null);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多线程并行生成多个Excel文件
|
||||
* 使用虚拟线程,高并发、低资源占用
|
||||
*
|
||||
* @return Map<文件名, 文件字节数组>
|
||||
*/
|
||||
private static <T> Map<String, byte[]> buildExcelZipData(List<List<T>> pageList, String sheetName, Class<T> clazz) {
|
||||
// 有序Map,保证文件按页码顺序打包
|
||||
Map<String, byte[]> excelMap = new LinkedHashMap<>(pageList.size());
|
||||
List<Future<Map.Entry<String, byte[]>>> futures = new ArrayList<>(pageList.size());
|
||||
|
||||
// 使用虚拟线程池执行导出任务
|
||||
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
|
||||
// 1. 提交所有分页导出任务
|
||||
for (int i = 0; i < pageList.size(); i++) {
|
||||
int pageNum = i + 1;
|
||||
List<T> pageData = pageList.get(i);
|
||||
futures.add(executor.submit(() -> buildExcelZipEntry(pageData, sheetName, clazz, pageNum)));
|
||||
}
|
||||
// 2. 获取所有线程执行结果
|
||||
for (Future<Map.Entry<String, byte[]>> future : futures) {
|
||||
Map.Entry<String, byte[]> excel = getExcelZipEntry(future);
|
||||
excelMap.put(excel.getKey(), excel.getValue());
|
||||
}
|
||||
}
|
||||
return excelMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单页Excel生成任务(线程执行单元)
|
||||
*
|
||||
* @param pageData 当前页数据
|
||||
* @param pageNum 当前页码
|
||||
* @return 文件名 + 文件字节
|
||||
*/
|
||||
private static <T> Map.Entry<String, byte[]> buildExcelZipEntry(List<T> pageData, String sheetName, Class<T> clazz, int pageNum) {
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
||||
String exportSheetName = sheetName + "_第" + pageNum + "页";
|
||||
exportExcel(pageData, exportSheetName, clazz, false, bos, null);
|
||||
return Map.entry(exportSheetName + ".xlsx", bos.toByteArray());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("第" + pageNum + "页Excel生成失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取异步任务结果
|
||||
* 处理中断异常、执行异常,保证任务稳定
|
||||
*/
|
||||
private static Map.Entry<String, byte[]> getExcelZipEntry(Future<Map.Entry<String, byte[]>> future) {
|
||||
try {
|
||||
return future.get();
|
||||
} catch (InterruptedException e) {
|
||||
// 恢复中断标志
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("Excel导出线程被中断", e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException("Excel导出失败", e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将多个Excel文件打包成ZIP并输出到浏览器下载
|
||||
*
|
||||
* @param excelMap 文件名 -> 文件字节
|
||||
*/
|
||||
private static void writeExcelZipResponse(String sheetName, HttpServletResponse response, Map<String, byte[]> excelMap) {
|
||||
try {
|
||||
// 设置ZIP下载响应头
|
||||
response.setContentType("application/zip");
|
||||
response.setHeader("Content-Disposition",
|
||||
"attachment;filename*=UTF-8''" + URLEncoder.encode(sheetName, StandardCharsets.UTF_8) + ".zip");
|
||||
|
||||
// 压缩写入多个Excel文件
|
||||
try (ZipOutputStream zipOut = ZipUtil.getZipOutputStream(response.getOutputStream(), CharsetUtil.CHARSET_UTF_8)) {
|
||||
for (Map.Entry<String, byte[]> entry : excelMap.entrySet()) {
|
||||
zipOut.putNextEntry(new ZipEntry(entry.getKey()));
|
||||
zipOut.write(entry.getValue());
|
||||
zipOut.closeEntry();
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Zip异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user