从0开始做一个导出功能,完整流程

一、定义导出行DTO

这是最基础的。

参考现成的例子:

java 复制代码
@Data
public class OrgBranchExportRowDTO implements Serializable {

    @ExcelProperty(value = "网点名称")
    private String orgName;

    @ExcelProperty(value = "网点ID")
    private String orgCode;

    @ExcelProperty(value = "网点地址")
    private String address;
}

这里的规则很简单:

  • 一个字段对应Excel一列
  • ExcelProperty(value = "...")就是表头名
  • 字段顺序通常就是导出顺序

二、查询业务数据并组长导出行

不要直接把数据库实体拿去导出,要先转成导出DTO

java 复制代码
List<OrgBranchExportRowDTO> rows = orgIds.stream()
    .map(orgById::get)
    .map(org -> {
        OrgBranchExportRowDTO row = new OrgBranchExportRowDTO();
        row.setOrgName(org.getOrgName());
        row.setOrgCode(org.getOrgCode());
        row.setAddress(org.getAddress());
        return row;
    })
    .toList();

三、创建临时目录

生成文件前,先准备目录

java 复制代码
File materialDirectory = FileUtil.mkdir(
    FileUtil.getTmpDirPath() + File.separator + "org_branch_export_" + UUID.randomUUID()
);

意思是:

  • 放到系统临时目录
  • 每次生成一个唯一目录
  • 避免同名冲突

四、调用GenerateExcelUtil生成文件

简单导出直接用:

java 复制代码
GenerateExcelUtil.exportExcelToFile(rows, sheetName, clazz, materialDirectory, fileName, generateType);

完整实例:

java 复制代码
String fileName = "司机信息报表_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));

GenerateExcelUtil.GenerateType generateType = GenerateExcelUtil.GenerateType.XLSX;

GenerateExcelUtil.exportExcelToFile(
    rows,
    "司机信息报表",
    DriverExportRowDTO.class,
    materialDirectory,
    fileName,
    generateType
);

这一步做完后,磁盘上就已经有文件了。


五、 拿到生成后的文件

可以像现有代码这样封装一个返回对象:

java 复制代码
OrgBranchExportFileDTO exportFile = new OrgBranchExportFileDTO();
exportFile.setFile(FileUtil.file(materialDirectory, fileName + "." + generateType.getExtension()));
exportFile.setFileName(fileName + "." + generateType.getExtension());

如果你不想包DTO,也可以直接:

java 复制代码
File excelFile = FileUtil.file(materialDirectory, fileName + "." + generateType.getExtension());

六、返回给前端下载

如果你是Controller里直接下载,最简单就是:

java 复制代码
GenerateExcelUtil.writeExcelToResponse(excelFile);

他已经帮你做了:

  • 设置附件下载响应头
  • 写出文件
  • 删除临时文件

如果你要多个文件打包下载:

java 复制代码
GenerateExcelUtil.writeZipToResponse(materialDirectory);

java 复制代码
@Slf4j
public class GenerateExcelUtil {

    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public static class ExcelWriteDto<T extends ExcelSheet> {
        File materialDirectory;
        @Builder.Default
        GenerateType generateType = GenerateType.XLSX;
        String excelFileName;
        List<T> data;
        CaseExcelImportContext context;
        List<Double> caseNos;
        Class<T> headerClass;
        List<WriteHandler> writeHandlers;
        @Builder.Default
        List<List<String>> dynamicHeader = new ArrayList<>();
        @Builder.Default
        List<Map<String, Object>> customList = new ArrayList<>();
        @Builder.Default
        List<String> excludeColumnFieldNames = new ArrayList<>();
    }

    @Getter
    @AllArgsConstructor
    public enum GenerateType {
        XLSX("xlsx"), CSV("csv");
        private final String extension;

        public static GenerateType getByExtension(String extension) {
            for (GenerateType value : values()) {
                if (value.extension.equals(extension)) {
                    return value;
                }
            }
            return null;
        }
    }

    public static File getExcelFile(File materialDirectory, String excelFileName, GenerateType generateType) {
        if (StringUtils.isBlank(excelFileName)) {
            excelFileName = "导出信息";
        }
        File parentFile = materialDirectory.getParentFile();
        return FileUtil.file(parentFile, excelFileName + "." + generateType.extension);
    }

    public static <T extends ExcelSheet> File writeToExcelWithCustomDataTo(ExcelWriteDto<T> excelWriteDto) {
        return writeToExcelWithCustomDataTo(excelWriteDto, 1);
    }

