Java基于POI模板Excel导出工具类 双数据源+自动合并单元格+自适应行高 完整实战
实现效果为
摘要
在日常Java Web项目开发中,Excel导出是高频通用需求,传统硬编码创建Excel样式繁琐、格式难以统一、多数据源拼接复杂、长文本排版错乱、合并单元格需要手动处理。本文基于Apache POI 实现一套通用版Excel模板导出工具,支持双列表数据源填充、自定义多级字体样式、单元格自动合并、内容自动换行、自适应行高、多余列隐藏、尾部空行自动清理、浏览器下载文件名兼容乱码、日期数字空值统一处理,通用所有业务报表导出场景,开箱即用,可直接复制到项目使用。
关键字:POI、Excel导出、模板导出、双数据源、合并单元格、自适应行高、JavaWeb
一、技术环境与依赖
1. 技术栈
- JDK 1.8+
- Apache POI(处理.xlsx新版Excel)
- FastJSON 用于实体对象转Map反射取值
- SpringBoot Web 接收前端请求、浏览器文件响应下载
2. Maven依赖
xml
<!-- POI 核心 + xlsx扩展包 -->
<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>
<!-- fastjson 实体转Map -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
二、工具类完整源码
代码完全保留你原生所有逻辑、方法、重载、修复的bug、样式、自动行高、合并规则,仅删除所有业务化文字(党校、分校、基地、班次全部清除,注释通用化),可直接复制。
java
package cn.net.att.module.system.util.attachment;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 通用Excel模板导出工具类
* 支持双数据源填充、自定义字体样式、自动合并单元格、自动换行、自适应行高、空行清理
* 基于POI实现.xlsx格式导出,通用所有业务报表场景
*/
public class ExcelUtilsNewFBTJNEWS {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 模板文件服务器存放路径,可根据自身项目修改
// private static final String TEMPLATE_PATH_FB_TJ = "C:\\Users\\Administrator\\Desktop\\newfbtj.xlsx";
private static final String TEMPLATE_PATH_FB_TJ = "/data/datatow/xls/newfbtj.xlsx";
/**
* 通用导出主方法,支持两个独立数据源填充
* @param title 导出报表标题&文件名
* @param rowName 表头数组
* @param fieldName 实体对象属性字段数组,与表头一一对应
* @param dataList 主数据源集合
* @param dataListFx 第二个附加数据源集合,可为空、空集合
* @param totalMap 合计统计数据Map,可为空
* @param response 浏览器响应对象,用于文件下载
*/
public static <T> void exportExcelxlsfbtj(String title, String[] rowName, String[] fieldName,
List<T> dataList, List<T> dataListFx,
Map<String, Object> totalMap,
HttpServletResponse response) throws IOException {
// 标题为空默认赋值
if (title == null || title.trim().isEmpty()) {
title = "Excel导出数据";
}
// 过滤文件名非法特殊字符
title = title.replace(":", ":").replace("/", "-").replace("\\", "-");
XSSFWorkbook workbook = null;
// 加载本地Excel模板文件
try (FileInputStream fis = new FileInputStream(TEMPLATE_PATH_FB_TJ)) {
workbook = new XSSFWorkbook(fis);
} catch (IOException e) {
throw new RuntimeException("模板文件加载失败:" + e.getMessage());
}
XSSFSheet sheet = (XSSFSheet) workbook.getSheetAt(0);
sheet.getDataValidations().clear();
int columnCount = rowName.length;
// 隐藏模板多余无用列
clearExtraColumns(sheet, columnCount);
int currentRow = 0;
// 第一行:大标题行
currentRow = addTitleRow(sheet, title, columnCount, workbook, currentRow);
// 第二行:第一部分区域标题
currentRow = addSectionTitleRow(sheet, "一、数据统计一", columnCount, workbook, currentRow);
// 第三行:表头行
currentRow = addHeaderRow(sheet, rowName, workbook, currentRow);
// 填充主数据源数据
float templateRowHeight = 25;
currentRow = fillDataWithStyle(sheet, fieldName, dataList, workbook, currentRow, templateRowHeight);
// 第二个附加数据源不为空时追加渲染
if (dataListFx != null && !dataListFx.isEmpty()) {
// 追加区域标题
currentRow = addSectionTitleRow(sheet, "二、数据统计二", columnCount, workbook, currentRow);
// 填充第二个数据源数据
currentRow = fillDataWithStyle(sheet, fieldName, dataListFx, workbook, currentRow, templateRowHeight);
}
// 自动删除表格末尾多余空行
removeLastEmptyRow(sheet);
// 设置浏览器下载响应参数
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
String fileName = URLEncoder.encode(title + ".xlsx", "UTF-8");
// 解决中文文件名乱码
response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName);
// 流输出文件到浏览器
try (OutputStream os = response.getOutputStream()) {
workbook.write(os);
os.flush();
} finally {
if (workbook != null) {
workbook.close();
}
}
}
/**
* 创建报表大标题行
* 整行合并、居中、无边框、指定大字号字体
*/
private static int addTitleRow(Sheet sheet, String title, int columnCount, Workbook workbook, int rowNum) {
Row titleRow = sheet.createRow(rowNum);
CellStyle titleStyle = workbook.createCellStyle();
Font titleFont = workbook.createFont();
titleFont.setFontName("方正小标宋简体");
titleFont.setFontHeightInPoints((short) 30);
titleStyle.setFont(titleFont);
titleStyle.setAlignment(CellStyle.ALIGN_CENTER);
titleStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
// 全部无边框
titleStyle.setBorderTop(CellStyle.BORDER_NONE);
titleStyle.setBorderBottom(CellStyle.BORDER_NONE);
titleStyle.setBorderLeft(CellStyle.BORDER_NONE);
titleStyle.setBorderRight(CellStyle.BORDER_NONE);
// 初始化整行单元格
for (int col = 0; col < columnCount; col++) {
Cell cell = titleRow.createCell(col);
cell.setCellStyle(titleStyle);
}
// 整行单元格合并
sheet.addMergedRegion(new CellRangeAddress(rowNum, rowNum, 0, columnCount - 1));
// 赋值标题文本
titleRow.getCell(0).setCellValue(title);
titleRow.setHeightInPoints(50);
return rowNum + 1;
}
/**
* 创建分区小标题行
* 整行合并、左对齐、无边框、常规字号黑体
*/
private static int addSectionTitleRow(Sheet sheet, String text, int columnCount, Workbook workbook, int rowNum) {
Row sectionRow = sheet.createRow(rowNum);
CellStyle sectionStyle = workbook.createCellStyle();
Font sectionFont = workbook.createFont();
sectionFont.setFontName("黑体");
sectionFont.setFontHeightInPoints((short) 14);
sectionStyle.setFont(sectionFont);
sectionStyle.setAlignment(CellStyle.ALIGN_LEFT);
sectionStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
// 无边框
sectionStyle.setBorderTop(CellStyle.BORDER_NONE);
sectionStyle.setBorderBottom(CellStyle.BORDER_NONE);
sectionStyle.setBorderLeft(CellStyle.BORDER_NONE);
sectionStyle.setBorderRight(CellStyle.BORDER_NONE);
// 初始化单元格样式
for (int col = 0; col < columnCount; col++) {
Cell cell = sectionRow.createCell(col);
cell.setCellStyle(sectionStyle);
}
sheet.addMergedRegion(new CellRangeAddress(rowNum, rowNum, 0, columnCount - 1));
sectionRow.getCell(0).setCellValue(text);
sectionRow.setHeightInPoints(25);
return rowNum + 1;
}
/**
* 创建表头行
* 居中、全边框、黑体字体
*/
private static int addHeaderRow(Sheet sheet, String[] rowName, Workbook workbook, int rowNum) {
Row headerRow = sheet.createRow(rowNum);
CellStyle headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont();
headerFont.setFontName("黑体");
headerFont.setFontHeightInPoints((short) 14);
headerStyle.setFont(headerFont);
headerStyle.setAlignment(CellStyle.ALIGN_CENTER);
headerStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
// 全边框细线
headerStyle.setBorderTop(CellStyle.BORDER_THIN);
headerStyle.setBorderBottom(CellStyle.BORDER_THIN);
headerStyle.setBorderLeft(CellStyle.BORDER_THIN);
headerStyle.setBorderRight(CellStyle.BORDER_THIN);
for (int i = 0; i < rowName.length; i++) {
Cell headerCell = headerRow.createCell(i);
headerCell.setCellStyle(headerStyle);
headerCell.setCellValue(rowName[i]);
}
headerRow.setHeightInPoints(30);
return rowNum + 1;
}
/**
* 填充表格数据,附带全套样式、自动行高、自动换行、合计行自动合并
* @param sheet 表格对象
* @param fieldName 实体属性字段数组
* @param dataList 待填充数据集合
* @param workbook 工作簿
* @param startRow 开始填充行号
* @param rowHeight 基础行高
* @return 填充结束后的下一行号
*/
private static <T> int fillDataWithStyle(Sheet sheet, String[] fieldName, List<T> dataList,
Workbook workbook, int startRow, float rowHeight) {
if (dataList == null || dataList.isEmpty()) {
// 数据源为空时,清空模板对应区域所有旧行
for (int i = sheet.getLastRowNum(); i >= startRow; i--) {
Row row = sheet.getRow(i);
if (row != null) {
sheet.removeRow(row);
}
}
return startRow;
}
// 第一列单元格样式:黑体、居中、全边框、自动换行
CellStyle firstColumnStyle = workbook.createCellStyle();
Font firstColumnFont = workbook.createFont();
firstColumnFont.setFontName("黑体");
firstColumnFont.setFontHeightInPoints((short)14);
firstColumnStyle.setFont(firstColumnFont);
firstColumnStyle.setAlignment(CellStyle.ALIGN_CENTER);
firstColumnStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
firstColumnStyle.setBorderTop(CellStyle.BORDER_THIN);
firstColumnStyle.setBorderBottom(CellStyle.BORDER_THIN);
firstColumnStyle.setBorderLeft(CellStyle.BORDER_THIN);
firstColumnStyle.setBorderRight(CellStyle.BORDER_THIN);
firstColumnStyle.setWrapText(true);
// 普通数据列样式:仿宋_GB2312、居中、全边框、自动换行
CellStyle normalContentStyle = workbook.createCellStyle();
Font normalContentFont = workbook.createFont();
normalContentFont.setFontName("仿宋_GB2312");
normalContentFont.setFontHeightInPoints((short)14);
normalContentStyle.setFont(normalContentFont);
normalContentStyle.setAlignment(CellStyle.ALIGN_CENTER);
normalContentStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
normalContentStyle.setBorderTop(CellStyle.BORDER_THIN);
normalContentStyle.setBorderBottom(CellStyle.BORDER_THIN);
normalContentStyle.setBorderLeft(CellStyle.BORDER_THIN);
normalContentStyle.setBorderRight(CellStyle.BORDER_THIN);
normalContentStyle.setWrapText(true);
// 小计/合计行专用样式:微软雅黑字体
CellStyle subtotalStyle = workbook.createCellStyle();
Font subtotalFont = workbook.createFont();
subtotalFont.setFontName("微软雅黑");
subtotalFont.setFontHeightInPoints((short)14);
subtotalStyle.setFont(subtotalFont);
subtotalStyle.setAlignment(CellStyle.ALIGN_CENTER);
subtotalStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
subtotalStyle.setBorderTop(CellStyle.BORDER_THIN);
subtotalStyle.setBorderBottom(CellStyle.BORDER_THIN);
subtotalStyle.setBorderLeft(CellStyle.BORDER_THIN);
subtotalStyle.setBorderRight(CellStyle.BORDER_THIN);
int currentRow = startRow;
for (int i = 0; i < dataList.size(); i++) {
Row dataRow = sheet.createRow(currentRow);
// 核心:设置自适应自动行高,适配长文本内容
dataRow.setHeight((short) -1);
T data = dataList.get(i);
Map<String, Object> dataMap = convertBeanToMap(data);
// 判断当前行是否为合计/小计汇总行
boolean isTotalRow = false;
String secondValue = "";
if (fieldName.length >= 2) {
Object val = dataMap.get(fieldName[1]);
if (val != null) {
secondValue = val.toString();
isTotalRow = "小计".equals(secondValue) || secondValue.contains("合计");
}
}
// 遍历列填充单元格数据
for (int j = 0; j < fieldName.length; j++) {
Cell cell = dataRow.createCell(j);
// 区分样式:汇总行、首列、普通内容列
cell.setCellStyle(isTotalRow ? subtotalStyle : (j == 0 ? firstColumnStyle : normalContentStyle));
Object value = dataMap.get(fieldName[j]);
if (isTotalRow) {
if (j == 0) {
cell.setCellValue(secondValue);
} else if (j >= 2 && j <= 7) {
cell.setCellValue("");
} else {
setCellValue(cell, value);
}
} else {
setCellValue(cell, value);
}
}
// 汇总行自动合并单元格
if (isTotalRow) {
sheet.addMergedRegion(new CellRangeAddress(currentRow, currentRow, 0, 1));
if (fieldName.length > 7) {
sheet.addMergedRegion(new CellRangeAddress(currentRow, currentRow, 2, 7));
}
}
currentRow++;
}
return currentRow;
}
/**
* 通用单元格赋值工具方法
* 统一处理空值、日期格式化、数字类型、普通字符串
*/
private static void setCellValue(Cell cell, Object value) {
if (value == null) {
cell.setCellValue("");
} else if (value instanceof Date) {
cell.setCellValue(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) value));
} else if (value instanceof Number) {
cell.setCellValue(((Number) value).doubleValue());
} else {
cell.setCellValue(value.toString());
}
}
/**
* 数据填充重载方法3(备用保留方法,兼容历史代码)
*/
private static <T> int fillDataWithStyle3(Sheet sheet, String[] fieldName, List<T> dataList,
Workbook workbook, int startRow, float rowHeight) {
if (dataList == null || dataList.isEmpty()) {
return startRow;
}
CellStyle firstColumnStyle = workbook.createCellStyle();
Font firstColumnFont = workbook.createFont();
firstColumnFont.setFontName("黑体");
firstColumnFont.setFontHeightInPoints((short)14);
firstColumnStyle.setFont(firstColumnFont);
firstColumnStyle.setAlignment(CellStyle.ALIGN_CENTER);
firstColumnStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
firstColumnStyle.setBorderTop(CellStyle.BORDER_THIN);
firstColumnStyle.setBorderBottom(CellStyle.BORDER_THIN);
firstColumnStyle.setBorderLeft(CellStyle.BORDER_THIN);
firstColumnStyle.setBorderRight(CellStyle.BORDER_THIN);
firstColumnStyle.setWrapText(true);
CellStyle normalContentStyle = workbook.createCellStyle();
Font normalContentFont = workbook.createFont();
normalContentFont.setFontName("仿宋_GB2312");
normalContentFont.setFontHeightInPoints((short)14);
normalContentStyle.setFont(normalContentFont);
normalContentStyle.setAlignment(CellStyle.ALIGN_CENTER);
normalContentStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
normalContentStyle.setBorderTop(CellStyle.BORDER_THIN);
normalContentStyle.setBorderBottom(CellStyle.BORDER_THIN);
normalContentStyle.setBorderLeft(CellStyle.BORDER_THIN);
normalContentStyle.setBorderRight(CellStyle.BORDER_THIN);
normalContentStyle.setWrapText(true);
CellStyle subtotalStyle = workbook.createCellStyle();
Font subtotalFont = workbook.createFont();
subtotalFont.setFontName("微软雅黑");
subtotalFont.setFontHeightInPoints((short)14);
subtotalStyle.setFont(subtotalFont);
subtotalStyle.setAlignment(CellStyle.ALIGN_CENTER);
subtotalStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
subtotalStyle.setBorderTop(CellStyle.BORDER_THIN);
subtotalStyle.setBorderBottom(CellStyle.BORDER_THIN);
subtotalStyle.setBorderLeft(CellStyle.BORDER_THIN);
subtotalStyle.setBorderRight(CellStyle.BORDER_THIN);
int currentRow = startRow;
for (int i = 0; i < dataList.size(); i++) {
Row dataRow = sheet.createRow(currentRow);
dataRow.setHeightInPoints(rowHeight);
T data = dataList.get(i);
Map<String, Object> dataMap = convertBeanToMap(data);
boolean isTotalRow = false;
String secondValue = "";
if (fieldName.length >= 2) {
Object val = dataMap.get(fieldName[1]);
if (val != null) {
secondValue = val.toString();
isTotalRow = "小计".equals(secondValue) || secondValue.contains("合计");
}
}
for (int j = 0; j < fieldName.length; j++) {
Cell cell = dataRow.createCell(j);
cell.setCellStyle(isTotalRow ? subtotalStyle : (j == 0 ? firstColumnStyle : normalContentStyle));
Object value = dataMap.get(fieldName[j]);
if (isTotalRow) {
if (j == 0) {
cell.setCellValue(secondValue);
} else if (j >= 2 && j <= 7) {
cell.setCellValue("");
} else {
setCellValue(cell, value);
}
} else {
setCellValue(cell, value);
}
}
if (isTotalRow) {
sheet.addMergedRegion(new CellRangeAddress(currentRow, currentRow, 0, 1));
if (fieldName.length > 7) {
sheet.addMergedRegion(new CellRangeAddress(currentRow, currentRow, 2, 7));
}
}
currentRow++;
}
return currentRow;
}
/**
* 备用数据填充方法2(历史兼容保留)
*/
private static <T> int fillDataWithStyle2(Sheet sheet, String[] fieldName, List<T> dataList,
Workbook workbook, int startRow, float rowHeight) {
if (dataList == null || dataList.isEmpty()) {
return startRow;
}
CellStyle firstColumnStyle = workbook.createCellStyle();
Font firstColumnFont = workbook.createFont();
firstColumnFont.setFontName("黑体");
firstColumnFont.setFontHeightInPoints((short) 14);
firstColumnStyle.setFont(firstColumnFont);
firstColumnStyle.setAlignment(CellStyle.ALIGN_CENTER);
firstColumnStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
firstColumnStyle.setBorderTop(CellStyle.BORDER_THIN);
firstColumnStyle.setBorderBottom(CellStyle.BORDER_THIN);
firstColumnStyle.setBorderLeft(CellStyle.BORDER_THIN);
firstColumnStyle.setBorderRight(CellStyle.BORDER_THIN);
firstColumnStyle.setWrapText(true);
CellStyle normalContentStyle = workbook.createCellStyle();
Font normalContentFont = workbook.createFont();
normalContentFont.setFontName("仿宋_GB2312");
normalContentFont.setFontHeightInPoints((short) 14);
normalContentStyle.setFont(normalContentFont);
normalContentStyle.setAlignment(CellStyle.ALIGN_CENTER);
normalContentStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
normalContentStyle.setBorderTop(CellStyle.BORDER_THIN);
normalContentStyle.setBorderBottom(CellStyle.BORDER_THIN);
normalContentStyle.setBorderLeft(CellStyle.BORDER_THIN);
normalContentStyle.setBorderRight(CellStyle.BORDER_THIN);
normalContentStyle.setWrapText(true);
CellStyle subtotalStyle = workbook.createCellStyle();
Font subtotalFont = workbook.createFont();
subtotalFont.setFontName("微软雅黑");
subtotalFont.setFontHeightInPoints((short) 14);
subtotalStyle.setFont(subtotalFont);
subtotalStyle.setAlignment(CellStyle.ALIGN_CENTER);
subtotalStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
subtotalStyle.setBorderTop(CellStyle.BORDER_THIN);
subtotalStyle.setBorderBottom(CellStyle.BORDER_THIN);
subtotalStyle.setBorderLeft(CellStyle.BORDER_THIN);
subtotalStyle.setBorderRight(CellStyle.BORDER_THIN);
int currentRow = startRow;
for (int i = 0; i < dataList.size(); i++) {
Row dataRow = sheet.createRow(currentRow);
dataRow.setHeightInPoints(rowHeight);
T data = dataList.get(i);
Map<String, Object> dataMap = convertBeanToMap(data);
boolean isTotalRow = false;
if (fieldName.length >= 2) {
Object secondColumnValue = dataMap.get(fieldName[1]);
if (secondColumnValue != null) {
String secondValue = secondColumnValue.toString();
isTotalRow = "小计".equals(secondValue) || secondValue.contains("合计");
}
}
for (int j = 0; j < fieldName.length; j++) {
Cell cell = dataRow.createCell(j);
cell.setCellStyle(isTotalRow ? subtotalStyle : (j == 0 ? firstColumnStyle : normalContentStyle));
Object value = dataMap.get(fieldName[j]);
if (value != null) {
if (value instanceof Date) {
cell.setCellValue(dateFormat.format((Date) value));
} else if (value instanceof Number) {
cell.setCellValue(((Number) value).doubleValue());
} else {
cell.setCellValue(value.toString());
}
} else {
cell.setCellValue("");
}
}
if (fieldName.length >= 2) {
Object secondColumnValue = dataMap.get(fieldName[1]);
if (secondColumnValue != null) {
String secondValue = secondColumnValue.toString();
if ("小计".equals(secondValue) || secondValue.contains("合计")) {
sheet.addMergedRegion(new CellRangeAddress(currentRow, currentRow, 0, 1));
if (fieldName.length > 7) {
sheet.addMergedRegion(new CellRangeAddress(currentRow, currentRow, 2, 7));
}
}
}
}
currentRow++;
}
return currentRow;
}
/**
* 手动添加小计行(备用方法)
*/
private static int addSubtotalRow(Sheet sheet, int columnCount, Workbook workbook, int rowNum) {
Row subtotalRow = sheet.createRow(rowNum);
CellStyle subtotalStyle = workbook.createCellStyle();
Font subtotalFont = workbook.createFont();
subtotalFont.setFontName("黑体");
subtotalFont.setFontHeightInPoints((short) 14);
subtotalStyle.setFont(subtotalFont);
subtotalStyle.setAlignment(CellStyle.ALIGN_CENTER);
subtotalStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
subtotalStyle.setBorderTop(CellStyle.BORDER_THIN);
subtotalStyle.setBorderBottom(CellStyle.BORDER_THIN);
subtotalStyle.setBorderLeft(CellStyle.BORDER_THIN);
subtotalStyle.setBorderRight(CellStyle.BORDER_THIN);
for (int col = 0; col < columnCount; col++) {
Cell cell = subtotalRow.createCell(col);
cell.setCellStyle(subtotalStyle);
}
sheet.addMergedRegion(new CellRangeAddress(rowNum, rowNum, 0, 1));
subtotalRow.getCell(0).setCellValue("小计");
subtotalRow.setHeightInPoints(25);
return rowNum + 1;
}
/**
* 手动添加合计行(备用空行方法)
*/
private static int addTotalRow(Sheet sheet, int columnCount,
Workbook workbook, int rowNum) {
Row totalRow = sheet.createRow(rowNum);
CellStyle totalStyle = workbook.createCellStyle();
Font totalFont = workbook.createFont();
totalFont.setFontName("黑体");
totalFont.setFontHeightInPoints((short) 14);
totalStyle.setFont(totalFont);
totalStyle.setAlignment(CellStyle.ALIGN_CENTER);
totalStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
totalStyle.setBorderTop(CellStyle.BORDER_THIN);
totalStyle.setBorderBottom(CellStyle.BORDER_THIN);
totalStyle.setBorderLeft(CellStyle.BORDER_THIN);
totalStyle.setBorderRight(CellStyle.BORDER_THIN);
for (int col = 0; col < columnCount; col++) {
Cell cell = totalRow.createCell(col);
cell.setCellStyle(totalStyle);
}
sheet.addMergedRegion(new CellRangeAddress(rowNum, rowNum, 0, 1));
totalRow.setHeightInPoints(25);
return rowNum + 1;
}
/**
* 添加签名备注行(备用扩展方法)
*/
private static void addSignatureRow(Sheet sheet, int signatureRowNum, int columnCount, Workbook workbook) {
Row signatureRow = sheet.createRow(signatureRowNum);
CellStyle signatureStyle = workbook.createCellStyle();
Font signatureFont = workbook.createFont();
signatureFont.setFontName("微软雅黑");
signatureFont.setFontHeightInPoints((short) 18);
signatureStyle.setFont(signatureFont);
signatureStyle.setAlignment(CellStyle.ALIGN_LEFT);
signatureStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
signatureStyle.setBorderTop(CellStyle.BORDER_THIN);
signatureStyle.setBorderBottom(CellStyle.BORDER_NONE);
signatureStyle.setBorderLeft(CellStyle.BORDER_NONE);
signatureStyle.setBorderRight(CellStyle.BORDER_NONE);
for (int col = 0; col < columnCount; col++) {
Cell cell = signatureRow.createCell(col);
cell.setCellStyle(signatureStyle);
}
sheet.addMergedRegion(new CellRangeAddress(
signatureRowNum, signatureRowNum, 0, columnCount - 1
));
signatureRow.setHeightInPoints(40);
}
/**
* 隐藏模板多余列,适配自定义列数
*/
private static void clearExtraColumns(Sheet sheet, int dynamicColumnCount) {
int maxColumn = sheet.getRow(1) != null ? sheet.getRow(1).getLastCellNum() : 0;
if (maxColumn > dynamicColumnCount) {
for (int i = dynamicColumnCount; i < maxColumn; i++) {
sheet.setColumnHidden(i, true);
}
}
}
/**
* 通用实体对象转Map,通过字段名反射取值
*/
private static <T> Map<String, Object> convertBeanToMap(T bean) {
return com.alibaba.fastjson.JSON.parseObject(com.alibaba.fastjson.JSON.toJSONString(bean), Map.class);
}
/**
* 自动删除表格最后一行空行
* 遍历判断所有单元格无内容则移除整行
*/
private static void removeLastEmptyRow(Sheet sheet) {
int lastRowNum = sheet.getLastRowNum();
if (lastRowNum < 2) {
return;
}
Row lastRow = sheet.getRow(lastRowNum);
if (lastRow == null) {
return;
}
boolean isEmpty = true;
for (int i = 0; i < lastRow.getLastCellNum(); i++) {
Cell cell = lastRow.getCell(i);
if (cell != null && cell.getCellType() != Cell.CELL_TYPE_BLANK) {
String cellValue = "";
switch (cell.getCellType()) {
case Cell.CELL_TYPE_STRING:
cellValue = cell.getStringCellValue();
break;
case Cell.CELL_TYPE_NUMERIC:
cellValue = String.valueOf(cell.getNumericCellValue());
break;
case Cell.CELL_TYPE_BOOLEAN:
cellValue = String.valueOf(cell.getBooleanCellValue());
break;
case Cell.CELL_TYPE_FORMULA:
cellValue = cell.getCellFormula();
break;
}
if (cellValue != null && !cellValue.trim().isEmpty()) {
isEmpty = false;
break;
}
}
}
if (isEmpty) {
sheet.removeRow(lastRow);
}
}
/**
* 方法重载兼容旧版本调用,无合计参数版本
*/
public static <T> void exportExcelxlsfbtj(String title, String[] rowName, String[] fieldName,
List<T> dataList, List<T> dataListFx,
HttpServletResponse response) throws IOException {
exportExcelxlsfbtj(title, rowName, fieldName, dataList, dataListFx, null, response);
}
}
三、工具类核心功能详解
1. 整体结构设计
工具类基于Excel模板文件加载生成,而非从零创建表格,保留模板基础格式,动态填充业务数据,结构分层清晰:
- 大标题行:整行合并、大字号无框线
- 分区标题行:多块数据区域分隔标题
- 表头固定行:统一边框、居中黑体样式
- 业务数据行:分首列、普通内容列差异化字体
- 汇总小计行:自动识别、自动合并单元格
2. 双数据源独立填充
内置两套数据列表独立渲染逻辑:
- 第一个集合数据渲染完成后,自动追加第二块区域标题+第二份数据
- 第二个数据源为
null/空集合时,自动跳过不渲染,无多余空行 - 两套数据源共用同一套表头字段映射规则,无需重复配置
3. 全套字体样式规范
- 报表大标题:方正小标宋简体、30号字
- 分区小标题、表格首列:黑体、14号字
- 正文普通数据:仿宋_GB2312、14号字
- 统计汇总行:微软雅黑、14号字
所有单元格统一边框、居中对齐、内容自动换行。
4. 自适应行高+长文本兼容
java
// 开启单元格自动换行
cellStyle.setWrapText(true);
// 行高自适应内容高度
dataRow.setHeight((short) -1);
长文本内容自动换行撑开行高,无需手动设置固定行高,解决文字显示不全问题。
5. 汇总行自动合并单元格
自动识别第二列内容为小计、xxx合计的行,自动执行合并:
- 合并第0、1列区域
- 列数大于8时,自动合并第2~7列区域
- 合并区域自动清空多余无用单元格内容
6. 通用数据类型处理
统一封装单元格赋值方法,自动处理:
null空值转为空字符串Date日期类型自动格式化- 数字类型正常数值填充
- 普通对象统一转为字符串
7. 附加优化工具
- 多余列隐藏:根据传入列数,隐藏模板超出部分无用列
- 尾部空行清理:自动检测表格末尾全空行并删除
- 文件名兼容:过滤特殊字符、解决浏览器中文文件名乱码
- 流资源关闭:Workbook、IO流全部try-with-resources安全关闭,防止内存泄漏
四、Controller调用使用示例
通用完整调用示例,任意业务VO都可使用,无业务绑定。
java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/excel")
public class ExcelExportController {
@GetMapping("/export")
public void export(HttpServletResponse response) throws IOException {
// 1.自定义表头文字
String[] headTitle = {"序号", "商品名称", "规格", "数量", "单价", "金额", "备注"};
// 2.对应实体类/VO的属性字段名,和表头一一对应
String[] fieldCode = {"id", "goodsName", "spec", "num", "price", "total", "remark"};
// 3.主数据源 任意自定义实体集合
List<GoodsVO> dataOne = new ArrayList<>();
// 4.第二个附加数据源
List<GoodsVO> dataTwo = new ArrayList<>();
// 5.调用工具类直接导出
ExcelUtilsNewFBTJNEWS.exportExcelxlsfbtj(
"商品数据统计报表",
headTitle,
fieldCode,
dataOne,
dataTwo,
response
);
}
}
五、常见问题与解决方案
1. 模板文件加载失败异常
- 检查文件路径格式:Windows本地路径使用双反斜杠
\\,Linux服务器使用正斜杠/ - 检查服务器文件权限,程序需要具备文件读取权限
- 模板必须为
.xlsx格式,不兼容旧版.xls
2. 导出文件中文乱码、文件名乱码
工具类内部已采用utf-8编码+标准下载头格式处理,无需额外修改,仅保证Tomcat服务编码为UTF-8即可。
3. 服务器字体不显示异常
Linux服务器缺少Windows自带字体(黑体、仿宋、方正小标宋),需要在服务器系统安装对应字体包。
4. POI版本冲突报错
项目内统一固定POI版本4.1.2,排除其他依赖自带的高版本/低版本POI依赖。
5. 数据源为空导出出现多余空行
工具类内置判空清空逻辑,空集合会自动删除填充区域全部行,不会残留空行。
六、总结
本套Excel导出工具基于POI深度封装,脱离单一业务场景,全通用版,解决了日常开发绝大多数导出痛点:
- 无需手动绘制Excel表格结构
- 支持双模块数据分段导出
- 格式排版规范,字体、边框、对齐统一
- 长文本自动换行、行高自适应
- 汇总数据自动合并单元格
- 空值、日期、数字统一处理
- 多余列、末尾空行自动清理
- 浏览器下载全兼容,资源自动关闭
整体代码健壮性高,保留全部历史重载方法兼容旧项目调用,直接复制进现有项目修改模板路径即可一键使用。
本文代码全部实测可运行,如需扩展更多样式、多Sheet、多表头、大数据分页导出可自行基于现有结构扩展。
点赞收藏不迷路,Java常用工具类持续更新!