بنقرة واحدة
streaming-export-safety
当代码涉及 Excel/CSV/JSON/PDF 大文件导出、批量序列化、内存里构建大对象时触发。防止 OOM、临时文件残留、同步导出阻塞 HTTP 线程等内存安全陷阱。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
当代码涉及 Excel/CSV/JSON/PDF 大文件导出、批量序列化、内存里构建大对象时触发。防止 OOM、临时文件残留、同步导出阻塞 HTTP 线程等内存安全陷阱。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
网关/代理/WAF/CDN 中间件的安全关键词匹配实现规范,防止纯子串匹配误判正常响应内容中的技术术语(如 Cloudflare、502、error)
涉及浏览器、编辑器、CDN/WAF、IM 平台、操作系统剪贴板、第三方 SaaS 等"外部黑盒系统"的代码编写或 bug 调试时触发。强制先抓真实环境数据再推理,避免连续 2 轮"凭代码推理"的修复 no-op。关键词:粘贴/复制异常、跨平台显示不一致、第三方 API 怪结果、CDN/WAF 拦截、本地复现失败、HTML→MD 转换丢属性。
前端开发规范,包含 Vue 3 编码规范、UI 风格约束、TypeScript 规范等
Java 开发规范,包含命名约定、异常处理、Spring Boot 最佳实践等
网关/代理/WAF/CDN 中间件响应判定与关键词匹配安全规范。适用于 proxy/gateway/waf/cdn/nginx/openresty/rewrite/redirect 等代码,防止把正常长正文中的 Cloudflare、502、bad gateway、error 等技术术语误判为上游错误。
涉及浏览器、编辑器、CDN/WAF、IM 平台、操作系统剪贴板、第三方 SaaS 等"外部黑盒系统"的代码编写或 bug 调试时触发。强制先抓真实环境数据再推理,避免连续 2 轮"凭代码推理"的修复 no-op。关键词:粘贴/复制异常、跨平台显示不一致、第三方 API 怪结果、CDN/WAF 拦截、本地复现失败、HTML→MD 转换丢属性。
| name | streaming-export-safety |
| description | 当代码涉及 Excel/CSV/JSON/PDF 大文件导出、批量序列化、内存里构建大对象时触发。防止 OOM、临时文件残留、同步导出阻塞 HTTP 线程等内存安全陷阱。 |
| paths | ["**/*Export*.{java,py,go,ts,js}","**/*Excel*.{java,py,go,ts,js}","**/*Report*.{java,py,go,ts,js}","**/*Download*.{java,py,go,ts,js}","**/*Backup*.{java,py,go,ts,js}","**/*Writer*.{java,py,go,ts,js}"] |
| keywords | ["SXSSFWorkbook","XSSFWorkbook","HSSFWorkbook","EasyExcel","openpyxl","excelize","ExcelJS","PDFBox","iText","ReportLab"] |
当一次性 in-memory 构建数据超过 1 万行 / 10 MB 时,必须切换为流式 API。 否则常见后果:OOM、Full GC 风暴、临时文件残留、HTTP 超时。
| 数据量级 | 推荐模式 | 备注 |
|---|---|---|
| ≤ 1 万行 / ≤ 5 MB | 内存构建 OK | XSSFWorkbook / pandas / 普通 JSON 序列化 |
| 1 万 ~ 10 万行 | 强烈建议流式 | 单机够用,但有 OOM 风险 |
| > 10 万行 / > 50 MB | 必须流式 + 异步 | 内存模型会爆 |
| 数据量未知(用户驱动) | 必须流式 + 上限保护 | 永远按最坏情况设计 |
核心判断:能不能预估上限?预估超过 1 万行就上流式。
代码评审看到以下模式立即怀疑:
new XSSFWorkbook() + 循环 createRow 超过 1 万次EasyExcel.write(...).sheet().doWrite(data) 中 data.size() > 10000openpyxl.Workbook()(未带 write_only=True)+ 循环 appendexcelize.NewFile() + SetCellValue 大量循环ExcelJS.Workbook()(未用 stream 子模块)// ❌ XSSF 全量内存(10w 行直接 OOM)
XSSFWorkbook wb = new XSSFWorkbook();
Sheet sheet = wb.createSheet();
for (int i = 0; i < 100_000; i++) {
Row row = sheet.createRow(i);
row.createCell(0).setCellValue("data");
}
// ✅ SXSSF 流式(windowSize=100,超出窗口写临时文件)
SXSSFWorkbook wb = null;
try {
wb = new SXSSFWorkbook(100);
wb.setCompressTempFiles(true);
Sheet sheet = wb.createSheet();
for (int i = 0; i < 100_000; i++) {
Row row = sheet.createRow(i);
row.createCell(0).setCellValue("data");
}
try (OutputStream os = new FileOutputStream("out.xlsx")) {
wb.write(os);
}
} finally {
if (wb != null) {
wb.dispose();
}
}
evaluator.evaluateAll())cloneSheet、不支持合并多个已写出的 Sheetjava.io.tmpdir,容器环境注意磁盘配额dispose(),否则临时文件残留// ✅ EasyExcel 默认就是流式,分批写入
EasyExcel.write("out.xlsx", DataDTO.class)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.sheet("data")
.doWrite(() -> {
// 分页拿数据,每次返回一批
return loadBatch(pageNo++, BATCH_SIZE);
});
| 语言 | 流式 API | 关键差异 |
|---|---|---|
| Python | openpyxl.Workbook(write_only=True) | ❌ 不能用 ws.cell(),只能 ws.append(row),不可回写 |
| Python | pandas.read_excel(chunksize=N) | 读端分批;写端用 ExcelWriter(engine='openpyxl') |
| Go | f.NewStreamWriter("Sheet1") (excelize v2.x) | 必须 sw.Flush(),否则数据丢失 |
| Node.js | new ExcelJS.stream.xlsx.WorkbookWriter({filename}) | 每个 row row.commit(),最后 workbook.commit() |
完整代码示例见
references/multi-lang-examples.md
// ❌ 一次性读入全部行
List<String> lines = Files.readAllLines(Paths.get("huge.csv"));
// ❌ 一次性构建全部字符串
StringBuilder sb = new StringBuilder();
for (Record r : records) sb.append(r.toCsv()).append("\n");
Files.write(path, sb.toString().getBytes());
// ✅ 读:BufferedReader 逐行
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line;
while ((line = reader.readLine()) != null) {
processLine(line);
}
}
// ✅ 写:OpenCSV CSVWriter 逐行 flush
try (CSVWriter writer = new CSVWriter(new FileWriter(path))) {
for (Record r : records) {
writer.writeNext(r.toArray());
}
}
| 语言 | 写 | 读 |
|---|---|---|
| Python | csv.writer(f).writerows(iter) | csv.reader(f) 迭代器 / pandas.read_csv(chunksize=N) |
| Go | csv.NewWriter(f) + w.Flush() 必须调用 | csv.NewReader(f).Read() 逐行 |
| Node.js | csv-stringify stream | csv-parse stream |
陷阱:Go 的 csv.Writer 不自动 flush,忘记调用 w.Flush() 数据全在 buffer 里。
// ❌ 整数组先序列化为 String,再写文件
List<Order> orders = repo.findAll(); // 100w 行
String json = objectMapper.writeValueAsString(orders); // OOM
Files.write(path, json.getBytes());
// ✅ Jackson JsonGenerator 流式写
try (JsonGenerator gen = objectMapper.getFactory()
.createGenerator(new FileOutputStream(path), JsonEncoding.UTF8)) {
gen.writeStartArray();
int page = 0;
Page<Order> orderPage;
do {
orderPage = repo.findAll(PageRequest.of(page++, 1000));
for (Order o : orderPage.getContent()) {
objectMapper.writeValue(gen, o);
}
} while (orderPage.hasNext());
gen.writeEndArray();
}
数据量极大时,输出格式选 JSONL(每行一个 JSON 对象)比标准 JSON 数组更友好:
{"id":1,"name":"a"}
{"id":2,"name":"b"}
下游可以一行一行解析,不需要先读完整个文件。
| 语言 | 流式 API |
|---|---|
| Python | orjson 单条 dumps + 写 .jsonl;或 ijson 读流式 |
| Go | json.NewEncoder(w).Encode(v) 逐对象(天然生成 JSONL) |
| Node.js | JSONStream.stringify() |
// ❌ 一次性把所有 Page 加到 Document,再 save
PDDocument doc = new PDDocument();
for (int i = 0; i < 10_000; i++) {
doc.addPage(buildPage(data.get(i))); // 全部 Page 在内存
}
doc.save(path);
// ✅ iText 流式:每个 Page 写完立即 flush
try (PdfDocument doc = new PdfDocument(new PdfWriter(path))) {
Document layout = new Document(doc);
for (Order o : orders) {
layout.add(buildParagraph(o));
if (orderIndex++ % 100 == 0) {
doc.getWriter().flush(); // 100 条 flush 一次
}
}
}
| 语言 | 流式 API | 关键操作 |
|---|---|---|
| Python | ReportLab Canvas | 每页 c.showPage() 后 c.save() 增量写 |
| Go | gofpdf.New(...) + pdf.AddPage() | OutputFileAndClose(path) 一次写出(内存大时考虑 unidoc 流式) |
| Node.js | PDFKit | doc.pipe(fs.createWriteStream(path)) |
注意:很多 PDF 库(如 PDFBox、jsPDF)没有真正的流式 API —— 整文档必须留在内存里。 数据量大时考虑:
同步导出 > 30s 会被 Nginx / 网关切断 504;HTTP 线程被长时间占用,影响其他请求。
// ❌ Controller 同步生成 + 返回
@GetMapping("/export/orders")
public ResponseEntity<byte[]> exportOrders() {
byte[] excel = buildExcelInMemory(); // 占用 HTTP 线程 60s+
return ResponseEntity.ok().body(excel);
}
// ✅ 接口立即返回任务 ID
@PostMapping("/export/orders")
public ExportTaskDTO export() {
String taskId = exportTaskService.submit(...);
return new ExportTaskDTO(taskId, "PROCESSING");
}
// ✅ 异步执行 + 写到对象存储
@Async("exportExecutor")
public void execute(String taskId, ...) {
try (SXSSFWorkbook wb = new SXSSFWorkbook(100)) {
// 流式写
File temp = streamToFile(wb);
String url = ossClient.upload(temp);
taskRepo.complete(taskId, url);
}
}
// ✅ 查询接口
@GetMapping("/export/tasks/{id}")
public ExportTaskDTO query(@PathVariable String id) { ... }
详见
async-task-patternskill —— 异步任务的状态机、超时、重试、幂等
try-finally / defer / with / using 中(SXSSF 必须 dispose())/tmp 可能很小)pageSize=999999)async-task-pattern —— 异步任务的状态机、超时、幂等query-performance-safety —— 防止导出时 N+1、IN 子句过长field-mapping-safety —— DTO 转换时字段映射一致性> 📋 本回复遵循:`streaming-export-safety` - [章节]