    public static <T extends ExcelSheet> File writeToExcelWithCustomDataTo(ExcelWriteDto<T> excelWriteDto, int headRowNum) {
        File materialDirectory = excelWriteDto.getMaterialDirectory();
        String excelFileName = excelWriteDto.getExcelFileName();
        List<T> data = excelWriteDto.getData();
//        CaseExcelImportContext context = excelWriteDto.getContext();
        List<Double> caseNos = excelWriteDto.getCaseNos();
        //按一级表头排序
        List<List<String>> dynamicHeader = excelWriteDto.getDynamicHeader().stream()
                .sorted(Comparator.comparing(List::getFirst))
                .collect(Collectors.toList());

        List<Map<String, Object>> customList = excelWriteDto.getCustomList();
        List<String> excludeColumnFieldNames = excelWriteDto.getExcludeColumnFieldNames();
        Class<T> headerClass = excelWriteDto.getHeaderClass();
//        List<WriteHandler> writeHandlers = excelWriteDto.getWriteHandlers();
        //写入表头
        Map<String, List<List<String>>> head = toList(dynamicHeader, "序号", new HashMap<>(), excludeColumnFieldNames, headerClass);
        List<Integer> excludeColumnIndexes = excludeColumnIndexes(excludeColumnFieldNames, headerClass);
        ExcelWriterSheetBuilder mainSheetBuilder = FesodSheet.writerSheet(SheetNameConstant.EXPORT_INFO)
                .head(head.get("result"))
//                .head(headerClass)
                .excludeColumnIndexes(excludeColumnIndexes)
//                .registerWriteHandler(new HeadCellWriteHandler<>(context)) //头处理器
                .registerWriteHandler(new DynamicHeadCellWriteHandler(headerClass, head.get("excludeResult"), excludeColumnFieldNames, customList, headRowNum)) //动态头处理器
                .registerWriteHandler(new CustomCellMergeStrategy(data.size(), caseNos)) //自定义合并策略
//                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())//自动适配
                .registerConverter(new ExcelBigNumberConvert());// 大数值自动转换 防止失真
        //注册写处理器
//        for (WriteHandler handler : writeHandlers) mainSheetBuilder.registerWriteHandler(handler);

        File excelFile = getExcelFile(materialDirectory, excelFileName, excelWriteDto.getGenerateType());

        try (ExcelWriter excelWriter = FesodSheet.write(excelFile).build()) {
            excelWriter.write(Collections.emptyList(), FesodSheet.writerSheet(SheetNameConstant.OPTIONAL_DATA).build());//写入下拉框选择sheet
            excelWriter.write(data, mainSheetBuilder.build());//写入主Sheet
            Workbook workbook = excelWriter.writeContext().writeWorkbookHolder().getWorkbook();
            workbook.getSheet(SheetNameConstant.EXPORT_INFO).createFreezePane(0, headRowNum);//冻结表头
            Sheet optionalData = workbook.getSheet(SheetNameConstant.OPTIONAL_DATA);
            optionalData.protectSheet("kxsjLock");
            workbook.setSheetHidden(workbook.getSheetIndex(optionalData), true);//隐藏下拉选择Sheet
        }

        return excelFile;
    }

