【java】基于hutool实现.Excel导出任意多级自定义表头数据和支持多sheet导出

该文章已生成可运行项目,

基于hutool实现.Excel导出多级表头数据

import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import org.apache.poi.ss.usermodel.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

public class ExcelUtils {

/**
     * 导出指定多级表头的 Excel
     * @param topLine  顶部插入数据,可以为null
     * @param headers  表头,支持无限级
     * @param dataList  要导出的数据
     * @param response
     * @throws IOException
     */
    public static void downloadExcelMoreHeaders(List<String> topLine,List<ExcelHeader> headers,
                                                List<HashMap<String, Object>> dataList,
                                                HttpServletResponse response,
                                                String fileName) throws IOException {
        ExcelWriter writer = null;
        try {
            writer = ExcelUtil.getWriter(true);
            Sheet sheet = writer.getSheet();


           int startRow = 0;
            if (topLine != null && !topLine.isEmpty()) {
                startRow = 1;
                Row topRow = sheet.createRow(0);
                for (int i = 0; i < topLine.size(); i++) {
                    Cell cell = topRow.createCell(i);
                    cell.setCellValue(topLine.get(i));
                    // 居中样式
                    CellStyle style = writer.getWorkbook().createCellStyle();
                    style.setAlignment(HorizontalAlignment.CENTER);
                    cell.setCellStyle(style);
                }
            }

            // 最大表头层级
            int maxLevel = getMaxHeaderLevel(headers);

            // 递归构建表头(按叶子节点列数自动合并)
            java.util.concurrent.atomic.AtomicInteger colIndex = new java.util.concurrent.atomic.AtomicInteger(0);
            buildHeaderRecursive(writer, sheet, headers, startRow, colIndex, startRow + maxLevel);

            // 收集所有最终字段
            List<String> fieldOrder = collectAllLeafFields(headers);

            // 写入数据
            List<List<Object>> dataRows = new ArrayList<>();
            for (HashMap<String, Object> data : dataList) {
                dataRows.add(fieldOrder.stream()
                        .map(field -> data.getOrDefault(field, ""))
                        .collect(Collectors.toList()));
            }

            writer.passRows(startRow + maxLevel);
            writer.write(dataRows, false);

            // 数据样式
            CellStyle dataStyle = createDataCellStyle(writer.getWorkbook());
            for (int rowNum = startRow + maxLevel; rowNum <= sheet.getLastRowNum(); rowNum++) {
                Row row = sheet.getRow(rowNum);
                if (row != null) {
                    for (Cell cell : row) {
                        if (cell != null) {
                            cell.setCellStyle(dataStyle);
                        }
                    }
                }
            }

            // ==================== 列宽计算(修复不越界) ====================
            int colCount = 0;
            for (int rowNum = 0; rowNum <= sheet.getLastRowNum(); rowNum++) {
                Row row = sheet.getRow(rowNum);
                if (row != null) {
                    colCount = Math.max(colCount, row.getLastCellNum());
                }
            }
            colCount = colCount == 0 ? 100 : colCount;

            float chineseRatio = 2.0f;
            float bufferWidth = 3.0f;
            int maxWidth = 50;
            int[] maxColWidths = new int[colCount];

            int checkRows = Math.min(sheet.getLastRowNum(), 1000);
            for (int rowNum = 0; rowNum <= checkRows; rowNum++) {
                Row row = sheet.getRow(rowNum);
                if (row == null) continue;

                int cellNum = row.getLastCellNum();
                for (int j = 0; j < cellNum; j++) {
                    Cell cell = row.getCell(j);
                    if (cell == null) continue;

                    String text = getCellStringValue(cell);
                    int width = getMaxLineWidth(text, chineseRatio);
                    if (width > maxColWidths[j]) {
                        maxColWidths[j] = width;
                    }
                }
            }

            for (int j = 0; j < colCount; j++) {
                sheet.setColumnWidth(j, Math.min((int) (maxColWidths[j] + bufferWidth) * 256, maxWidth * 256));
            }

            // 输出
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
            response.setHeader("Content-Disposition",
                    "attachment;filename*=UTF-8''" +
                            URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20") +
                            ".xlsx");
            writer.flush(response.getOutputStream(), true);

        } finally {
            if (writer != null) writer.close();
            try {
                if (response.getOutputStream() != null) response.getOutputStream().close();
            } catch (Exception ignored) {}
        }
    }

