基于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;
}