EasyExcel(FastExcel)Excel导出功能 技术文档
文档说明
本文档基于项目中实际开发的 cn.idev.excel(fastexcel) 导出功能编写,包含依赖配置、核心工具类、自定义处理器、数据转换器、导出实体配置、业务接口实现等全量技术细节,文档适用于开发维护与功能迭代参考。
注:本项目使用的fastexcel 1.3.0是EasyExcel的增强版(EasyExcel-Plus),API与阿里官方EasyExcel兼容,底层基于POI封装,核心优势为低内存占用、高性能、轻量化。
一、技术依赖配置
1.1 版本管理
xml
<fastexcel.version>1.3.0</fastexcel.version>
1.2 Maven核心依赖
xml
<!-- Excel导出核心依赖 EasyExcel-Plus(fastexcel) -->
<dependency>
<groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId>
<version>${fastexcel.version}</version>
</dependency>
无其他冗余依赖,组件本身已内置POI核心包,无需额外引入。
二、核心功能概述
本次实现的Excel导出为Web端HTTP流式导出,直接将Excel文件写入HttpServletResponse输出流,浏览器自动下载,核心能力包含:
-
支持任意实体类的结构化数据导出,泛型化封装,一行代码调用;
-
自动适配列宽,根据单元格内容长度动态计算最优列宽;
-
固定Excel表头(冻结第一行),滚动表格时表头常驻,提升体验;
-
自定义数据类型转换:布尔值转中文描述、时间戳转格式化时间字符串;
-
完善的异常处理与响应重置,导出失败时返回友好提示;
-
高性能大数据量导出:10万条数据导出耗时5-10秒,内存占用极低(流式写入,不加载全量数据到内存);
-
注解式配置导出样式:表头高度、内容行高度、指定列宽、忽略指定字段。
三、核心代码模块说明
3.1 包结构总览
plain
com.cn.datacollectionplatform.data
├── util # Excel导出核心工具类 EasyExcelUtil
├── handler # Excel写入处理器 FreezeHeaderWriteHandler(冻结表头)
├── converter # 自定义数据转换器(2个)
├── entity.response # Excel导出实体类 DataExcelDTO
└── controller # 业务导出接口(POST /api/v1/data/excel/sync)
3.2 核心工具类:EasyExcelUtil (com.xxx.data.util)
3.2.1 核心方法说明
java
public static <T> void exportWeb(HttpServletResponse response, String fileName, String sheetName, List<T> data, Class<T> clazz) throws IOException
方法作用 :Web环境下通用的Excel导出入口方法,一站式完成响应头配置+Excel写入+流式下载,泛型化支持所有实体类。
入参说明:
-
response:HttpServletResponse 响应对象,用于写入Excel流和配置响应头; -
fileName:导出文件的名称(无需后缀,工具类自动拼接.xlsx); -
sheetName:Excel工作表名称; -
data:待导出的业务数据集合 List; -
clazz:导出对应的实体类字节码对象(如DataExcelDTO.class)。
3.2.2 核心能力实现
-
响应头标准化配置 :私有方法
setResponseHeader完成浏览器下载的核心配置-
设置响应类型:
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet标准xlsx文件类型; -
统一编码:UTF-8,避免中文乱码;
-
中文文件名处理:
URLEncoder.encode编码+替换+为%20,解决浏览器兼容性问题; -
响应头:
attachment;filename*=utf-8''xxx.xlsx触发浏览器下载动作。
-
-
Excel写入核心配置链
EasyExcel.write(response.getOutputStream(), clazz) .sheet(sheetName) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 自动适配列宽 .registerWriteHandler(new FreezeHeaderWriteHandler()) // 冻结表头第一行 .doWrite(data); -
异常处理机制
-
捕获
IOException(流异常、文件写入异常等); -
导出失败时重置响应对象,清空原有响应内容;
-
响应类型改为JSON,返回
导出失败: 异常信息的友好提示; -
保证异常时不会返回损坏的Excel文件。
-
3.3 自定义Sheet处理器:FreezeHeaderWriteHandler (com.xxx.data.handler)
3.3.1 功能说明
实现cn.idev.excel.write.handler.SheetWriteHandler接口,在Excel工作表创建完成后,冻结第一行表头,滚动表格时表头不随内容滚动,提升大表格的阅读体验。
3.3.2 核心API说明
java
sheet.createFreezePane(0, 1, 0, 1);
POI原生API参数详解(核心必知):
-
第1个参数
colSplit:需要冻结的列数,0=不冻结列; -
第2个参数
rowSplit:需要冻结的行数,1=冻结第一行(表头行); -
第3个参数
leftmostColumn:冻结区域右侧的首列索引,固定传0即可; -
第4个参数
topRow:冻结区域下方的首行索引,固定传1即可。
3.4 自定义数据转换器 (com.xxx.data.converter)
核心规范:所有转换器均实现
cn.idev.excel.converters.Converter接口,用于Java数据类型 → Excel单元格展示数据 的自定义转换,本项目仅做导出,因此convertToJavaData方法返回null即可,无需实现读Excel的转换逻辑。
转换器生效方式:在导出实体类的字段上通过@ExcelProperty(converter=xxx.class)指定绑定。
3.4.1 布尔值转换器:BooleanCheckConverter
功能 :将Java中的Boolean类型值,转换为Excel中友好的中文描述
-
null → 空字符串
-
true → 必须
-
false → 非必须
3.4.2 时间戳转换器:TimestampConverter
功能 :将Java中的Long类型毫秒级时间戳 ,转换为Excel中标准的时间字符串格式yyyy-MM-dd HH:mm:ss
-
null → 空字符串
-
合法时间戳 → 格式化的日期时间字符串
-
核心API:基于Java8时间工具类
Instant+LocalDateTime+ZoneId实现,线程安全无时区问题。
3.5 导出实体类:DataExcelDTO (com.xxx.data.entity.response)
3.5.1 核心定位
该类是Java业务数据 → Excel表格列的核心映射载体,所有导出的列名、列顺序、列宽、数据转换、样式配置均通过该类的注解+字段配置实现,是Excel导出的核心配置类。
3.5.2 核心注解全量说明(优先级:局部注解 > 全局策略)
✅ 类级别的样式注解(作用于整个工作表)
-
@HeadRowHeight(20):设置Excel表头行的高度为20; -
@ContentRowHeight(18):设置Excel所有数据行的高度为18;
✅ 字段级别的核心注解
-
@ExcelIgnore:导出时忽略当前字段,该字段不会出现在Excel中; -
@ExcelProperty:核心注解,配置字段与Excel列的映射关系,必配参数:-
value:Excel中的列名(表头文字); -
index:Excel中的列索引,从0开始,必须连续且唯一,决定列的展示顺序; -
converter:可选,指定当前字段的自定义转换器,实现数据类型转换;
-
-
@ColumnWidth(xx):指定当前列的固定宽度,优先级高于全局的自动列宽策略。
3.5.3 字段配置规范
-
所有需要导出的字段,必须指定
@ExcelProperty的index属性,避免列错位; -
无需导出的字段,添加
@ExcelIgnore注解; -
需要特殊转换的字段,通过
converter绑定对应的转换器; -
字段类型支持基础类型、包装类,null值会被转换器处理为空字符串,保证Excel美观。
四、业务导出接口实现
4.1 接口地址与请求方式
java
@PostMapping("/api/v1/data/excel/sync")
-
请求方式:POST
-
请求体:JSON格式的查询参数
DataQueryVO(已做参数校验@Valid) -
返回值:无返回值(void),直接将Excel流写入HttpServletResponse,浏览器自动触发下载
4.2 核心业务流程
plain
1. 记录导出开始时间 → 用于统计耗时
2. 调用业务服务查询待导出数据:motorTestDataService.exportData(request) → 返回List<DataExcelDTO>
3. 生成唯一文件名:数据导出_+时间戳,避免文件重名
4. 调用EasyExcelUtil.exportWeb完成导出核心逻辑
5. 打印导出日志:文件名、数据量、耗时(毫秒)
6. 全局异常捕获:捕获所有异常,打印错误堆栈,避免接口报错导致前端无响应
4.3 性能指标
plain
日志打印:EasyExcel 导出成功,文件名:数据导出_1705xxxxxx,数据量:100000,耗时:7800 ms
核心性能优势 :fastexcel采用流式写入机制,数据逐条写入Excel流,不会将全量数据加载到JVM内存中,因此即使导出10万+数据,内存占用依然极低,不会出现OOM内存溢出问题。
五、核心开发规范与注意事项
✅ 必须遵循的开发规范
-
导出实体类的
@ExcelProperty(index)必须从0开始,连续且无重复,否则会导致Excel列错位、空列; -
自定义转换器必须实现
Converter接口,且在实体类字段上通过converter参数绑定,否则转换器不生效; -
时间戳字段必须是毫秒级 ,
TimestampConverter仅支持毫秒级Long类型,秒级时间戳会导致时间展示异常; -
中文文件名必须通过
URLEncoder.encode编码,否则会出现文件名乱码(如:文件名变成下划线/问号); -
导出接口必须返回
void,且不能在接口上添加@ResponseBody注解,否则会导致Excel流被覆盖,文件损坏无法打开;
✅ 优化与扩展建议
-
如需冻结Excel的列(如冻结前2列),只需修改
FreezeHeaderWriteHandler的createFreezePane参数:sheet.createFreezePane(2, 1, 2, 1); -
如需自定义更多数据类型转换(如枚举值转中文),可参照现有转换器的写法,实现
Converter接口即可; -
如需批量导出多个Sheet,可在
EasyExcel.write()后多次调用.sheet().doWrite(); -
大数据量查询时,建议业务层采用分页查询+流式拼接,避免一次性查询10万条数据导致查询阶段内存溢出;
-
如需自定义表头样式(如背景色、字体加粗),可新增
CellStyleWriteHandler处理器,注册到registerWriteHandler即可。
✅ 常见问题排查
-
Excel文件下载后打不开/提示损坏:
-
检查响应头是否配置正确的xlsx文件类型;
-
检查接口是否返回void,是否有多余的返回值;
-
检查异常时是否执行了
response.reset()重置响应。
-
-
中文列名/文件名乱码:
-
检查是否设置了
response.setCharacterEncoding("utf-8"); -
检查文件名是否通过
URLEncoder.encode编码并替换+为%20。
-
-
转换器不生效:
-
检查转换器是否正确实现
Converter接口; -
检查
@ExcelProperty是否指定了converter参数; -
检查转换器的
supportJavaTypeKey是否与字段类型一致。
-
-
列宽适配异常:
-
局部字段通过
@ColumnWidth指定了固定宽度,优先级高于全局的自动列宽策略; -
自动列宽策略会根据字段内容的最长长度适配,无需额外配置。
-
六、核心API/注解速查表
6.1 核心工具类API
java
// 导出核心调用
EasyExcelUtil.exportWeb(response, fileName, sheetName, dataList, DataExcelDTO.class);
6.2 核心注解速查表
| 注解 | 作用域 | 核心作用 |
|---|---|---|
| @ExcelIgnore | 字段 | 忽略当前字段,不导出 |
| @ExcelProperty | 字段 | 配置列名、列索引、绑定转换器 |
| @ColumnWidth | 字段 | 指定列的固定宽度 |
| @HeadRowHeight | 类 | 设置表头行高度 |
| @ContentRowHeight | 类 | 设置数据行高度 |
6.3 核心处理器
-
LongestMatchColumnWidthStyleStrategy:全局自动适配列宽策略; -
FreezeHeaderWriteHandler:自定义冻结表头策略。
七、代码附录
本章节整理所有核心代码文件,按包结构顺序排列,可直接复制到项目中使用。
7.1 Maven依赖配置
xml
<fastexcel.version>1.3.0</fastexcel.version>
<!-- Excel 工具(EasyExcel-PLus ) -->
<dependency>
<groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId>
<version>${fastexcel.version}</version>
</dependency>
7.2 Excel工具类:EasyExcelUtil
java
package com.cn.datacollectionplatform.data.util;
import cn.idev.excel.EasyExcel;
import cn.idev.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.cn.datacollectionplatform.data.handler.FreezeHeaderWriteHandler;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class EasyExcelUtil {
/**
* 导出 Excel (Web)
*/
public static <T> void exportWeb(HttpServletResponse response, String fileName, String sheetName, List<T> data, Class<T> clazz) throws IOException {
try {
// 1. 设置响应头
setResponseHeader(response, fileName);
// 2. 写入 Excel
EasyExcel.write(response.getOutputStream(), clazz)
.sheet(sheetName)
// 注册自动列宽策略
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
// 【新增】注册冻结表头策略
.registerWriteHandler(new FreezeHeaderWriteHandler())
.doWrite(data);
} catch (IOException e) {
// 重置响应
response.reset();
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().println("导出失败: " + e.getMessage());
}
}
private static 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, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx");
}
}
7.3 表头冻结处理器:FreezeHeaderWriteHandler
java
package com.cn.datacollectionplatform.data.handler;
import cn.idev.excel.write.handler.SheetWriteHandler;
import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder;
import org.apache.poi.ss.usermodel.Sheet;
/**
* 冻结第一行(表头)的处理器
*/
public class FreezeHeaderWriteHandler implements SheetWriteHandler {
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
// 获取底层的 POI Sheet 对象
Sheet sheet = writeSheetHolder.getSheet();
/**
* createFreezePane 参数说明:
* int colSplit: 冻结的列数 (0 表示不冻结列)
* int rowSplit: 冻结的行数 (1 表示冻结第 1 行,即表头)
* int leftmostColumn: 冻结区域右侧第一列的索引 (通常为 0)
* int topRow: 冻结区域下方第一行的索引 (通常为 1)
*/
sheet.createFreezePane(0, 1, 0, 1);
}
}
7.4 数据转换器:BooleanCheckConverter
java
package com.cn.datacollectionplatform.data.converter;
import cn.idev.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.ReadCellData;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
public class BooleanCheckConverter implements Converter<Boolean> {
@Override
public Class<?> supportJavaTypeKey() {
return Boolean.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Boolean convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
return null;
}
@Override
public WriteCellData<?> convertToExcelData(Boolean value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
if (value == null) {
return new WriteCellData<>("");
}
return new WriteCellData<>(value ? "必须" : "非必须");
}
}
7.5 数据转换器:TimestampConverter
java
package com.cn.datacollectionplatform.data.converter;
import cn.idev.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.ReadCellData;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class TimestampConverter implements Converter<Long> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public Class<?> supportJavaTypeKey() {
return Long.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Long convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
return null;
}
@Override
public WriteCellData<?> convertToExcelData(Long value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
if (value == null) {
return new WriteCellData<>("");
}
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneId.systemDefault());
return new WriteCellData<>(FORMATTER.format(dateTime));
}
}
7.6 导出实体类:DataExcelDTO
java
package com.cn.datacollectionplatform.data.entity.response;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import com.cn.datacollectionplatform.data.converter.BooleanCheckConverter;
import com.cn.datacollectionplatform.data.converter.TimestampConverter;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Excel导出实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@HeadRowHeight(20)
@ContentRowHeight(18)
public class DataExcelDTO {
// 加上这个注解,导出时会自动忽略该字段
@ExcelIgnore
private Long time;
@ExcelProperty(value = "上传批次号", index = 0)
@ColumnWidth(25)
private String uploadId;
@ExcelProperty(value = "订单号", index = 1)
@ColumnWidth(20)
private String orderNum;
@ExcelProperty(value = "产品序列号", index = 2)
@ColumnWidth(20)
private String productSn;
@ExcelProperty(value = "设备编码", index = 3)
@ColumnWidth(20)
private String equipmentSn;
@ExcelProperty(value = "设备名称", index = 4)
@ColumnWidth(15)
private String equipmentName;
@ExcelProperty(value = "工序说明", index = 5)
@ColumnWidth(15)
private String process;
@ExcelProperty(value = "线别", index = 6)
@ColumnWidth(10)
private String lineNo;
@ExcelProperty(value = "测试结果", index = 7)
@ColumnWidth(10)
private String testResult;
// 注意:Converter 类必须是 cn.idev.excel 包下的
@ExcelProperty(value = "测试时间", index = 8, converter = TimestampConverter.class)
@ColumnWidth(20)
private Long testTime;
@ExcelProperty(value = "所属公司", index = 9)
@ColumnWidth(15)
private String belongCompany;
@ExcelProperty(value = "是否必须", index = 10, converter = BooleanCheckConverter.class)
@ColumnWidth(15)
private Boolean check;
@ExcelProperty(value = "测试项描述", index = 11)
@ColumnWidth(15)
private String desc;
//@ExcelProperty(value = "数据类型", index = 12)
@ExcelIgnore
private String type;
@ExcelProperty(value = "单位", index = 12)
private String unit;
@ExcelProperty(value = "数值", index = 13)
@ColumnWidth(15)
private String value;
@ExcelProperty(value = "上限", index = 14)
private String upperLimit;
@ExcelProperty(value = "下限", index = 15)
private String lowerLimit;
}
7.7 业务导出接口
java
/**
* 同步导出Excel (基于 EasyExcel 改造)
* 性能优化:预计 10万条数据在 5-10秒内完成,内存占用极低。
*/
@PostMapping("/api/v1/data/excel/sync")
public void exportExcelSync(
@Valid @RequestBody DataQueryVO request,
HttpServletResponse response) {
long startTime = System.currentTimeMillis();
try {
// 1. 查询数据
// 注意:确保 motorTestDataService.exportData 返回的 List 已经是扁平化的 DataExcelDTO 列表
List<DataExcelDTO> dataList = motorTestDataService.exportData(request);
String fileName = "数据导出_" + System.currentTimeMillis();
// 3. 调用工具类导出 (一行代码搞定)
EasyExcelUtil.exportWeb(response, fileName, "Sheet1", dataList, DataExcelDTO.class);
long costTime = System.currentTimeMillis() - startTime;
log.info("EasyExcel 导出成功,文件名:{},数据量:{},耗时:{} ms", fileName, dataList.size(), costTime);
} catch (Exception e) {
log.error("同步导出失败", e);
}
}
文档编写完成时间:2026-01-17
文档适用版本:fastexcel 1.3.0