当前位置: 首页 > java >正文

Java报表导出框架

一、为什么要写这篇博客
做 ToB SaaS 报表系统,最头疼的就是“运营小姐姐今天想看团队巡检明细,明天想看激励明细,后天又想看预警明细”。
如果每个需求都 copy-paste 一个 ExportController,很快就会收获一座“代码屎山”。
去年 11 月我们团队用 模板方法 + 策略 + 工厂 三板斧重构了导出模块,至今零事故、零回归、零加班。今天把设计思路与关键代码分享出来,希望能帮大家少掉几根头发。


二、整体思路一句话

把“不变的流程”固化在模板里,把“善变的数据”交给策略,把“路由”交给工厂。


三、核心代码走读(混淆后)

  1. 模板方法:ExportTemplateService

@Slf4j
@Service
public class ExportTemplateService<T extends PageParam, R> {@Value("${export.temp.dir:/tmp/export}")private String tempDir;public <S> ApiResp<S> export(DownloadPolicy<T, R, S> policy, T req) {R resp = policy.newResp();/* 1. 统一参数校验 */policy.checkReq(req);/* 2. 查询首屏数据,顺便拿总数 */PageResult<?> page = policy.query(req);if (page.getTotal() == 0) {policy.onEmpty(resp);return ApiResp.error("EMPTY_DATA");}if (page.getTotal() > 10_000) {policy.onOverflow(resp);return ApiResp.error("TOO_MUCH_DATA");}/* 3. 真正写文件 */File tmp = null;String fileId = null;try {tmp = File.createTempFile(policy.filePrefix(req), ".xlsx", new File(tempDir));ExcelWriter writer = EasyExcel.write(tmp, policy.dataClass(req)).registerWriteHandler(new LongestMatchWidthStrategy()).build();WriteSheet sheet = EasyExcel.writerSheet(policy.sheetName(req)).build();/* 3-1 写第一页 */writer.write(page.getRows(), sheet);/* 3-2 写剩余页 */for (int i = 2; i <= page.getPages(); i++) {req.setPage(i);writer.write(policy.query(req).getRows(), sheet);}writer.finish();/* 4. 上传到对象存储 */fileId = OssClient.upload(tmp);} catch (Exception e) {log.error("export err", e);policy.onError(resp, e.getMessage());} finally {FileUtils.deleteQuietly(tmp);}/* 5. 回填下载地址 */if (StringUtils.isNotBlank(fileId)) {policy.fill(resp, fileId, OssClient.genUrl(fileId));return ApiResp.ok(policy.toFinal(resp));}return ApiResp.error("EXPORT_FAIL");}
}

亮点

  • 全流程 try-finally 保证临时文件必删。

  • 分页写文件,内存占用 O(1)。

  • 泛型 <T, R, S> 兼容所有业务请求/响应对象。

  1. 策略接口:DownloadPolicy

public interface DownloadPolicy<T, R, S> {R newResp();                       // 创建响应壳void checkReq(T req);              // 业务校验PageResult<?> query(T req);        // 查询数据Class<?> dataClass(T req);         // Excel 头模型String sheetName(T req);           // 工作表名String filePrefix(T req);          // 临时文件名前缀/* 钩子方法 */default void onEmpty(R resp) {}default void onOverflow(R resp) {}default void onError(R resp, String msg) {}default void fill(R resp, String fileId, String url) {}S toFinal(R resp);                 // 适配前端格式
}

亮点

  • 所有钩子给了默认空实现,策略类按需 Override。

  • 接口即文档,新人一眼看懂。

  1. 工厂 + 枚举:RouterFactory

@Component
public class RouterFactory {private final Map<ExportType, DataFetcher<?>> map = new ConcurrentHashMap<>();public RouterFactory(List<DataFetcher<?>> list) {list.forEach(f -> map.put(f.type(), f));}public PageResult<?> fetch(ExportType type, QueryParam q) {DataFetcher<?> fetcher = map.get(type);if (fetcher == null) throw new BizException("UNSUPPORTED_TYPE");return fetcher.query(q);}
}

枚举示例

@AllArgsConstructor
@Getter
public enum ExportType {TEAM_INSPECTION("TI", TeamInspectionVO.class),ITEM_DETAIL("ID", ItemDetailVO.class),REWARD_DETAIL("RD", RewardDetailVO.class);private final String code;private final Class<?> clazz;
}

亮点

  • Spring 启动时自动收集所有 DataFetcher Bean,零配置。

  • 枚举集中管理 code ↔ clazz ↔ desc,拒绝魔法字符串。

  1. 业务策略示例:TeamInspectionFetcher

@Slf4j
@Service
public class TeamInspectionFetcher implements DataFetcher<TeamInspectionVO> {@Resourceprivate TeamInspectionMapper mapper;@Overridepublic ExportType type() {return ExportType.TEAM_INSPECTION;}@Overridepublic PageResult<TeamInspectionVO> query(QueryParam q) {return mapper.selectByPage(q);}
}

亮点

  • 只关心“怎么拿数据”,其余全部交给模板与工厂。


四、可扩展性实战

需求改动点预计工时
新增“退货明细”导出① 新建 VO ② 新建 Fetcher ③ 枚举加一行10 min
导出上限调到 50,000修改 ExportTemplateService 常量30 s
增加 CSV 格式在模板里加策略分支 & 实现 CsvWriter2 h

五、性能 & 稳定性小贴士

  1. 临时目录挂载 tmpfs,IO 提升 30%。

  2. 分页大小动态化:小数据 5 000/页,大数据 500/页,内存更稳。

  3. 对象存储异步上传 + WebSocket 推送,体验升级。


六、FAQ

Q:为什么不用 SpringBatch?
A:Batch 重调度、重作业,报表导出多是“即时点查即走”,模板方法足矣。

Q:EasyExcel 遇到超大文件 OOM?
A:我们阈值 10 k 行 + 分页写盘,生产跑了半年单实例 2 G 堆无压力。

http://www.xdnf.cn/news/15398.html

相关文章:

  • 详解BIO,NIO,AIO
  • 【git fetch submodule报错】Errors during submodule fetch 如何解决?
  • 【Java EE】多线程-初阶 认识线程(Thread)
  • urlencode、html实体编码、unicode
  • 进程---基础知识+命令+函数(fork+getpid+exit+wait+exec)
  • ACL流量控制实验
  • 12.如何判断字符串是否为空?
  • 记字节前端面试一道简单的算法题
  • 游戏玩法的专利博弈
  • 大话数据结构之 <链表>(C语言)
  • 使用 keytool 在服务器上导入证书操作指南(SSL 证书验证错误处理)
  • 【DOCKER】-4 dockerfile镜像管理
  • Python数据容器-通用功能
  • grpo nl2sql qwen3 模型强化学习训练有效果的成立条件有哪些
  • java--ThreadLocal创建以及get源码解析
  • 131. Java 泛型 - 目标类型与泛型推断
  • RNN(循环神经网络)
  • js与vue基础学习
  • Cesium源码打包
  • 从数据库到播放器:Java视频续播功能完整实现解析
  • Netty编程模型介绍
  • 聚宽sql数据库传递
  • 【WPF】WPF 自定义控件 实战详解,含命令实现
  • Node.js + Express的数据库AB View切换方案设计
  • 渗透笔记1-4
  • vim扩展
  • Spring Boot Cucumber 测试报告嵌入方法
  • Linux 基础命令详解:从入门到实践(1)
  • 微前端框架深度对决:qiankun、micro-app、wujie 技术内幕与架构选型指南
  • MFC UI表格制作从专家到入门