什么是POI?
POI(Apache POI) 是 Apache 软件基金会下的一个开源 Java 库,全称为 Poor Obfuscation Implementation。它主要用于 读写 Microsoft Office 格式文件(如 Excel、Word、PowerPoint),是 Java 生态中最流行的 Office 文档处理工具之一。
POI 的核心功能
|-----------|---------------------------|----------------------|
| 组件模块 | 支持格式 | 典型应用场景 |
| HSSF | Excel 97-2003 (.xls) | 读写旧版 Excel 二进制文件 |
| XSSF | Excel 2007+ (.xlsx) | 读写新版 Excel(基于 OOXML) |
| SXSSF | 大数据量 Excel (.xlsx) | 流式导出百万级数据,避免内存溢出 |
| HWPF | Word 97-2003 (.doc) | 处理旧版 Word 文档 |
| XWPF | Word 2007+ (.docx) | 生成新版 Word 报告 |
| HSLF | PowerPoint 97-2003 (.ppt) | 操作旧版 PPT |
| XSLF | PowerPoint 2007+ (.pptx) | 创建动态 PPT 演示文稿 |
POI 的核心优势
- 跨平台性
纯 Java 实现,无需安装 Office 软件即可操作文档。 - 功能全面
-
- Excel:读写单元格、公式、图表、样式、合并单元格等。
- Word:段落、表格、页眉页脚、书签等。
- PPT:幻灯片、文本框、图片、动画等。
- 社区活跃
Apache 顶级项目,持续更新维护,文档和示例丰富。
POI的案例实现
1、Pom依赖
<!-- Apache POI核心库依赖,用于处理Microsoft Office格式文件 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.2</version>
</dependency>
<!-- Apache POI OOXML扩展库依赖,用于处理Office Open XML格式文件(如.xlsx, .docx等) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.2</version>
</dependency>
2、自定义注解(用于导出Excel时配置字段属性)
package com.example.springboottest.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解:用于导出Excel时配置字段属性
* 该注解可以应用于类的字段上,运行时仍然保留
*/
@Retention(RetentionPolicy.RUNTIME) // 表示注解在运行时仍然存在
@Target(ElementType.FIELD) // 表示该注解只能用于类的字段上
public @interface ExcelExport {
/**
* 设置Excel列名
* @return 列名
*/
String name(); // 列名
/**
* 设置列的显示顺序
* @return 列顺序
*/
int order(); // 列顺序
/**
* 设置字段的格式化规则
* 例如日期格式:"yyyy-MM-dd"
* @return 格式化字符串
*/
String format() default ""; // 格式化(如日期格式)
/**
* 设置字典类型,用于数据字典转换
* 系统会根据该值查找对应的字典数据进行转换
* @return 字典类型
*/
String dictType() default ""; // 字典类型(用于数据字典转换)
}
3、使用注解完善导出实体类
package com.example.springboottest.entity;
import com.example.springboottest.annotation.ExcelExport; // 导入ExcelExport注解,用于Excel导出功能
import com.fasterxml.jackson.annotation.JsonFormat; // 导入Jackson的JsonFormat注解,用于JSON日期格式化
import lombok.Data; // 使用Lombok的@Data注解,自动生成getter、setter等方法
import org.springframework.format.annotation.DateTimeFormat; // 导入Spring的日期格式化注解
import java.io.Serializable; // 实现Serializable接口,支持对象序列化
import java.util.Date; // 导入日期类
/**
* 北京市人口信息表;数据表的PO对象
* 该类用于表示北京市人口信息的实体类,包含人口的基本信息字段
* <p>
* 使用注解实现Excel导出、JSON序列化等功能
*
* @author : lw // 作者信息
* @date : 2025-8-17 // 创建日期
*/
@Data // Lombok注解,自动生成getter、setter、toString、equals、hashCode等方法
public class PopulationBj implements Serializable, Cloneable { // 实现Serializable接口支持序列化,实现Cloneable接口支持克隆
// 使用ExcelExport注解标记该字段将被导出到Excel
// name: Excel列名, order: 列顺序, dictType: 字典类型(用于数据字典转换), format: 日期格式
@ExcelExport(name = "ID", order = 1) // ID字段,作为第一列导出
private Long id; // 人口ID,主键
@ExcelExport(name = "姓名", order = 2) // 姓名字段,作为第二列导出
private String personname; // 人口姓名
@ExcelExport(name = "性别", order = 3, dictType = "sys_user_sex") // 性别字段,作为第三列导出,使用字典类型
private String gender; // 人口性别,使用字典值
@ExcelExport(name = "年龄", order = 4) // 年龄字段,作为第四列导出
private Integer age; // 人口年龄
@ExcelExport(name = "身份证号", order = 5) // 身份证号字段,作为第五列导出
private String idCard; // 人口身份证号
@ExcelExport(name = "详细地址", order = 6) // 详细地址字段,作为第六列导出
private String address; // 人口详细地址
@ExcelExport(name = "教育程度", order = 7, dictType = "sys_education") // 教育程度字段,作为第七列导出,使用字典类型
private String education; // 人口教育程度,使用字典值
@ExcelExport(name = "职业", order = 8) // 职业字段,作为第八列导出
private String occupation; // 人口职业
@ExcelExport(name = "创建时间", order = 9, format = "yyyy-MM-dd HH:mm:ss") // 创建时间字段,作为第九列导出,指定日期格式
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") // Spring表单日期格式化
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") // JSON日期格式化,指定时区为GMT+8
private Date createTime; // 人口信息创建时间
}
4、编写POIUtil工具类
package com.example.springboottest.util;
import com.example.springboottest.annotation.ExcelExport;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
/**
* Excel导出工具类(完整优化版)
* 功能特点:
* 1. 支持自动分Sheet,每个Sheet最多10万行
* 2. 支持同步/异步导出
* 3. 支持字典转换和日期格式化
* 4. 大数据量处理优化
* 5. 完善的异常处理和资源管理
*/
@Slf4j
@Component
public class ExcelExportUtil {
// 每个Sheet最大行数(Excel限制为1048576,这里设置为10万以便管理)
private static final int MAX_ROWS_PER_SHEET = 100000;
// 内存中保留的行数(影响内存占用)
private static final int ROW_ACCESS_WINDOW_SIZE = 1000;
// 默认列宽(单位:1/256字符宽度)
private static final int DEFAULT_COLUMN_WIDTH = 15 * 256;
// 数据字典缓存(实际项目中可以从数据库加载)
private static final Map<String, Map<String, String>> DICT_CACHE = new ConcurrentHashMap<>();
static {
// 初始化示例字典数据
initDictCache();
}
/**
* 导出数据到Excel文件
*
* @param dataList 需要导出的数据列表
* @param outputStream 输出流,用于写入Excel文件
* @param clazz 数据对象的Class类型,用于获取字段信息
* @throws Exception 导出过程中可能抛出的异常
*/
public <T> void export(List<T> dataList, OutputStream outputStream, Class<T> clazz) throws Exception {
// 参数校验
validateParams(dataList, outputStream, clazz);
try (SXSSFWorkbook workbook = new SXSSFWorkbook(ROW_ACCESS_WINDOW_SIZE)) {
// 获取排序后的字段列表
List<Field> fields = getSortedFields(clazz);
// 计算需要的Sheet数量
int totalSheets = calculateTotalSheets(dataList.size());
// 分Sheet处理数据
for (int sheetIndex = 0; sheetIndex < totalSheets; sheetIndex++) {
// 处理单个工作表的数据
processSingleSheet(workbook, sheetIndex, dataList, fields); // 调用方法处理指定工作表,传入工作簿对象、工作表索引、数据列表和字段信息
}
// 写入输出流
workbook.write(outputStream);
outputStream.flush();
} catch (Exception e) {
log.error("Excel导出过程中发生异常", e);
throw e;
} finally {
closeOutputStreamSilently(outputStream);
}
}
/**
* 处理单个Sheet的数据
* 该方法用于将数据列表中的数据分割并写入到指定的Sheet中
*
* @param workbook SXSSFWorkbook对象,用于创建Sheet和写入数据
* @param sheetIndex Sheet的索引,从0开始
* @param dataList 包含所有数据的列表
* @param fields 数据对象的字段列表,用于获取表头和数据
* @throws Exception 可能抛出的异常
*/
private <T> void processSingleSheet(SXSSFWorkbook workbook, int sheetIndex,
List<T> dataList, List<Field> fields) throws Exception {
// 计算当前Sheet的数据范围
// 根据sheetIndex和每个Sheet的最大行数(MAX_ROWS_PER_SHEET)来确定当前Sheet应包含的数据范围
int fromIndex = sheetIndex * MAX_ROWS_PER_SHEET;
int toIndex = Math.min((sheetIndex + 1) * MAX_ROWS_PER_SHEET, dataList.size());
log.info("处理数据范围:{} - {}", fromIndex, toIndex);
List<T> subList = dataList.subList(fromIndex, toIndex);
// 创建Sheet
// 使用workbook创建一个新的Sheet,并命名为"Sheet_"加上序号(从1开始)
Sheet sheet = workbook.createSheet("Sheet_" + (sheetIndex + 1));
// 创建表头
// 调用createHeaderRow方法为当前Sheet创建表头行
createHeaderRow(sheet, fields);
// 填充数据
// 调用fillDataRows方法将subList中的数据填充到当前Sheet中
fillDataRows(sheet, subList, fields);
// 定期flush防止内存占用过高
// 每处理10个Sheet执行一次flush,将内存中的数据写入磁盘,释放内存
// ROW_ACCESS_WINDOW_SIZE定义了可访问的行数窗口大小
if (sheetIndex % 10 == 0) {
((SXSSFSheet) sheet).flushRows(ROW_ACCESS_WINDOW_SIZE);
}
}
/**
* 获取排序后的字段列表
* 获取带有ExcelExport注解的字段列表,并按照注解中的order值进行排序
*
* @param clazz 需要处理的类对象
* @return 排序后的字段列表,只包含带有ExcelExport注解的字段
*/
private <T> List<Field> getSortedFields(Class<T> clazz) {
// 创建一个用于存储字段的列表
List<Field> fields = new ArrayList<>();
// 遍历类中声明的所有字段
for (Field field : clazz.getDeclaredFields()) {
// 检查字段上是否存在ExcelExport注解
if (field.isAnnotationPresent(ExcelExport.class)) {
// 如果存在注解,则将该字段添加到列表中
fields.add(field);
}
}
// 按order排序
fields.sort(Comparator.comparingInt(f -> f.getAnnotation(ExcelExport.class).order()));
return fields;
}
/**
* 创建表头行
* 该方法用于在工作表中创建表头行,并根据字段列表设置表头内容
*
* @param sheet Excel工作表对象
* @param fields 包含ExcelExport注解的字段列表
*/
private void createHeaderRow(Sheet sheet, List<Field> fields) {
// 创建表头行,行号为0
Row headerRow = sheet.createRow(0);
// 创建表头单元格样式
CellStyle headerStyle = createHeaderCellStyle(sheet.getWorkbook());
// 遍历字段列表,为每个字段创建表头单元格
for (int i = 0; i < fields.size(); i++) {
// 获取当前字段
Field field = fields.get(i);
// 获取字段上的ExcelExport注解
ExcelExport annotation = field.getAnnotation(ExcelExport.class);
// 在表头行中创建单元格
Cell cell = headerRow.createCell(i);
// 设置单元格值为注解中指定的名称
cell.setCellValue(annotation.name());
// 应用表头样式
cell.setCellStyle(headerStyle);
// 设置列宽,使用默认列宽值
sheet.setColumnWidth(i, DEFAULT_COLUMN_WIDTH);
}
}
/**
* 填充数据行
*
* @param sheet Excel工作表对象
* @param dataList 要填充的数据列表
* @param fields 包含Excel注解的字段列表
* @throws Exception 可能抛出的异常
*/
private <T> void fillDataRows(Sheet sheet, List<T> dataList, List<Field> fields) throws Exception {
// 创建日期格式的单元格样式
CellStyle dateCellStyle = createDateCellStyle(sheet.getWorkbook());
// 遍历数据列表,填充每一行数据
for (int rowNum = 0; rowNum < dataList.size(); rowNum++) {
// 获取当前数据项
T item = dataList.get(rowNum);
Row row = sheet.createRow(rowNum + 1); // +1跳过表头行
for (int colNum = 0; colNum < fields.size(); colNum++) {
// 获取当前字段
Field field = fields.get(colNum);
// 设置可访问,包括私有字段
field.setAccessible(true);
// 获取字段值
Object value = field.get(item);
// 在指定列位置创建单元格
Cell cell = row.createCell(colNum);
// 设置单元格值,并处理日期格式
setCellValue(cell, value, field.getAnnotation(ExcelExport.class), dateCellStyle);
}
// 定期flush
if (rowNum > 0 && rowNum % 1000 == 0) {
((SXSSFSheet) sheet).flushRows(1000);
}
}
}
/**
* 设置单元格值
* 根据不同的数据类型和注解配置,将值设置到Excel单元格中
*
* @param cell Excel单元格对象
* @param value 要设置的值
* @param annotation Excel导出注解,包含格式化等配置信息
* @param dateCellStyle 日期格式样式
*/
private void setCellValue(Cell cell, Object value, ExcelExport annotation, CellStyle dateCellStyle) {
// 如果值为空,则设置为空字符串并返回
if (value == null) {
cell.setCellValue("");
return;
}
// 处理字典转换:如果注解中配置了字典类型,则从缓存中获取字典数据进行转换
if (!annotation.dictType().isEmpty()) {
Map<String, String> dict = DICT_CACHE.get(annotation.dictType());
if (dict != null) {
String dictValue = dict.get(value.toString());
if (dictValue != null) {
cell.setCellValue(dictValue);
return;
}
}
}
// 处理日期格式化:如果值是日期类型且注解中配置了格式,则应用日期样式
if (value instanceof Date && !annotation.format().isEmpty()) {
cell.setCellStyle(dateCellStyle);
cell.setCellValue((Date) value);
return;
}
// 默认处理:将值转换为字符串并设置到单元格
cell.setCellValue(value.toString());
}
/**
* 创建表头样式
* 该方法用于创建Excel表格中表头的样式设置
* 包括字体加粗、背景颜色、对齐方式等格式
*
* @param workbook Excel工作簿对象,用于创建样式和字体
* @return 返回配置好的单元格样式对象
*/
private CellStyle createHeaderCellStyle(Workbook workbook) {
// 创建新的单元格样式对象
CellStyle style = workbook.createCellStyle();
// 创建字体对象并设置为加粗
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
// 设置单元格背景颜色为灰色25%
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 设置单元格内容水平居中对齐
style.setAlignment(HorizontalAlignment.CENTER);
// 返回配置好的样式对象
return style;
}
/**
* 创建日期样式
*/
private CellStyle createDateCellStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
CreationHelper createHelper = workbook.getCreationHelper();
style.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-MM-dd HH:mm:ss"));
return style;
}
/**
* 计算需要的Sheet总数
* 根据总行数和每张工作表最大行数计算所需的工作表总数
*
* @param totalRows 总行数
* @return 需要的工作表总数,向上取整
*/
private int calculateTotalSheets(int totalRows) {
// 将总行数转换为double类型以确保浮点数除法
// 使用Math.ceil方法向上取整,确保所有行都能被包含
return (int) Math.ceil((double) totalRows / MAX_ROWS_PER_SHEET);
}
/**
* 参数校验
*/
private <T> void validateParams(List<T> dataList, OutputStream outputStream, Class<T> clazz) {
if (dataList == null) {
log.warn("数据列表为null");
throw new IllegalArgumentException("数据列表不能为null");
}
if (outputStream == null) {
log.warn("输出流为null");
throw new IllegalArgumentException("输出流不能为null");
}
if (clazz == null) {
log.warn("实体类类型为null");
throw new IllegalArgumentException("实体类类型不能为null");
}
}
/**
* 静默关闭输出流
*/
private void closeOutputStreamSilently(OutputStream outputStream) {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
log.warn("关闭输出流时发生异常", e);
}
}
}
/**
* 初始化字典缓存
*/
private static void initDictCache() {
// 性别字典
Map<String, String> genderDict = new HashMap<>();
genderDict.put("M", "男");
genderDict.put("F", "女");
DICT_CACHE.put("sys_user_sex", genderDict);
// 教育程度字典
Map<String, String> educationDict = new HashMap<>();
educationDict.put("1", "小学");
educationDict.put("2", "初中");
educationDict.put("3", "高中");
educationDict.put("4", "大专");
educationDict.put("5", "本科");
educationDict.put("6", "硕士");
educationDict.put("7", "博士");
DICT_CACHE.put("sys_education", educationDict);
}
}
5、编写service实现导出
/**
* 导出Excel
* 该方法用于将北京市人口数据导出为Excel文件并提供下载
*
* @param response HttpServletResponse对象,用于设置响应头和输出流
* @param populationBjReq 查询条件对象,包含分页和其他查询参数
* @throws IOException 可能抛出IO异常,如输出流操作失败
*/
@Override
public void exportExcel(HttpServletResponse response, PopulationBjReq populationBjReq) throws IOException {
long startTime = System.currentTimeMillis();
// 验证输入参数
if (response == null || populationBjReq == null) {
throw new IllegalArgumentException("Response or request parameter cannot be null");
}
// 设置响应头
String fileName = URLEncoder.encode("北京市人口数据", StandardCharsets.UTF_8.name()) + ".xlsx";
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
// 使用游标分页查询优化大数据量导出
List<PopulationBj> dataList = new ArrayList<>();
Long lastId = 0L;
final int pageSize = 100000; // 每页大小
boolean hasMoreData = true;
PopulationBj queryParams = new PopulationBj();
BeanUtils.copyProperties(populationBjReq, queryParams);
try {
// 分批查询数据
while (hasMoreData) {
List<PopulationBj> batchData = populationBjMapper.queryByCursor(lastId, pageSize, queryParams);
if (batchData.isEmpty() || dataList.size() >= 100000L ) {
hasMoreData = false;
} else {
dataList.addAll(batchData);
lastId = batchData.get(batchData.size() - 1).getId();
log.debug("已加载 {} 条数据", dataList.size());
}
}
log.info("数据查询完成,总数:{},耗时:{}ms",
dataList.size(),
System.currentTimeMillis() - startTime);
// 流式导出Excel
try (OutputStream outputStream = response.getOutputStream()) {
long exportStart = System.currentTimeMillis();
excelExportUtil.export(dataList, outputStream, PopulationBj.class);
log.info("数据导出完成,耗时:{}ms", System.currentTimeMillis() - exportStart);
}
} catch (Exception e) {
log.error("导出Excel时发生错误", e);
throw new RuntimeException("导出Excel失败", e);
}
log.info("导出完成,总耗时:{}ms", System.currentTimeMillis() - startTime);
}
6、编写DAO
List<PopulationBj> queryByCursor(@Param("lastId") Long lastId,
@Param("pageSize") Integer pageSize,
@Param("populationBj") PopulationBj populationBj);
<select id="queryByCursor" resultMap="PopulationBjMap">
select
id,personName,gender,age,id_card,address,education,occupation,create_time
from population_bj
<where>
<if test="lastId != null and lastId > 0">
and id > #{lastId}
</if>
<if test="populationBj.personname != null and populationBj.personname != ''">
and personName = #{populationBj.personname}
</if>
<if test="populationBj.gender != null and populationBj.gender != ''">
and gender = #{populationBj.gender}
</if>
<if test="populationBj.age != null">
and age = #{populationBj.age}
</if>
<if test="populationBj.idCard != null and populationBj.idCard != ''">
and id_card = #{populationBj.idCard}
</if>
<if test="populationBj.address != null and populationBj.address != ''">
and address = #{populationBj.address}
</if>
<if test="populationBj.education != null and populationBj.education != ''">
and education = #{populationBj.education}
</if>
<if test="populationBj.occupation != null and populationBj.occupation != ''">
and occupation = #{populationBj.occupation}
</if>
<if test="populationBj.createTime != null">
and create_time = #{populationBj.createTime}
</if>
</where>
order by id
limit #{pageSize}
</select>
什么是EasyExcel?
EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。
他能让你在不用考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能。
EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel 官网
EasyExcel要解决POI什么问题?
EasyExcel 主要解决 Apache POI 在处理大数据量 Excel 文件时的内存溢出问题 和性能瓶颈 。POI 采用全内存加载模式,当读取/写入数万行数据时会消耗大量内存,而 EasyExcel 通过逐行读写的流式处理机制(SAX模式解析),将内存占用从百兆级别降至几MB,同时保持高性能,特别适合处理百万级数据的导入导出场景。
EasyExcel的使用
pom文件:
<!-- EasyExcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>4.0.3</version>
</dependency>
导出Excel
实体类
package com.example.springboottest.entity;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 福建省人口信息表;数据表的PO对象
*
* @author : lw
* @date : 2025-8-19
*/
@Data
@HeadRowHeight(20) // 表头行高
@ContentRowHeight(15) // 内容行高
public class PopulationFj implements Serializable, Cloneable {
/**
* 主键ID,;
*/
@ExcelProperty("ID")
@ColumnWidth(10)
private Long id;
/**
* 姓名,;
*/
@ExcelProperty("姓名")
@ColumnWidth(15)
private String personname;
/**
* 性别(M-男,F-女),;
*/
@ExcelProperty("姓名")
@ColumnWidth(15)
private String gender;
/**
* 年龄,;
*/
@ExcelProperty("年龄")
@ColumnWidth(8)
private Integer age;
/**
* 身份证号,;
*/
@ExcelProperty("身份证号")
@ColumnWidth(25)
private String idCard;
/**
* 详细地址,;
*/
@ExcelProperty("详细地址")
@ColumnWidth(40)
private String address;
/**
* 教育程度(小学/初中/高中/大专/本科/硕士/博士),;
*/
@ExcelProperty("教育程度")
@ColumnWidth(15)
private String education;
/**
* 职业,;
*/
@ExcelProperty("职业")
@ColumnWidth(20)
private String occupation;
/**
* 创建时间,;
*/
@ExcelProperty("创建时间")
@ColumnWidth(20)
private Date createTime;
}
导出服务类
package com.example.springboottest.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.example.springboottest.entity.PopulationFj;
import com.example.springboottest.mapper.PopulationFjMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
/**
* 高性能Excel导出工具类(支持多线程分Sheet导出)
*
* 该工具类提供了三种导出方式:
* 1. 单线程导出:适合小数据量
* 2. 多线程导出:适合大数据量,支持分Sheet并行处理
* 3. 异步导出:适合超大数据量,在后台执行
*
* 使用EasyExcel库实现高性能的Excel导出功能,支持大数据量导出
* 通过多线程和分Sheet的方式提高导出效率
*/
@Slf4j
@Component
public class HighPerformanceExcelExporter {
// 依赖注入PopulationFjMapper用于数据库查询
private final PopulationFjMapper populationFjMapper;
// 自定义线程池执行器
private final Executor customTaskExecutor;
// 每Sheet最大行数(Excel2007+单个Sheet最多支持1048576行)
private static final int MAX_ROWS_PER_SHEET = 50000;
// 每个线程处理的数据量
private static final int ROWS_PER_THREAD = 10000;
/**
* 构造函数,注入必要的依赖
* @param populationFjMapper 数据库操作接口
* @param customTaskExecutor 自定义线程池
*/
public HighPerformanceExcelExporter(PopulationFjMapper populationFjMapper,
Executor customTaskExecutor) {
this.populationFjMapper = populationFjMapper;
this.customTaskExecutor = customTaskExecutor;
}
/**
* 通用导出方法(自动选择单线程或多线程)
* 根据数据量自动选择最优的导出方式
*
* @param response HTTP响应对象
* @param fileName 导出的文件名
* @throws IOException IO异常
*/
public void export(HttpServletResponse response, String fileName) throws IOException {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 获取总数据量
long total = populationFjMapper.count(new PopulationFj());
if (total <= ROWS_PER_THREAD * 2) {
log.info("数据量小,使用单线程导出");
// 小数据量使用单线程导出
singleThreadExport(response, fileName, total);
} else {
log.info("数据量大,使用多线程导出");
// 大数据量使用多线程导出
multiThreadExport(response, fileName, total);
}
stopWatch.stop();
log.info("Excel导出完成,总数据量:{},耗时:{}秒", total, stopWatch.getTotalTimeSeconds());
}
/**
* 单线程导出(适合小数据量)
* 将数据按Sheet分割,单线程顺序写入
*
* @param response HTTP响应对象
* @param fileName 导出的文件名
* @param total 总数据量
* @throws IOException IO异常
*/
private void singleThreadExport(HttpServletResponse response, String fileName, long total) throws IOException {
setResponseHeader(response, fileName);
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), PopulationFj.class).build();
try {
// 计算需要的Sheet数量
int sheetCount = (int) Math.ceil((double) total / MAX_ROWS_PER_SHEET);
for (int i = 0; i < sheetCount; i++) {
// 计算当前Sheet的偏移量和限制数
int offset = i * MAX_ROWS_PER_SHEET;
int limit = Math.min(MAX_ROWS_PER_SHEET, (int) (total - offset));
PopulationFj query = new PopulationFj();
List<PopulationFj> data = populationFjMapper.queryLimit(query, offset, limit);
WriteSheet writeSheet = EasyExcel.writerSheet(i, "Sheet" + (i + 1)).build();
excelWriter.write(data, writeSheet);
log.info("单线程导出进度:{}/{}", offset + data.size(), total);
}
} finally {
// 确保ExcelWriter被正确关闭
if (excelWriter != null) {
excelWriter.finish();
}
}
}
/**
* 多线程分Sheet导出(适合大数据量)
* 将数据按Sheet和线程分割,多线程并行写入
*
* @param response HTTP响应对象
* @param fileName 导出的文件名
* @param total 总数据量
* @throws IOException IO异常
*/
private void multiThreadExport(HttpServletResponse response, String fileName, long total) throws IOException {
setResponseHeader(response, fileName);
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), PopulationFj.class).build();
try {
// 计算需要的Sheet数量
int sheetCount = (int) Math.ceil((double) total / MAX_ROWS_PER_SHEET);
List<CompletableFuture<Void>> futures = new ArrayList<>();
// 遍历每个Sheet
for (int sheetIndex = 0; sheetIndex < sheetCount; sheetIndex++) {
final int currentSheet = sheetIndex;
// 计算当前Sheet的偏移量和限制数
int offset = currentSheet * MAX_ROWS_PER_SHEET;
int remaining = (int) (total - offset);
int limit = Math.min(MAX_ROWS_PER_SHEET, remaining);
// 每个Sheet再拆分为多个线程处理
int threadsPerSheet = (int) Math.ceil((double) limit / ROWS_PER_THREAD);
for (int threadIndex = 0; threadIndex < threadsPerSheet; threadIndex++) {
final int currentThread = threadIndex;
int threadOffset = offset + (currentThread * ROWS_PER_THREAD);
int threadLimit = Math.min(ROWS_PER_THREAD, (int) (total - threadOffset));
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
PopulationFj query = new PopulationFj();
List<PopulationFj> data = populationFjMapper.queryLimit(query, threadOffset, threadLimit);
synchronized (excelWriter) {
WriteSheet writeSheet = EasyExcel.writerSheet(currentSheet, "Sheet" + (currentSheet + 1)).build();
excelWriter.write(data, writeSheet);
}
log.info("多线程导出进度:Sheet {}/{}, 线程 {} - 已处理 {} 条",
currentSheet + 1, sheetCount, currentThread + 1, data.size());
} catch (Exception e) {
log.error("多线程导出异常", e);
throw new RuntimeException(e);
}
}, customTaskExecutor);
futures.add(future);
}
}
// 等待所有线程完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
} finally {
if (excelWriter != null) {
excelWriter.finish();
}
}
}
/**
* 异步导出方法(适合超大数据量,后台执行)
*/
@Async("customTaskExecutor")
public void asyncExport(String filePath, long total) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ExcelWriter excelWriter = EasyExcel.write(filePath, PopulationFj.class).build();
try {
int sheetCount = (int) Math.ceil((double) total / MAX_ROWS_PER_SHEET);
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (int sheetIndex = 0; sheetIndex < sheetCount; sheetIndex++) {
final int currentSheet = sheetIndex;
int offset = currentSheet * MAX_ROWS_PER_SHEET;
int remaining = (int) (total - offset);
int limit = Math.min(MAX_ROWS_PER_SHEET, remaining);
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
PopulationFj query = new PopulationFj();
List<PopulationFj> data = populationFjMapper.queryLimit(query, offset, limit);
synchronized (excelWriter) {
WriteSheet writeSheet = EasyExcel.writerSheet(currentSheet, "Sheet" + (currentSheet + 1)).build();
excelWriter.write(data, writeSheet);
}
log.info("异步导出进度:{}/{}", offset + data.size(), total);
} catch (Exception e) {
log.error("异步导出异常", e);
throw new RuntimeException(e);
}
}, customTaskExecutor);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
} finally {
if (excelWriter != null) {
excelWriter.finish();
}
}
stopWatch.stop();
log.info("异步Excel导出完成,总数据量:{},耗时:{}秒", total, stopWatch.getTotalTimeSeconds());
}
/**
* 设置响应头
*/
private void setResponseHeader(HttpServletResponse response, String fileName) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx");
}
}
控制层调用
package com.example.springboottest.controller;
import com.example.springboottest.util.HighPerformanceExcelExporter;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
@RestController
@RequestMapping("/api/population")
public class PopulationExportController {
private final HighPerformanceExcelExporter excelExporter;
public PopulationExportController(HighPerformanceExcelExporter excelExporter) {
this.excelExporter = excelExporter;
}
/**
* 同步导出(自动选择单线程或多线程)
*/
@GetMapping("/sync")
public void exportSync(HttpServletResponse response) throws IOException {
excelExporter.export(response, "福建人口数据");
}
}