    // ==================== 核心递归:构建表头(自动合并正确列数) ====================
    private static void buildHeaderRecursive(ExcelWriter writer, Sheet sheet,
                                            List<ExcelHeader> headers,
                                            int row, AtomicInteger colIndex, int maxLevel) {
        for (ExcelHeader header : headers) {
            int currentCol = colIndex.get();
            List<ExcelHeader> children = header.getChildren();

            if (children == null || children.isEmpty()) {
                // 叶子节点
                int rowSpan = maxLevel - row;
                if (rowSpan > 1) {
                    writer.merge(row, row + rowSpan - 1, currentCol, currentCol, header.getTitle(), true);
                } else {
                    writer.writeCellValue(currentCol, row, header.getTitle());
                }
                applyHeaderStyle(writer, currentCol, row);
                colIndex.incrementAndGet();
            } else {
                // 父节点:合并列数 = 底层真实叶子列数
                int span = countLeafColumns(children);
                writer.merge(row, row, currentCol, currentCol + span - 1, header.getTitle(), true);
                applyHeaderStyle(writer, currentCol, row);

                // 递归子节点
                buildHeaderRecursive(writer, sheet, children, row + 1, colIndex, maxLevel);
            }
        }
    }

    // ==================== 工具方法 ====================
    private static int countLeafColumns(List<ExcelHeader> headers) {
        int count = 0;
        for (ExcelHeader h : headers) {
            if (h.getChildren() == null || h.getChildren().isEmpty()) count++;
            else count += countLeafColumns(h.getChildren());
        }
        return count;
    }

    private static int getMaxHeaderLevel(List<ExcelHeader> headers) {
        int max = 1;
        for (ExcelHeader h : headers) {
            if (h.getChildren() != null && !h.getChildren().isEmpty()) {
                int level = getMaxHeaderLevel(h.getChildren()) + 1;
                max = Math.max(max, level);
            }
        }
        return max;
    }

    private static List<String> collectAllLeafFields(List<ExcelHeader> headers) {
        List<String> list = new ArrayList<>();
        for (ExcelHeader h : headers) {
            if (h.getChildren() == null || h.getChildren().isEmpty()) list.add(h.getFieldName());
            else list.addAll(collectAllLeafFields(h.getChildren()));
        }
        return list;
    }

    private static int getRealColumnCount(Sheet sheet) {
        int count = 0;
        for (int i = 0; i <= sheet.getLastRowNum(); i++) {
            Row row = sheet.getRow(i);
            if (row != null) count = Math.max(count, row.getLastCellNum());
        }
        return Math.max(count, 100);
    }

    private static int[] calculateColumnWidths(Sheet sheet, int colCount, float chineseRatio) {
        int[] widths = new int[colCount];
        int checkRows = Math.min(sheet.getLastRowNum(), 1000);
        for (int i = 0; i <= checkRows; i++) {
            Row row = sheet.getRow(i);
            if (row == null) continue;
            for (int j = 0; j < colCount; j++) {
                Cell cell = row.getCell(j);
                if (cell == null) continue;
                String text = getCellStringValue(cell);
                int w = getMaxLineWidth(text, chineseRatio);
                if (w > widths[j]) widths[j] = w;
            }
        }
        return widths;
    }

    private static CellStyle createDataCellStyle(Workbook wb) {
        CellStyle style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setWrapText(true);
        return style;
    }

    private static void applyHeaderStyle(ExcelWriter writer, int col, int row) {
        CellStyle style = writer.getWorkbook().createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        writer.setStyle(style, col, row);
    }

    private static String getCellStringValue(Cell cell) {
        if (cell == null) return "";
        switch (cell.getCellType()) {
            case STRING: return cell.getStringCellValue();
            case NUMERIC: return String.valueOf(cell.getNumericCellValue());
            case BOOLEAN: return String.valueOf(cell.getBooleanCellValue());
            default: return "";
        }
    }

    private static int getMaxLineWidth(String text, float chineseRatio) {
        if (text == null || text.isEmpty()) return 0;
        int max = 0;
        for (String line : text.split("\n")) {
            int w = 0;
            for (char c : line.toCharArray()) {
                w += (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS) ? chineseRatio : 1;
            }
            max = Math.max(max, w);
        }
        return max;
    }
}

配套 ExcelHeader.java

import lombok.Data;
import java.util.List;

@Data
public class ExcelHeader {
    private String title;
    private String fieldName;
    private List<ExcelHeader> children;
}

必须依赖(pom.xml)

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.1.2</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.1.2</version>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.10</version>
</dependency>

