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 的核心优势

  1. 跨平台性
    纯 Java 实现,无需安装 Office 软件即可操作文档。
  2. 功能全面
    • Excel:读写单元格、公式、图表、样式、合并单元格等。
    • Word:段落、表格、页眉页脚、书签等。
    • PPT:幻灯片、文本框、图片、动画等。
  1. 社区活跃
    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 &gt; #{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, "福建人口数据");
    }


}