    private static Map<String, List<List<String>>> toList(List<List<String>> dynamicHeader, String noColName
            , Map<String, String> headRenameMap, List<String> excludeFields, Class<? extends ExcelSheet> headerClass) {
        Map<String, List<List<String>>> resultMap = new HashMap<>();
        Field[] fields = ReflectUtil.getFields(headerClass);
        //初始化一个fields长度的集合
        List<List<String>> result = new ArrayList<>();
        while (result.size() != fields.length) result.add(null);
        List<List<String>> excludeResult = new ArrayList<>(result);

        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
            if (Objects.nonNull(excelProperty)) {
                int index = excelProperty.index();
                List<String> value = Arrays.stream(excelProperty.value()).collect(Collectors.toList());
                // 自定义序号表头不为空,替换序号列表头
                if (StringUtils.isNotBlank(noColName) && "no".equals(field.getName())) {
                    value.set(0, noColName);
                }
                if (CollUtil.isNotEmpty(headRenameMap)
                        && value.size() >= 2
                        && headRenameMap.containsKey(value.get(1))) {
                    value.set(1, headRenameMap.get(value.get(1)));
                }
                //如果注解设置了index,设置index的值
                if (index != -1) {
                    result.add(index, value);
                } else {
                    result.add(i, value);
                }
                result.remove(null);
                //排除字段后结果
                if (!excludeFields.contains(field.getName())) {
                    if (index != -1) {
                        excludeResult.add(index, value);
                    } else {
                        excludeResult.add(i, value);
                    }
                    excludeResult.remove(null);
                }
            }
        }
        if (!CollectionUtils.isEmpty(dynamicHeader)) {
            result.addAll(dynamicHeader);
            excludeResult.addAll(dynamicHeader);
        }
        resultMap.put("result", result);
        resultMap.put("excludeResult", excludeResult);
        return resultMap;
    }

    private static List<Integer> excludeColumnIndexes(List<String> excludeColumnFieldNames, Class<? extends ExcelSheet> headerClass) {
        Field[] fields = ReflectUtil.getFields(headerClass);
        //初始化一个fields长度的集合
        List<String> result = new ArrayList<>();
        while (result.size() != fields.length) result.add(null);

        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
            if (Objects.nonNull(excelProperty)) {
                int index = excelProperty.index();
                // 自定义序号表头不为空,替换序号列表头
                //如果注解设置了index,设置index的值
                if (index != -1) {
                    result.add(index, field.getName());
                } else {
                    result.add(i, field.getName());
                }
                result.remove(null);
            }
        }
        return excludeColumnFieldNames.stream()
                .map(result::indexOf)
                .collect(Collectors.toList());
    }

    public static void writeZipToResponse(File materialDirectory) {
        File zip = null;
        File parentFile = null;
        try {
            parentFile = materialDirectory.getParentFile();
            //压缩目录并写回响应体
            zip = ZipUtil.zip(parentFile);

            FileUtil.del(materialDirectory);

            HttpServletResponse response = ServletUtils.getResponse();
            FileUtils.setAttachmentResponseHeader(response, zip.getName());
            response.addHeader("Content-Length", "" + zip.length());
            response.addHeader("Access-Control-Allow-Origin", "*");

            JakartaServletUtil.write(response, zip);
        } catch (Exception e) {
            log.error("文件下载失败: " + (zip != null ? zip.getName() : "未知文件"), e);
            AssertUtil.throwException("文件下载失败");
        } finally {
            FileUtil.del(zip);
            FileUtil.del(parentFile);
        }
    }

    public static void writeExcelToResponse(File excelFile) {
        if (excelFile == null || !excelFile.exists()){
            AssertUtil.throwException("excel文件不存在");
        }
        try {
            HttpServletResponse response = ServletUtils.getResponse();
            FileUtils.setAttachmentResponseHeader(response, excelFile.getName());
            response.addHeader("Content-Length", "" + excelFile.length());
            response.addHeader("Access-Control-Allow-Origin", "*");

            JakartaServletUtil.write(response, excelFile);
        } catch (Exception e) {
            log.error("文件下载失败: " + excelFile.getName(), e);
            AssertUtil.throwException("文件下载失败");
        } finally {
            FileUtil.del(excelFile);
        }
    }


    /**
     * 导出 Excel 到指定文件路径
     *
     * @param list              导出数据集合
     * @param sheetName         工作表的名称
     * @param clazz             实体类
     * @param materialDirectory 目标文件路径
     * @param merge             是否合并单元格
     */
    public static <T> File exportExcelToFile(List<T> list, String sheetName, Class<T> clazz, File materialDirectory, String excelFileName, boolean merge, GenerateType generateType) {
        if (StringUtils.isBlank(excelFileName)) {
            excelFileName = "案件信息";
        }
        File excelFile = FileUtil.file(materialDirectory, excelFileName + "." + generateType.getExtension());
        ExcelWriterSheetBuilder builder = FesodSheet.write(excelFile, clazz)
//                .autoCloseStream(false)
                // 自动适配
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                // 大数值自动转换 防止失真
                .registerConverter(new ExcelBigNumberConvert())
                .sheet(sheetName);
        if (merge) {
            // 合并处理器
            builder.registerWriteHandler(new CellMergeStrategy(list, true));
        }
        builder.doWrite(list);

        return excelFile;
    }

    /**
     * 导出 Excel 到指定文件(默认不合并单元格)
     *
     * @param list              导出数据集合
     * @param sheetName         工作表的名称
     * @param clazz             实体类
     * @param materialDirectory 目标文件路径
     */
    public static <T> File exportExcelToFile(List<T> list, String sheetName, Class<T> clazz, File materialDirectory, String excelFileName, GenerateType generateType) {
        return exportExcelToFile(list, sheetName, clazz, materialDirectory, excelFileName, false, generateType);
    }
}
相关推荐
java1234_小锋2 小时前
SpringBoot可以同时处理多少请求?
java·spring boot·后端
海棠Flower未眠3 小时前
Spring Boot 3 + JPA多模块系统对MySQL和DORIS进行多数据源集成实战(荣耀典藏版)
spring boot·后端·mysql
北风朝向3 小时前
Spring Boot 集成 Open WebUI 实现 AI 流式对话
人工智能·spring boot·状态模式
海棠Flower未眠4 小时前
Spring Boot 2.4后,特定配置文件不能再使用spring.profiles.include的解决思路
数据库·spring boot·spring
C雨后彩虹4 小时前
SpringBoot整合Redis String,全套原生API讲解,覆盖80%缓存业务场景
java·数据结构·spring boot·redis·string
加藤不太惠5 小时前
SpringBoot + MinIO 实现大文件秒传 + 断点续传 + 分片上传
spring boot·后端·minio分片
Devin~Y5 小时前
电商AIGC智能客服面试:JVM调优、Spring Cloud微服务、Redis缓存、Kafka消息、K8s观测与RAG落地
java·jvm·spring boot·redis·spring cloud·kafka·kubernetes
Boop_wu5 小时前
[Java EE 进阶] SpringBoot 统一功能处理全解:拦截器、统一返回、统一异常
java·spring boot·java-ee