Java POI 通用Excel导出工具(支持大数据流式导出、PO/VO适配、解决列错位问题)
一、项目简介
Excel导出是后端开发高频刚需场景,普通POI一次性加载全量数据容易引发内存溢出(OOM)。本文封装一套高兼容、高性能、可通用 的Excel导出工具,完美适配 2003(.xls) / 2007(.xlsx) 格式,支持普通List集合导出、MyBatis Cursor大数据流式导出,同时兼容数据库PO实体、自定义导出VO,彻底解决日常导出列错位、数据为空、内存溢出等常见问题。
核心优势
-
双格式兼容:支持2003低版本、2007高版本Excel文件导出
-
大数据友好:基于SXSSF实现流式导出、MyBatis Cursor游标查询,百万级数据无OOM风险
-
高性能反射:缓存实体Getter方法,避免重复反射,大幅提升导出效率
-
全类型适配:自动兼容数字、日期、布尔、字符串等字段类型,布尔值自动转换「是/否」
-
场景全覆盖:支持PO实体直接导出、自定义VO联查导出,适配多表联查导出场景
二、Maven依赖引入
采用稳定经典的POI 3.10-FINAL版本,适配绝大多数SpringBoot项目,无版本冲突问题:
XML
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.10-FINAL</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.10-FINAL</version>
</dependency>
三、通用Excel导出工具类(完整版)
工具类封装2003、2007双版本导出逻辑,内置方法缓存、数据类型自动适配、统一单元格样式、流式临时文件自动释放,开箱即用,无需重复改造。
java
package com.skm.utils;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFCellStyle;
import org.apache.poi.hssf.usermodel.HSSFFont;
import org.apache.poi.hssf.usermodel.HSSFRichTextString;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFColor;
import org.apache.poi.xssf.usermodel.XSSFFont;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
/**
* 导出Excel(支持 SXSSF 流式导出,适合大数据量)
*
* @param <T>
*/
public class ExportExcelUtil<T> {
// 2007 版本以上 最大支持1048576行
public final static String EXCEl_FILE_2007 = "2007";
// 2003 版本 最大支持65536 行
public final static String EXCEL_FILE_2003 = "2003";
// SXSSF 内存中保留的行数窗口,默认 1000,可根据内存调整
private static final int DEFAULT_STREAM_WINDOW_SIZE = 1000;
// 反射方法缓存:避免每行都重复 getMethod
private static final Map<Class<?>, Method[]> METHOD_CACHE = new ConcurrentHashMap<>();
// 数字字符串正则(修复了原代码 //d 的 bug)
private static final Pattern NUMBER_PATTERN = Pattern.compile("^-?\\d+(\\.\\d+)?$");
/**
* 设置 SXSSF 流式窗口大小。值越小内存占用越低,但写入速度可能略有下降。
*/
private int streamWindowSize = DEFAULT_STREAM_WINDOW_SIZE;
public void setStreamWindowSize(int streamWindowSize) {
this.streamWindowSize = streamWindowSize;
}
/**
* <p>
* 导出无头部标题行Excel <br>
* 时间格式默认:yyyy-MM-dd HH:mm:ss <br>
* </p>
*
* @param title 表格标题
* @param dataset 数据集合
* @param out 输出流
* @param version 2003 或者 2007,不传时默认生成2003版本
*/
public void exportExcel(String title, Collection<T> dataset, OutputStream out, String version) {
if (StringUtils.isEmpty(version) || EXCEL_FILE_2003.equals(version.trim())) {
exportExcel2003(title, null, dataset, out, "yyyy-MM-dd HH:mm:ss");
} else {
exportExcel2007(title, null, dataset, out, "yyyy-MM-dd HH:mm:ss");
}
}
/**
* <p>
* 导出带有头部标题行的Excel <br>
* 时间格式默认:yyyy-MM-dd HH:mm:ss <br>
* </p>
*
* @param title 表格标题
* @param headers 头部标题集合
* @param dataset 数据集合
* @param out 输出流
* @param version 2003 或者 2007,不传时默认生成2003版本
*/
public void exportExcel(String title, String[] headers, Collection<T> dataset, OutputStream out, String version) {
if (StringUtils.isBlank(version) || EXCEL_FILE_2003.equals(version.trim())) {
exportExcel2003(title, headers, dataset, out, "yyyy-MM-dd HH:mm:ss");
} else {
exportExcel2007(title, headers, dataset == null ? null : dataset.iterator(), out, "yyyy-MM-dd HH:mm:ss");
}
}
/**
* 流式导出入口:支持 Iterator,适合 MyBatis Cursor 等流式数据源。
*/
public void exportExcel(String title, String[] headers, Iterator<T> dataIt, OutputStream out, String version) {
if (StringUtils.isBlank(version) || EXCEL_FILE_2003.equals(version.trim())) {
exportExcel2003(title, headers, dataIt, out, "yyyy-MM-dd HH:mm:ss");
} else {
exportExcel2007(title, headers, dataIt, out, "yyyy-MM-dd HH:mm:ss");
}
}
/**
* 流式导出 2007+ 版本 Excel(xlsx),适合大数据量(百万级)。
*
* @param title 表格标题名
* @param headers 表格头部标题集合
* @param dataset 需要显示的数据集合,集合中放置符合 JavaBean 风格的对象
* @param out 与输出设备关联的流对象
* @param pattern 时间数据输出格式,默认为 "yyyy-MM-dd HH:mm:ss"
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public void exportExcel2007(String title, String[] headers, Collection<T> dataset, OutputStream out, String pattern) {
exportExcel2007(title, headers, dataset == null ? null : dataset.iterator(), out, pattern);
}
/**
* 流式导出 2007+ 版本 Excel(xlsx)------ Iterator 版本。
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public void exportExcel2007(String title, String[] headers, Iterator<T> dataIt, OutputStream out, String pattern) {
if (dataIt == null || !dataIt.hasNext()) {
return;
}
// 使用 SXSSFWorkbook 进行流式写入
SXSSFWorkbook workbook = new SXSSFWorkbook(streamWindowSize);
workbook.setCompressTempFiles(true); // 压缩临时文件,降低磁盘占用
try {
Sheet sheet = workbook.createSheet(title);
sheet.setDefaultColumnWidth(20);
// 头部样式(SXSSFWorkbook 实际创建的是 XSSFCellStyle)
XSSFCellStyle headerStyle = (XSSFCellStyle) workbook.createCellStyle();
headerStyle.setFillForegroundColor(new XSSFColor(java.awt.Color.gray));
headerStyle.setFillPattern(CellStyle.SOLID_FOREGROUND);
headerStyle.setBorderBottom(CellStyle.BORDER_THIN);
headerStyle.setBorderLeft(CellStyle.BORDER_THIN);
headerStyle.setBorderRight(CellStyle.BORDER_THIN);
headerStyle.setBorderTop(CellStyle.BORDER_THIN);
headerStyle.setAlignment(CellStyle.ALIGN_CENTER);
XSSFFont headerFont = (XSSFFont) workbook.createFont();
headerFont.setBoldweight(Font.BOLDWEIGHT_BOLD);
headerFont.setFontName("宋体");
headerFont.setColor(new XSSFColor(java.awt.Color.BLACK));
headerFont.setFontHeightInPoints((short) 11);
headerStyle.setFont(headerFont);
// 数据样式
XSSFCellStyle dataStyle = (XSSFCellStyle) workbook.createCellStyle();
dataStyle.setFillForegroundColor(new XSSFColor(java.awt.Color.WHITE));
dataStyle.setFillPattern(CellStyle.SOLID_FOREGROUND);
dataStyle.setBorderBottom(CellStyle.BORDER_THIN);
dataStyle.setBorderLeft(CellStyle.BORDER_THIN);
dataStyle.setBorderRight(CellStyle.BORDER_THIN);
dataStyle.setBorderTop(CellStyle.BORDER_THIN);
dataStyle.setAlignment(CellStyle.ALIGN_CENTER);
dataStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
XSSFFont dataFont = (XSSFFont) workbook.createFont();
dataFont.setBoldweight(Font.BOLDWEIGHT_NORMAL);
dataStyle.setFont(dataFont);
// 写入表头
if (headers != null && headers.length > 0) {
Row headerRow = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellStyle(headerStyle);
cell.setCellValue(new XSSFRichTextString(headers[i]));
}
}
// 写入数据行
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
int index = (headers == null || headers.length == 0) ? 0 : 1;
while (dataIt.hasNext()) {
T t = (T) dataIt.next();
Row row = sheet.createRow(index++);
writeRow2007(row, t, dataStyle, sdf);
}
workbook.write(out);
out.flush();
} catch (IOException e) {
throw new RuntimeException("导出 Excel 失败", e);
} finally {
// 必须 dispose,否则临时文件不会删除(POI 3.10 SXSSFWorkbook 没有 close() 方法)
workbook.dispose();
}
}
/**
* 写入一行数据(SXSSF)
*/
private void writeRow2007(Row row, T t, XSSFCellStyle dataStyle, SimpleDateFormat sdf) {
Method[] methods = getGetterMethods(t.getClass());
for (int i = 0; i < methods.length; i++) {
Cell cell = row.createCell(i);
cell.setCellStyle(dataStyle);
setCellValue(cell, invokeGetter(methods[i], t), sdf);
}
}
/**
* 通用导出方法:生成 2003 版本 xls(仅适合 65536 行以内)。
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public void exportExcel2003(String title, String[] headers, Collection<T> dataset, OutputStream out, String pattern) {
exportExcel2003(title, headers, dataset == null ? null : dataset.iterator(), out, pattern);
}
/**
* 通用导出方法:生成 2003 版本 xls(仅适合 65536 行以内)------ Iterator 版本。
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public void exportExcel2003(String title, String[] headers, Iterator<T> dataIt, OutputStream out, String pattern) {
if (dataIt == null || !dataIt.hasNext()) {
return;
}
HSSFWorkbook workbook = new HSSFWorkbook();
try {
HSSFSheet sheet = workbook.createSheet(title);
sheet.setDefaultColumnWidth(20);
HSSFCellStyle style = workbook.createCellStyle();
style.setFillForegroundColor(HSSFColor.GREY_50_PERCENT.index);
style.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);
style.setBorderBottom(HSSFCellStyle.BORDER_THIN);
style.setBorderLeft(HSSFCellStyle.BORDER_THIN);
style.setBorderRight(HSSFCellStyle.BORDER_THIN);
style.setBorderTop(HSSFCellStyle.BORDER_THIN);
style.setAlignment(HSSFCellStyle.ALIGN_CENTER);
HSSFFont font = workbook.createFont();
font.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);
font.setFontName("宋体");
font.setColor(HSSFColor.WHITE.index);
font.setFontHeightInPoints((short) 11);
style.setFont(font);
HSSFCellStyle style2 = workbook.createCellStyle();
style2.setFillForegroundColor(HSSFColor.WHITE.index);
style2.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);
style2.setBorderBottom(HSSFCellStyle.BORDER_THIN);
style2.setBorderLeft(HSSFCellStyle.BORDER_THIN);
style2.setBorderRight(HSSFCellStyle.BORDER_THIN);
style2.setBorderTop(HSSFCellStyle.BORDER_THIN);
style2.setAlignment(HSSFCellStyle.ALIGN_CENTER);
style2.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);
HSSFFont font2 = workbook.createFont();
font2.setBoldweight(HSSFFont.BOLDWEIGHT_NORMAL);
style2.setFont(font2);
if (headers != null && headers.length > 0) {
HSSFRow row = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
HSSFCell cell = row.createCell(i);
cell.setCellStyle(style);
cell.setCellValue(new HSSFRichTextString(headers[i]));
}
}
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
int index = (headers == null || headers.length == 0) ? 0 : 1;
while (dataIt.hasNext()) {
T t = (T) dataIt.next();
HSSFRow row = sheet.createRow(index++);
Method[] methods = getGetterMethods(t.getClass());
for (int i = 0; i < methods.length; i++) {
HSSFCell cell = row.createCell(i);
cell.setCellStyle(style2);
setCellValue(cell, invokeGetter(methods[i], t), sdf);
}
}
workbook.write(out);
out.flush();
} catch (IOException e) {
throw new RuntimeException("导出 Excel 失败", e);
}
// POI 3.10 的 HSSFWorkbook 没有 close() 方法,无需关闭
}
/**
* 获取并缓存指定类的所有 getter 方法,按字段声明顺序排列。
*/
private Method[] getGetterMethods(Class<?> clazz) {
return METHOD_CACHE.computeIfAbsent(clazz, k -> {
java.lang.reflect.Field[] fields = k.getDeclaredFields();
Method[] methods = new Method[fields.length];
for (int i = 0; i < fields.length; i++) {
String fieldName = fields[i].getName();
String getMethodName = "get" + fieldName.substring(0, 1).toUpperCase()
+ fieldName.substring(1);
try {
methods[i] = k.getMethod(getMethodName);
} catch (NoSuchMethodException e) {
// 如果没有对应的 getter,则尝试 isXxx(Boolean 常见)
if (fields[i].getType() == boolean.class || fields[i].getType() == Boolean.class) {
try {
methods[i] = k.getMethod("is" + fieldName.substring(0, 1).toUpperCase()
+ fieldName.substring(1));
} catch (NoSuchMethodException ex) {
throw new RuntimeException("字段 " + fieldName + " 缺少 getter 方法", ex);
}
} else {
throw new RuntimeException("字段 " + fieldName + " 缺少 getter 方法", e);
}
}
}
return methods;
});
}
/**
* 调用 getter 方法获取值。
*/
private Object invokeGetter(Method method, Object target) {
try {
return method.invoke(target);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("调用 " + method.getName() + " 失败", e);
}
}
/**
* 根据值类型设置单元格内容。
*/
private void setCellValue(Cell cell, Object value, SimpleDateFormat sdf) {
if (value == null) {
return;
}
if (value instanceof Number) {
// Integer / Long / Float / Double / BigDecimal 等直接写为数字
cell.setCellValue(((Number) value).doubleValue());
} else if (value instanceof Boolean) {
cell.setCellValue((Boolean) value ? "是" : "否");
} else if (value instanceof Date) {
cell.setCellValue(sdf.format((Date) value));
} else {
String textValue = value.toString();
Matcher matcher = NUMBER_PATTERN.matcher(textValue);
if (matcher.matches()) {
cell.setCellValue(Double.parseDouble(textValue));
} else {
cell.setCellValue(textValue);
}
}
}
}
四、实际业务导出示例
提供两种生产常用导出方案:大数据Cursor流式导出(PO实体) 、多表联查VO导出,适配不同业务场景。
4.1 大数据量流式导出(MyBatis Cursor + PO实体)
适用于单表大批量数据导出,基于游标分批读取数据,内存零压力,百万级数据不OOM。
java
@Override
@Transactional // Cursor 需要保持连接打开
public void exportAll(HttpServletResponse response) {
ExportExcelUtil<Child> util = new ExportExcelUtil<>();
util.setStreamWindowSize(1000);
String[] headers = {"Id","姓名","性别", "年级", "班级", "出生日期", "入园日期", "过敏史", "饮食禁忌", "家长手机号"};
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=children.xlsx");
try (Cursor<Child> cursor = childMapper.selectAllCursor()) {
util.exportExcel("儿童信息", headers, cursor.iterator(), response.getOutputStream(), "2007");
} catch (IOException e) {
throw new BusinessException("导出儿童信息失败", e);
}
}
4.2 多表联查VO导出(最常用)
适用于多表联查、需要整合扩展字段的导出场景(如关联班级名称),使用自定义VO接收联查数据。
java
@Override
public void exportChild(HttpServletResponse response) {
ExportExcelUtil<ChildExcelVo> util = new ExportExcelUtil<>();
//child_id,name,grade,gender,birth_date,enroll_date,current_class_id,allergy_info,diet_restrictions,parent_phone,create_time,update_time,status
// 核心重点:表头顺序、VO字段声明顺序、数据查询顺序 三者必须完全一致,否则列数据错位
String[] headers = {"Id","姓名", "年级","性别", "出生日期", "入园日期","班级", "过敏史", "饮食禁忌", "家长手机号","创建时间","更新时间","状态"};
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=children.xlsx");
List<ChildExcelVo> childList = childMapper.selectExcelList(new ChildQuery());
try {
util.exportExcel("儿童信息", headers, childList, response.getOutputStream(), "2007");
} catch (IOException e) {
throw new BusinessException("导出儿童信息失败", e);
}
}
五、核心避坑注意事项(重点)
5.1 VO导出列错位方案
本工具不根据SQL查询顺序、表头顺序赋值 ,而是通过反射VO字段声明顺序依次填充单元格。
三者必须严格对齐,缺一不可:
-
VO实体字段从上到下的声明顺序(核心依据)
-
Java表头数组headers的顺序
-
SQL查询字段顺序(建议对齐,避免冗余字段)
错误场景:VO第1个字段是姓名,表头第1列是ID → 整列数据全部错位。
5.2 数据为空/映射失败问题
-
开启MyBatis下划线转驼峰:
map-underscore-to-camel-case: true,否则数据库下划线字段无法映射VO驼峰字段 -
注意字段拼写错误,会直接导致字段值为null
-
Mapper.xml查询VO必须指定
resultType="VO全类名",否则MyBatis无法封装数据
5.3 大数据导出优化规则
-
万级以上数据必须使用 SXSSF流式导出 + MyBatis Cursor,禁止一次性查询全量List
-
导出完成必须执行
workbook.dispose(),释放POI临时文件,避免磁盘爆满 -
合理设置streamWindowSize,内存充足可设2000,内存紧张设500