调用案例

  public static void export(HttpServletResponse response) throws Exception {
        // ===================== 1. 构建 3级表头 =====================
        List<ExcelHeader> headers = new ArrayList<>();

        // 第1行:大标题
        ExcelHeader title = new ExcelHeader();
        title.setTitle("学生成绩统计表");
        title.setChildren(new ArrayList<>());
        headers.add(title);

        // 第2行:分组
        ExcelHeader group1 = new ExcelHeader();
        group1.setTitle("学生信息");
        group1.setChildren(new ArrayList<>());
        title.getChildren().add(group1);

        ExcelHeader group2 = new ExcelHeader();
        group2.setTitle("成绩信息");
        group2.setChildren(new ArrayList<>());
        title.getChildren().add(group2);

        // 第3行:真实列(子集)
        group1.getChildren().add(createHeader("姓名", "name"));
        group1.getChildren().add(createHeader("班级", "className"));

        group2.getChildren().add(createHeader("语文", "chinese"));
        group2.getChildren().add(createHeader("数学", "math"));
        group2.getChildren().add(createHeader("英语", "english"));

        // ===================== 2. 测试数据 =====================
        List<HashMap<String, Object>> dataList = new ArrayList<>();

        HashMap<String, Object> row1 = new HashMap<>();
        row1.put("name", "张三");
        row1.put("className", "高三1班");
        row1.put("chinese", 125);
        row1.put("math", 143);
        row1.put("english", 130);
        dataList.add(row1);

        HashMap<String, Object> row2 = new HashMap<>();
        row2.put("name", "李四");
        row2.put("className", "高三1班");
        row2.put("chinese", 118);
        row2.put("math", 136);
        row2.put("english", 127);
        dataList.add(row2);

        // ===================== 3. 导出 =====================
        ExcelUtils.downloadExcelMoreHeaders(null,headers, dataList, response, "学生成绩表");
    }

    // 快速创建列
    private static ExcelHeader createHeader(String title, String fieldName) {
        ExcelHeader h = new ExcelHeader();
        h.setTitle(title);
        h.setFieldName(fieldName);
        return h;
    }

实现导出多sheet 合并的数据文档

一、基于downloadExcelMoreHeaders方法,创建一个包含单个Sheet的Workbook的方法

    /**
     * 创建一个包含单个Sheet的Workbook(不下载,只返回对象)
     */
    public static Workbook createSingleSheetBook(
            List<String> topLine,
            List<ExcelHeader> headers,
            List<HashMap> dataList,
            String sheetName
    ) {
        // 直接用POI创建,完美支持设置sheet名称
        Workbook workbook = new XSSFWorkbook();
        // 第二个参数直接传字符串 sheetName
        ExcelWriter writer = new ExcelWriter(workbook, sheetName);
        Sheet sheet = writer.getSheet();
        // ========== 你的原有逻辑 完全不动 ==========
        int startRow = 0;
        if (topLine != null && !topLine.isEmpty()) {
            startRow = 1;
            Row topRow = sheet.createRow(0);
            for (int i = 0; i < topLine.size(); i++) {
                Cell cell = topRow.createCell(i);
                cell.setCellValue(topLine.get(i));
                CellStyle style = writer.getWorkbook().createCellStyle();
                style.setAlignment(HorizontalAlignment.CENTER);
                cell.setCellStyle(style);
            }
        }

        int maxLevel = getMaxHeaderLevel(headers);
        java.util.concurrent.atomic.AtomicInteger colIndex = new java.util.concurrent.atomic.AtomicInteger(0);
        buildHeaderRecursive(writer, sheet, headers, startRow, colIndex, startRow + maxLevel);

        List<String> fieldOrder = collectAllLeafFields(headers);
        List<List<Object>> dataRows = new ArrayList<>();
        for (HashMap<String, Object> data : dataList) {
            dataRows.add(fieldOrder.stream()
                    .map(field -> data.getOrDefault(field, ""))
                    .collect(Collectors.toList()));
        }

        writer.passRows(startRow + maxLevel);
        writer.write(dataRows, false);

        CellStyle dataStyle = createDataCellStyle(writer.getWorkbook());
        for (int rowNum = startRow + maxLevel; rowNum <= sheet.getLastRowNum(); rowNum++) {
            Row row = sheet.getRow(rowNum);
            if (row != null) {
                for (Cell cell : row) {
                    if (cell != null) {
                        cell.setCellStyle(dataStyle);
                    }
                }
            }
        }

        // 自动列宽(你的逻辑)
        int colCount = 0;
        for (int rowNum = 0; rowNum <= sheet.getLastRowNum(); rowNum++) {
            Row row = sheet.getRow(rowNum);
            if (row != null) {
                colCount = Math.max(colCount, row.getLastCellNum());
            }
        }
        colCount = colCount == 0 ? 100 : colCount;

        float chineseRatio = 2.0f;
        float bufferWidth = 3.0f;
        int maxWidth = 50;
        int[] maxColWidths = new int[colCount];

        int checkRows = Math.min(sheet.getLastRowNum(), 1000);
        for (int rowNum = 0; rowNum <= checkRows; rowNum++) {
            Row row = sheet.getRow(rowNum);
            if (row == null) continue;

            int cellNum = row.getLastCellNum();
            for (int j = 0; j < cellNum; j++) {
                Cell cell = row.getCell(j);
                if (cell == null) continue;

                String text = getCellStringValue(cell);
                int width = getMaxLineWidth(text, chineseRatio);
                if (width > maxColWidths[j]) {
                    maxColWidths[j] = width;
                }
            }
        }

        for (int j = 0; j < colCount; j++) {
            sheet.setColumnWidth(j, Math.min((int) (maxColWidths[j] + bufferWidth) * 256, maxWidth * 256));
        }

        // 关键:只返回 workbook,不关闭、不下载
        return writer.getWorkbook();
    }

