2025-05-08🌱上海: ☀️ 🌡️+19°C 🌬️↖19km/h
# **Part008 通用 Excel 导出功能实战
# 1. 为什么(Why)
# 1.1 项目背景
part008
模块实现了一个基于 Java 的通用 Excel 导出框架,解决了企业应用中数据导出功能的常见需求。在实际业务系统中,数据导出是一个高频操作,如导出用户列表、订单记录、报表数据等。传统的 Excel 导出实现方式往往缺乏统一标准,不同模块的导出功能实现差异较大,代码复用性低,维护成本高。本模块设计了一套灵活、可扩展的 Excel 导出框架,通过统一的 API 和面向对象的设计,简化了 Excel 导出功能的开发难度,提高了代码的可复用性和可维护性。
# 1.2 解决的问题
-
重复开发问题:传统方式每个 Excel 导出功能都需要编写类似的代码,导致大量重复工作。
-
格式不统一:不同开发人员实现的导出功能,在 Excel 格式、样式、命名等方面缺乏统一标准。
-
扩展性差:硬编码的导出功能难以适应需求变更,如增加导出字段、修改格式等。
-
异常处理不完善:导出过程中的异常处理不统一,容易导致用户体验不佳。
-
大数据量支持不足:缺乏对大数据量导出的优化处理,可能导致内存溢出或性能问题。
# 2. 如何实现(How)
# 2.1 项目结构
part008
模块的项目结构如下:
part008/ | |
├── src/ | |
│ ├── main/ | |
│ │ ├── java/ | |
│ │ │ └── com/ | |
│ │ │ └── muzi/ | |
│ │ │ └── part8/ | |
│ │ │ ├── aspect/ # 切面拦截 | |
│ │ │ │ └── ExcelExportAspect.java # Excel导出切面 | |
│ │ │ ├── controller/ # 控制层 | |
│ │ │ │ └── UserController.java # 用户控制器 | |
│ │ │ ├── dto/ # 数据传输对象 | |
│ │ │ │ ├── User.java # 用户对象 | |
│ │ │ │ └── UserExportRequest.java # 用户导出请求 | |
│ │ │ ├── excel/ # Excel导出核心包 | |
│ │ │ │ ├── ExcelExportField.java # 导出字段定义 | |
│ │ │ │ ├── ExcelExportRequest.java # 导出请求基类 | |
│ │ │ │ ├── ExcelExportResponse.java # 导出响应对象 | |
│ │ │ │ ├── ExcelExportUtils.java # 导出工具类 | |
│ │ │ │ ├── ExcelHead.java # Excel表头定义 | |
│ │ │ │ └── ExcelSheet.java # Excel工作表定义 | |
│ │ │ ├── service/ # 服务层 | |
│ │ │ │ └── UserService.java # 用户服务 | |
│ │ │ └── utils/ # 工具类 | |
│ │ │ └── CollUtils.java # 集合工具类 | |
│ │ └── resources/ # 配置文件 | |
│ └── test/ # 测试类 | |
└── pom.xml # Maven配置文件 |
# 2.2 关键技术点
# 2.2.1 案例分析:面向对象的 Excel 导出模型设计
技术实现: 本模块设计了一套完整的 Excel 导出领域模型,通过面向对象的设计实现灵活的导出功能:
- 核心领域模型
// Excel导出请求基类
public class ExcelExportRequest {
private String excelName; // Excel文件名
private String sheetName; // 工作表名称
private List<ExcelExportField> fieldList; // 导出字段列表
// getter/setter 略
}
// Excel导出字段定义
public class ExcelExportField {
private String fieldName; // 字段名称(对象的属性名)
private String fieldDesc; // 字段描述(表头显示名)
// getter/setter 略
}
// Excel导出响应对象
public class ExcelExportResponse {
private String excelName; // Excel文件名
private List<ExcelSheet> sheetList; // 工作表列表
// getter/setter 略
}
// Excel工作表定义
public class ExcelSheet {
private String sheetName; // 工作表名称
private List<ExcelHead> headList; // 表头列表
private List<Map<String, String>> dataList; // 数据列表
// getter/setter 略
}
- 业务导出请求实现
// 用户导出请求(继承通用导出请求)
public class UserExportRequest extends ExcelExportRequest {
private List<Integer> userIdList; // 要导出的用户ID列表
// getter/setter 略
}
原理分析:
-
分层设计
-
将 Excel 导出功能拆分为多个层次的对象,每个对象负责特定职责
-
使用继承关系建立通用导出基类和业务导出请求之间的关系
-
通过组合关系构建 Excel 文档的结构 (工作簿、工作表、表头、数据)
-
-
灵活性与扩展性
-
业务模块只需继承通用基类,定义业务特定属性即可实现定制化导出
-
支持动态指定导出字段,实现按需导出
-
工作表定义支持多 Sheet 导出,满足复杂场景需求
-
-
映射关系
-
字段定义 (ExcelExportField) 建立了 Java 对象属性与 Excel 表头的映射关系
-
数据列表 (dataList) 使用 Map<String, String > 存储,键为字段名称,值为单元格内容
-
使用反射机制自动将 Java 对象属性值映射到 Excel 单元格
-
# 2.2.2 案例分析:导出工具类实现
技术实现: 本模块通过 ExcelExportUtils 工具类实现 Excel 导出的核心逻辑:
public class ExcelExportUtils {
// 构建Excel导出响应对象
public static ExcelExportResponse build(List<?> dataList, ExcelExportRequest request) {
// 创建响应对象
ExcelExportResponse result = new ExcelExportResponse();
result.setExcelName(request.getExcelName());
// 组装工作表列表
List<ExcelSheet> sheetList = new ArrayList<>();
result.setSheetList(sheetList);
// 创建工作表
ExcelSheet excelSheet = new ExcelSheet();
excelSheet.setSheetName(request.getSheetName());
// 设置表头
excelSheet.setHeadList(buildSheetHeadList(request));
// 设置数据
excelSheet.setDataList(buildSheetDataList(dataList, request));
sheetList.add(excelSheet);
return result;
}
// 根据导出请求构建表头列表
public static List<ExcelHead> buildSheetHeadList(ExcelExportRequest request) {
List<ExcelExportField> fieldList = request.getFieldList();
List<ExcelHead> excelHeadList = new ArrayList<>(fieldList.size());
for (ExcelExportField field : fieldList) {
ExcelHead head = new ExcelHead();
head.setFieldName(field.getFieldName());
head.setFieldDesc(field.getFieldDesc());
excelHeadList.add(head);
}
return excelHeadList;
}
// 根据数据列表和请求构建数据列表
public static List<Map<String, String>> buildSheetDataList(
List<?> dataList, ExcelExportRequest request) {
if (CollUtils.isEmpty(dataList)) {
return CollUtils.emptyArrayList();
}
List<Map<String, String>> sheetDataList = new ArrayList<>(dataList.size());
List<ExcelExportField> fieldList = request.getFieldList();
List<String> exportFieldNameList =
CollUtils.convertList(fieldList, ExcelExportField::getFieldName);
for (Object data : dataList) {
Map<String, String> dataMap = new HashMap<>();
for (String fieldName : exportFieldNameList) {
// 通过反射获取字段值
Object fieldValue = ReflectUtil.getFieldValue(data, fieldName);
dataMap.put(fieldName, convertToString(fieldValue));
}
sheetDataList.add(dataMap);
}
return sheetDataList;
}
// 将Excel写入HTTP响应
public static void writeExcelToResponse(ExcelExportResponse excelExportResult)
throws IOException {
HttpServletResponse response =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getResponse();
OutputStream outputStream = response.getOutputStream();
// 设置响应头
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setHeader("Content-Disposition", "attachment;filename=" +
URLEncodeUtil.encode(excelExportResult.getExcelName() + ".xlsx"));
// 写入Excel
write(excelExportResult, outputStream);
}
// 将Excel写入输出流
public static void write(ExcelExportResponse result, OutputStream outputStream) {
List<ExcelSheet> sheetList = result.getSheetList();
try (ExcelWriter writer = EasyExcel.write(outputStream).build()) {
for (int sheetNo = 0; sheetNo < sheetList.size(); sheetNo++) {
ExcelSheet excelSheet = sheetList.get(sheetNo);
// 构建EasyExcel需要的头和数据格式
List<List<String>> head = buildEasyExcelHead(excelSheet);
List<List<String>> dataList = buildEasyExcelDataList(excelSheet);
// 创建工作表并写入数据
WriteSheet writeSheet = EasyExcel
.writerSheet(sheetNo, excelSheet.getSheetName())
.head(head).build();
writer.write(dataList, writeSheet);
}
}
}
}
原理分析:
-
数据转换过程
-
将业务对象列表转换为 Excel 导出的标准模型
-
通过反射机制动态获取对象属性值,实现灵活映射
-
将 Java 对象的复杂结构转换为 EasyExcel 需要的二维表格结构
-
-
分步骤组装
-
先构建 Excel 导出响应对象 (ExcelExportResponse)
-
再构建工作表 (ExcelSheet),包括表头和数据
-
最后将数据转换为 EasyExcel 需要的格式并写入
-
-
输出处理
-
支持将 Excel 写入 HTTP 响应,实现浏览器下载
-
支持将 Excel 写入指定输出流,适应不同场景需求
-
设置适当的响应头,确保浏览器正确处理下载文件
-
# 2.2.3 案例分析:AOP 实现自动导出
技术实现: 本模块使用 Spring AOP 实现了自动 Excel 导出功能:
@Component
@Aspect
public class ExcelExportAspect {
@Around(value = "execution(* com.muzi.*Controller.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 执行原方法
Object result = proceedingJoinPoint.proceed();
// 判断返回值类型,如果是Excel导出响应,则自动处理导出
if (result instanceof ExcelExportResponse) {
// 下载Excel
ExcelExportUtils.writeExcelToResponse((ExcelExportResponse) result);
return null;
} else {
// 其他类型返回值正常返回
return result;
}
}
}
原理分析:
-
AOP 拦截
-
使用切面拦截所有 Controller 方法的返回值
-
对于 Excel 导出响应类型,自动处理导出逻辑
-
对于其他类型返回值,不影响正常处理流程
-
-
简化导出流程
-
业务代码只需关注数据准备和导出配置
-
无需编写导出响应处理代码,统一由切面处理
-
避免重复编写相似的导出代码
-
-
关注点分离
-
导出配置和数据准备由业务层负责
-
导出实现细节由框架层负责
-
业务代码和导出框架代码解耦
-
# 2.2.4 案例分析:用户导出实现
技术实现: 本模块实现了一个用户列表导出的示例:
// 控制器方法
@PostMapping("/userExport")
@ResponseBody
public ExcelExportResponse userExport(@RequestBody UserExportRequest userExportRequest) throws IOException {
LOGGER.info("userExportRequest:{}", JSONUtil.toJsonPrettyStr(userExportRequest));
return this.userService.userExport(userExportRequest);
}
// 服务实现
public ExcelExportResponse userExport(UserExportRequest request) {
List<Integer> userIdList = request.getUserIdList();
// 获取用户列表
List<User> userList;
if (CollectionUtil.isEmpty(userIdList)) {
userList = this.getUserList();
} else {
userList = this.getUserList(request.getUserIdList());
}
// 调用工具类构建导出响应
return ExcelExportUtils.build(userList, request);
}
原理分析:
-
业务流程
-
控制器接收客户端传来的导出请求 (包含文件名、字段配置等)
-
服务层根据请求参数查询需要导出的数据
-
调用导出工具构建导出响应对象,由 AOP 切面自动处理导出
-
-
动态字段导出
-
客户端可以指定需要导出的字段列表,实现按需导出
-
支持自定义表头名称,提高用户体验
-
支持按条件筛选导出数据 (示例中支持按用户 ID 列表导出)
-
-
简洁实现
-
完整的用户导出功能仅需几行代码
-
导出逻辑与业务逻辑分离,易于维护
-
通过继承复用通用导出框架,减少重复代码
-
# 3. 技术点详解(Detail)
# 3.1 EasyExcel 原理与优势
本模块基于阿里巴巴开源的 EasyExcel 库实现 Excel 导出功能:
-
EasyExcel 特点
-
基于 POI 实现,但大幅降低内存占用
-
采用 SAX 模式逐行读取,避免将整个 Excel 加载到内存
-
使用注解方式定义映射关系,简化开发
-
-
性能优势
-
内存占用低,适合大数据量导出
-
支持流式处理,避免 OOM 问题
-
写入性能高,支持批量写入优化
-
-
扩展能力
-
支持自定义样式、合并单元格等高级功能
-
支持 Excel 模板填充,适合复杂报表生成
-
提供丰富的事件监听接口,可以实现各种定制需求
-
# 3.2 面向对象设计与设计模式应用
本模块的设计应用了多种设计模式:
-
模板方法模式
-
ExcelExportUtils 类中的 build 方法定义了 Excel 构建的骨架算法
-
将构建表头、构建数据等步骤抽象为独立方法,便于扩展和复用
-
-
装饰器模式
-
使用 AOP 切面装饰控制器方法,增强其导出功能
-
不改变原有业务逻辑的情况下,添加 Excel 导出响应处理能力
-
-
策略模式
-
ExcelExportRequest 可视为导出策略的抽象
-
具体业务请求类 (如 UserExportRequest) 提供具体的导出策略实现
-
客户端可以灵活配置导出字段,实现不同的导出策略
-
-
建造者模式
-
ExcelExportUtils 的 build 方法实现了建造者模式
-
分步骤构建复杂的 Excel 导出响应对象
-
将构建过程与表示分离,使同样的构建过程可以创建不同的表示
-
# 3.3 反射与动态数据处理
本模块使用反射机制实现动态数据处理:
-
动态字段获取
-
使用 Hutool 工具库的 ReflectUtil 获取对象属性值
-
支持任意类型对象的属性读取,无需编写特定的 getter 调用
-
-
反射优化考量
-
反射操作性能较低,但在导出场景下影响相对有限
-
可考虑使用缓存或 Map 预处理减少反射调用次数
-
在大数据量场景下,应权衡反射便利性与性能影响
-
-
类型转换处理
-
将各种类型的属性值统一转换为字符串,便于 Excel 处理
-
处理 null 值情况,避免 NullPointerException
-
可扩展支持自定义类型转换逻辑,如日期格式化等
-
# 3.4 HTTP 响应与文件下载
本模块实现了将 Excel 文件通过 HTTP 响应下载的功能:
-
响应头设置
-
设置 Content-Type 为 "application/vnd.ms-excel",指示浏览器处理 Excel 文件
-
设置 Content-Disposition 头,指定文件名和下载行为
-
设置字符编码和跨域头,确保跨域场景下的正确处理
-
-
文件名编码
-
使用 URLEncodeUtil 对文件名进行 URL 编码,处理中文文件名问题
-
避免不同浏览器下文件名乱码的问题
-
-
流处理
-
直接将 Excel 写入响应输出流,避免临时文件
-
使用 try-with-resources 确保流正确关闭,防止资源泄漏
-
异常处理确保在出错情况下仍能给客户端正确的响应
-
# 4. 使用示例(Usage)
# 4.1 基本使用
// 控制器方法
@PostMapping("/userExport")
@ResponseBody
public ExcelExportResponse userExport(@RequestBody UserExportRequest request) {
// 设置导出配置
request.setExcelName("用户列表");
request.setSheetName("用户信息");
// 设置导出字段
List<ExcelExportField> fieldList = new ArrayList<>();
fieldList.add(new ExcelExportField("userId", "用户ID"));
fieldList.add(new ExcelExportField("userName", "用户名"));
fieldList.add(new ExcelExportField("age", "年龄"));
fieldList.add(new ExcelExportField("address", "地址"));
request.setFieldList(fieldList);
// 获取数据并构建导出响应
List<User> userList = userService.getUserList(request.getUserIdList());
return ExcelExportUtils.build(userList, request);
}
# 4.2 客户端调用示例
// 前端发起导出请求
async function exportUsers() {
const response = await fetch('/userExport', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
excelName: '用户列表',
sheetName: '用户信息',
fieldList: [
{ fieldName: 'userId', fieldDesc: '用户ID' },
{ fieldName: 'userName', fieldDesc: '用户名' },
{ fieldName: 'age', fieldDesc: '年龄' },
{ fieldName: 'address', fieldDesc: '地址' }
],
userIdList: [1, 2, 3] // 只导出ID为1,2,3的用户
})
});
// 处理文件下载
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '用户列表.xlsx';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}
# 4.3 多 Sheet 导出示例
public ExcelExportResponse exportMultiSheet(MultiSheetRequest request) {
// 创建响应对象
ExcelExportResponse response = new ExcelExportResponse();
response.setExcelName(request.getExcelName());
// 创建工作表列表
List<ExcelSheet> sheetList = new ArrayList<>();
response.setSheetList(sheetList);
// 添加第一个工作表(用户信息)
ExcelSheet userSheet = new ExcelSheet();
userSheet.setSheetName("用户信息");
userSheet.setHeadList(buildUserHeadList());
userSheet.setDataList(buildUserDataList(userService.getUserList()));
sheetList.add(userSheet);
// 添加第二个工作表(订单信息)
ExcelSheet orderSheet = new ExcelSheet();
orderSheet.setSheetName("订单信息");
orderSheet.setHeadList(buildOrderHeadList());
orderSheet.setDataList(buildOrderDataList(orderService.getOrderList()));
sheetList.add(orderSheet);
return response;
}
# 4.4 自定义样式示例
// 扩展ExcelExportUtils,添加样式支持
public static void writeWithStyle(ExcelExportResponse result, OutputStream outputStream) {
List<ExcelSheet> sheetList = result.getSheetList();
// 创建样式
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short) 12);
headWriteFont.setBold(true);
headWriteCellStyle.setWriteFont(headWriteFont);
// 设置内容样式
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
WriteFont contentWriteFont = new WriteFont();
contentWriteFont.setFontHeightInPoints((short) 11);
contentWriteCellStyle.setWriteFont(contentWriteFont);
// 应用样式
try (ExcelWriter writer = EasyExcel.write(outputStream)
.registerWriteHandler(new HorizontalCellStyleStrategy(
headWriteCellStyle, contentWriteCellStyle))
.build()) {
// 写入数据,同原方法
for (int sheetNo = 0; sheetNo < sheetList.size(); sheetNo++) {
// …省略数据写入代码
}
}
}
# 5. 总结与优化方向(Summary)
# 5.1 技术总结
本模块实现了一个灵活、可扩展的 Excel 导出框架:
-
设计了完整的 Excel 导出领域模型,实现了面向对象的导出功能
-
使用 EasyExcel 库作为底层实现,保证了大数据量下的导出性能
-
通过 AOP 切面实现了自动导出处理,简化了业务代码
-
支持动态字段配置、多 Sheet 导出等高级功能
# 5.2 优化方向
-
异步导出支持
-
对于大数据量导出,可以实现异步导出机制
-
先返回任务 ID,后台异步生成 Excel 文件
-
提供接口查询导出进度,完成后提供下载链接
-
-
导出模板支持
-
增加模板导出功能,支持复杂的预定义格式
-
通过模板文件定义样式、合并单元格等复杂布局
-
实现只需填充数据的模板导出功能
-
-
分批次导出优化
-
针对超大数据量导出,实现分批次查询数据
-
避免一次性加载全部数据导致内存压力
-
使用 EasyExcel 的分批写入功能,优化性能
-
-
导出权限控制
-
集成权限系统,控制用户可导出的字段和数据范围
-
根据用户角色动态生成导出字段列表
-
确保敏感数据安全,避免数据泄露风险
-
-
导出监控与统计
-
记录导出操作日志,包括导出人、导出内容、耗时等
-
实现导出性能监控,识别慢导出操作
-
提供导出次数、数据量等统计功能,优化系统资源分配
-