SpringBoot-集成POI和EasyExecl
什么是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+8private 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) {// 创建表头行,行号为0Row 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);}// 定期flushif (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异常,如输出流操作失败*/@Overridepublic 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);// 流式导出Exceltry (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">selectid,personName,gender,age,id_card,address,education,occupation,create_timefrom 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 idlimit #{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<>();// 遍历每个Sheetfor (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, "福建人口数据");}}