二、合并多个workbook并下载

    /**
     * 合并多个 workbook 并输出下载(每个 workbook 取第一个sheet)
     */
    public static void mergeAndDownload(
            HttpServletResponse response,
            String fileName,
            Workbook... workbooks
    ) throws IOException {

        Workbook finalWb = new XSSFWorkbook();

        for (Workbook wb : workbooks) {
            if (wb == null) continue;
            Sheet sourceSheet = wb.getSheetAt(0);
            String sheetName = sourceSheet.getSheetName();

            Sheet targetSheet = finalWb.createSheet(sheetName);

            // 复制行、单元格、样式
            for (int rowNum = 0; rowNum <= sourceSheet.getLastRowNum(); rowNum++) {
                Row sourceRow = sourceSheet.getRow(rowNum);
                Row targetRow = targetSheet.createRow(rowNum);
                if (sourceRow == null) continue;

                for (int cellNum = 0; cellNum < sourceRow.getLastCellNum(); cellNum++) {
                    Cell sourceCell = sourceRow.getCell(cellNum);
                    Cell targetCell = targetRow.createCell(cellNum);
                    if (sourceCell == null) continue;

                    // 复制值
                    switch (sourceCell.getCellType()) {
                        case STRING: targetCell.setCellValue(sourceCell.getStringCellValue()); break;
                        case NUMERIC: targetCell.setCellValue(sourceCell.getNumericCellValue()); break;
                        case BOOLEAN: targetCell.setCellValue(sourceCell.getBooleanCellValue()); break;
                        default: targetCell.setCellValue(sourceCell.toString());
                    }

                    // 复制样式
                    CellStyle newStyle = finalWb.createCellStyle();
                    newStyle.cloneStyleFrom(sourceCell.getCellStyle());
                    targetCell.setCellStyle(newStyle);
                }
            }

            // 复制列宽
            for (int i = 0; i < sourceSheet.getRow(0).getLastCellNum(); i++) {
                targetSheet.setColumnWidth(i, sourceSheet.getColumnWidth(i));
            }
            // ========== 3.复制【合并单元格】 ==========
            for (int i = 0; i < sourceSheet.getNumMergedRegions(); i++) {
                CellRangeAddress region = sourceSheet.getMergedRegion(i);
                targetSheet.addMergedRegion(region);
            }
        }

        // 下载
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        response.setHeader("Content-Disposition",
                "attachment;filename*=UTF-8''" +
                        URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20") + ".xlsx");

        finalWb.write(response.getOutputStream());
        finalWb.close();
        response.getOutputStream().close();
    }

三、调用方式

Workbook wb1 = ExcelUtils.createSingleSheetBook(
         null, headers1,
        data1,
         "表1"
 );

 Workbook wb2 = ExcelUtils.createSingleSheetBook(
         null, headers2,
         data2,
         "表2"
 );

 // ====================== 合并导出 ======================
 ExcelUtils.mergeAndDownload(
         response,
         "多表合并",
         wb1, wb2
 );
本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值