Spring+PDFBox实现PDF流式合并并实时返回客户端的技术问询
如何用PDFBox实现PDF合并时的实时流式输出
可以通过PDFBox的增量PDF更新机制实现合并过程中实时向客户端流式返回结果,既避免全量加载文档到内存,又能让客户端逐步接收数据。
之前方案的问题分析
- 原代码的
mergeDocuments方法(即使使用临时文件模式):会先将合并后的完整文档写入临时存储,再一次性输出到响应流,客户端需等待合并完成才能收到数据。 - 直接使用
appendDocument+全量保存:目标PDDocument会在内存中累积所有页面和资源,随着文件数量/体积增大,内存占用飙升导致堆溢出。
解决方案代码
修改原Spring接口方法,利用增量保存实现流式输出:
@RequestMapping(value = "/v2/mergePdfs", method = RequestMethod.POST, consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}, produces = {MediaType.APPLICATION_PDF_VALUE, MediaType.APPLICATION_JSON_UTF8_VALUE}) public ResponseEntity<StreamingResponseBody> mergePdfsStreaming(@RequestPart @NotEmpty List<MultipartFile> pdfsToMerge, @RequestParam(value = "correlationId", required = false) String correlationId, @RequestParam(value = "resultFilename", defaultValue = "merged.pdf") String resultFilename) throws IOException { LOGGER.info("[ID: {}] Received a /v2/mergePdfs request", correlationId); StreamingResponseBody responseBody = outputStream -> { PDDocument targetDoc = null; try { LOGGER.info("[ID: {}] Beginning to merge {} PDFs into 1 PDF (streaming mode).", correlationId, pdfsToMerge.size()); for (int i = 0; i < pdfsToMerge.size(); i++) { MultipartFile pdfFile = pdfsToMerge.get(i); try (PDDocument sourceDoc = PDDocument.load(pdfFile.getInputStream())) { if (targetDoc == null) { // 初始化目标文档为第一个PDF,写入基础内容到输出流 targetDoc = sourceDoc; targetDoc.save(outputStream); outputStream.flush(); LOGGER.info("[ID: {}] Sent first PDF content to client.", correlationId); } else { // 遍历源文档所有页面,添加到目标文档 for (PDPage page : sourceDoc.getPages()) { targetDoc.addPage(page); } // 增量保存:仅写入新增的内容到输出流 targetDoc.saveIncremental(outputStream); outputStream.flush(); LOGGER.info("[ID: {}] Sent {}th PDF content incrementally to client.", correlationId, i + 1); } } } LOGGER.info("[ID: {}] All PDFs merged and streamed to client.", correlationId); } catch (Exception e) { LOGGER.error(String.format("[ID: %s] Couldn't merge all PDFs.", correlationId), e); throw new RuntimeException(e); } finally { if (targetDoc != null) { targetDoc.close(); } } }; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_PDF); headers.setContentDispositionFormData("attachment", resultFilename); headers.setCacheControl("must-revalidate, post-check=0, pre-check=0"); return new ResponseEntity<>(responseBody, headers, HttpStatus.OK); }
关键细节说明
- 初始化基础文档:将第一个PDF作为目标文档的基础,直接写入输出流,这是文档的完整初始内容。
- 增量保存逻辑:后续每个PDF添加页面后,调用
saveIncremental(outputStream),仅将新增的页面和资源数据写入输出流,而非整个文档。 - 实时刷新流:每次写入后调用
outputStream.flush(),确保数据立即发送到客户端,实现实时流式传输。 - 内存控制:每个源文档处理完成后立即关闭(通过try-with-resources),目标文档仅保留当前结构,不会累积所有原始PDF内容,内存占用维持在较低水平。
注意事项
- 若源PDF为多页文档,需循环遍历
sourceDoc.getPages()逐个添加到目标文档。 - 现代客户端默认支持分块传输,Spring的
StreamingResponseBody会自动设置Transfer-Encoding: chunked响应头。 - 增量PDF结构合法,部分PDF阅读器支持边下载边查看,可配合客户端实现进度展示。
内容的提问来源于stack exchange,提问作者budikpet
相关产品推荐
相关产品推荐

