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

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

java 复制代码
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 {

    /**
     * 多级表头导出(支持3级/无限级 + 自动铺满 + 边框正常 + 不报错)
     */
    public static void downloadExcel2Headers(List<ExcelHeader> headers,
                                             List<HashMap<String, Object>> dataList,
                                             HttpServletResponse response,
                                             String fileName) throws IOException {
        ExcelWriter writer = ExcelUtil.getWriter(true);
        Sheet sheet = writer.getSheet();

        try {
            // 1. 获取表头最大层数
            int maxLevel = getMaxHeaderLevel(headers);

            // 2. 递归构建表头(自动按子节点铺满)
            AtomicInteger colIndex = new AtomicInteger(0);
            buildHeaderRecursive(writer, sheet, headers, 0, colIndex, maxLevel);

            // 3. 收集所有真实列
            List<String> fieldOrder = collectAllLeafFields(headers);

            // 4. 写入数据
            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(maxLevel);
            writer.write(dataRows, false);

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

            // 6. 自动列宽(修复越界)
            int colCount = getRealColumnCount(sheet);
            float chineseRatio = 2.0f;
            float bufferWidth = 3.0f;
            int maxWidth = 50;
            int[] maxColWidths = calculateColumnWidths(sheet, colCount, chineseRatio);

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

            // 7. 输出下载
            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 {
            writer.close();
        }
    }

    // ==================== 核心递归:构建表头(自动合并正确列数) ====================
    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

java 复制代码
import lombok.Data;
import java.util.List;

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

必须依赖(pom.xml)

java 复制代码
<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>

调用案例

java 复制代码
  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.downloadExcel2Headers(headers, dataList, response, "学生成绩表");
    }

    // 快速创建列
    private static ExcelHeader createHeader(String title, String fieldName) {
        ExcelHeader h = new ExcelHeader();
        h.setTitle(title);
        h.setFieldName(fieldName);
        return h;
    }
相关推荐
徒 花2 小时前
HCIA知识整理2
开发语言·php
承渊政道2 小时前
【优选算法】(实战领略前缀和的真谛)
开发语言·数据结构·c++·笔记·学习·算法
xiaoliuliu123452 小时前
Dev C++ 5.11开发编辑器 安装教程:详细步骤+自定义安装路径(附简体中文设置)
开发语言·c++
闻哥2 小时前
深入理解 InnoDB 的 MVCC:原理、Read View 与可见性判断
java·开发语言·jvm·数据库·b树·mysql·面试
Jul1en_2 小时前
Java 集合判空方法对比
java·spring boot·算法·spring
golang学习记2 小时前
IDEA 2026.1:这些 核心功能免费开放!
java·ide·intellij-idea
我就是你毛毛哥2 小时前
Docker 安装 Jenkins JDK8 版
java·docker·jenkins
爱敲代码的菜菜2 小时前
【Redis】Redis基本操作
java·数据库·redis·缓存·hash·zset
编码忘我2 小时前
java之线程池
java·后端·面试