复用为王:四个 Excel 导出端点的快速搭建
为广东水文项目的一站一档与地下水模块新增 4 个 Excel 导出端点,覆盖墒情、咸情、地下水统计与地下水监测四种数据。整个过程没引入任何新依赖,完全复用项目已有的 @Excel 注解 + ExcelUtil.exportExcelToBytes() 模式 —— 写得越像现有代码,维护成本就越低。
🎧 文章导读
🎵 背景音乐
前言
水文监测系统的前端页面早就支持站点数据的在线查询,但用户一直没法把这些数据下载下来。本次迭代要补的就是这个短板 —— 在一站一档和地下水两个模块各加几个导出接口,让墒情、咸情、地下水统计和地下水监测的数据能直接落地成 Excel 文件。

图1:四个导出端点的模块归属与数据流向
需求背景
监测页面上的数据列表只能在线浏览,有两个痛点一直没解决:
- 现场调查没网络,工程师到野外站点检查时,需要带一份离线 Excel
- 上级汇报要数据,把表格直接贴进 Word 或 PPT,远比”打开页面截图”高效
需求来源是前端组提出来的,优先级中等。四个端点分别对应四种数据:
| 端点 | 归属模块 | 导出字段 |
|---|---|---|
GET /basic/station/archive/soil/{stcd}/export |
一站一档 | 时间、10cm/20cm/40cm 含水量 |
GET /basic/station/archive/salinity/{stcd}/export |
一站一档 | 时间、含氯度 |
GET /api/groundwater/stations/{stcd}/statistics/export |
地下水 | 时间、深埋(支持 day/month/year) |
GET /api/groundwater/stations/{stcd}/waterLevel/export |
地下水 | 时间、深埋 |
设计思考
项目里已经存在一套成熟的 Excel 导出模式:
- 创建带
@Excel注解的 VO 类,声明列名、宽度、排序 - 调用业务层已有方法拿数据
- 转换为 VO 列表,用
ExcelUtil.exportExcelToBytes()生成字节流 - 通过
FileUtils.setAttachmentResponseHeader()写 HTTP 响应
水情监测模块就是这套范式,跑得稳稳的。所以本次决策很明确 —— 完全复用,不引入任何新东西。
[!tip] 一致性带来的红利
风格统一、维护成本低,团队成员看一眼就知道在做什么。代价是如果将来需求复杂化(模板导出、大数据量流式写入),可能要换方案 —— 但当前没必要为这种未来需求买单。
在 VO 的设计上做了一个取舍:**地下水统计导出和监测导出共享同一个 GwStatsExportVO**。语义上它们是两种不同的数据,但导出字段都是”时间 + 深埋”两列,完全一样。为了”概念纯粹”维护两个一模一样的类是 over-engineering。反过来,墒情(三层土壤含水量)和咸情(含氯度)字段差异明显,各自独立 VO 才合理。
接口归属上,墒情/咸情放在 StationArchiveController,地下水两个放在 GroundwaterController,遵循”谁的数据谁负责“原则,而不是把所有导出集中到一个 Controller。

图2:三个导出 VO 的字段映射关系
实现方案
导出 VO 设计
VO 只包含需要进入 Excel 的字段,通过 @Excel 注解描述列。以最简洁的咸情导出 VO 为例:
1 |
|
两个细节值得记一下:
time用 String 而不是 Date —— 上游业务方法返回的就是格式化后的字符串,直接透传,避免二次格式化引入的时区问题sort显式声明列顺序 —— 不依赖字段声明顺序,无论代码怎么改,输出列顺序都稳定
端点核心逻辑
四个端点的代码结构几乎完全一致,以咸情导出为代表:
1 | public void exportSalinity( String stcd, |
关键就在 query.setPaginated(false) 这一行。复用的业务方法本身是支持分页的,通过这个标志位告诉它”全量返回”,既复用了查询逻辑,又避免了导出被分页截断。这是最便宜的复用方式 —— 改一个参数,而不是重写一套查询。
错误处理三层结构
| 场景 | HTTP 状态 | 处理位置 |
|---|---|---|
| 查询无数据 | 404 | Controller 显式判断 |
参数非法(如 type 不属于 day/month/year) |
400 | Controller 显式校验 |
| 服务端异常 | 500 | 全局兜底 |
所有错误响应统一为 JSON 格式 {"msg":"...","code":...},前端可以一套逻辑处理全部错误场景。
遇到的几个小坑
[!bug] writeExcelResponse 参数顺序写反
第一次写墒情导出时,把参数传成了(response, fileName, excelBytes),实际签名是(response, excelBytes, fileName)。方法名暗示”往 response 里写 excel”,字节流应该在前。编译直接报错,所以定位很快 —— 类型不同的参数,放反了至少编译会救你一命。
[!warning]
mvn clean compile在 Windows 下偶尔卡死
target 目录下的banner.txt有时被进程占着文件锁,clean 删不掉就报错。解决办法:用mvn compile(不 clean),因为编译本身是增量的,class 文件能正常更新,不需要先删整个 target。
[!info] 编译完必须重启应用
看似常识,但快速迭代时容易忘:改完代码必须重启 JVM 才能让新逻辑生效,否则会陷入”代码改了,行为没变”的困惑里反复怀疑人生。

图3:四个端点的测试覆盖矩阵
测试与验证
用 curl 打本地接口(端口 8086),覆盖正常和异常两类场景:
1 | # 墒情导出 - 站 80100600 |
经验总结
复盘下来,这次开发的几个收获:
- 找到已有模式比创造新模式更值得 —— 复用
@Excel+ExcelUtil让 4 个端点的实现成本变得几乎线性可加,出问题的排查方式都一样 - 共享 VO 的边界要看导出字段而不是业务语义 ——
GwStatsExportVO同时承担统计和监测两种场景,因为它们的字段完全一致;而墒情和咸情字段不同,就老老实实写两个 VO - 复用业务方法的开关参数(如
paginated)是最便宜的扩展点 —— 不写新方法,只切换参数,就能把分页查询变成全量导出 - 错误响应格式统一(都是
{msg, code})能极大降低前端处理成本
后续可以考虑的方向
- 数据量上升到几万条时,引入 SXSSF 流式写入,避免内存溢出
- 前端加导出进度提示,虽然现在数据量不大,但体验更友好
- 把通用导出逻辑抽到一个
ExportSupport工具类里,减少 controller 里的样板代码
结语
这次迭代的核心不是”造轮子”,而是”在正确的位置上重用已有的轮子”。四个端点的代码结构高度一致,这种一致性本身就是项目长期可维护性的一部分。