You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Java使用OpenPDF生成PDF仅IDM可下载问题求解

问题根因
  • 响应体冲突:当前方法既通过response.getOutputStream()写入了PDF字节流,又返回了带字符串内容的ResponseEntity,导致最终响应内容是PDF字节+额外字符串,整体格式不合法,浏览器无法识别为可下载的PDF文件。
  • 响应头缺失:缺少缓存控制头、跨域头暴露配置,部分浏览器无法准确判断响应内容类型,无法触发下载逻辑。
  • 文件名编码问题:原文件名未做标准化编码,含特殊字符时会导致Content-Disposition头失效。
  • 导出方法流处理不完整:仅关闭了Document对象,未关闭PdfWriter,可能存在PDF字节未完全写入响应流的问题。
  • 前端调用方式错误:如果是AJAX调用接口未指定blob响应类型,浏览器会默认按文本解析响应,直接输出字节码。
可行解决方案

1. 修复Controller层代码

@CrossOrigin(origins = "*", exposedHeaders = "Content-Disposition") // 跨域场景下暴露文件头给前端
@ApiOperation(value = "Generate Customer Request PDF ")
@PostMapping(path = { "/customer/generatePDF" }, 
    consumes = MediaType.APPLICATION_JSON_VALUE,
    produces = MediaType.APPLICATION_PDF_VALUE)
@ApiResponses(value = { @ApiResponse(code = 200, message = "Successful"),
        @ApiResponse(code = 400, message = "Bad Request, Validation Errors, ...", response = BadRequest.class),
        @ApiResponse(code = 401, message = "Unauthorized", response = Unauthorized.class),
        @ApiResponse(code = 500, message = "Internal server error", response = InternalError.class) })
// 改为void,不要返回额外的ResponseEntity内容,避免响应体冲突
public void generateCustomerRequestPDF(@RequestBody CustomerRequestDto customersRequestDto,
                                                    HttpServletResponse response) throws BusinessException, DocumentException, IOException, ParseException, TransformerException{
    
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
    // create PDF object
    List<CustomerRequestPDF> requests = new ArrayList<>();
    CustomerRequestPDF c = new CustomerRequestPDF();
    c.setName(customersRequestDto.getCusName());
    c.setAmount(customersRequestDto.getAmount()+" "+customersRequestDto.getCurrencySymbol());
    c.setPaymentDate( customersRequestDto.getCompleteAt().format(formatter) );
    c.setPaymentType(customersRequestDto.getOperationTypeName());
    c.setPaymentReference(customersRequestDto.getRrn());
    c.setPaymentNature(customersRequestDto.getGroupStatus());
    requests.add(c);
    
    CustomerPDFExporter exporter = new CustomerPDFExporter(requests);
    
    // 先重置响应,避免之前的缓存内容干扰
    response.reset();
    // 标准化编码文件名,兼容所有浏览器
    String fileName = URLEncoder.encode("recu_" + c.getPaymentReference() + "_"+ LocalDateTime.now() + ".pdf", StandardCharsets.UTF_8.name());
    response.setContentType("application/pdf");
    response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName);
    // 添加缓存控制头
    response.setHeader("Pragma", "no-cache");
    response.setHeader("Cache-Control", "no-cache");
    
    exporter.export(response);
    // 手动刷新输出流,保证所有字节写入响应
    response.getOutputStream().flush();
}

2. 修复导出方法的流处理逻辑

public void export(HttpServletResponse response) throws DocumentException, IOException {
    Document document = new Document(PageSize.A4);
    // 持有PdfWriter引用后续关闭
    PdfWriter writer = PdfWriter.getInstance(document, response.getOutputStream());
    
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
     
    document.open();
    Font font = FontFactory.getFont(FontFactory.HELVETICA_BOLD);
    font.setSize(25);
    font.setColor(Color.BLUE);
    
    // titre du recu 
    Paragraph p = new Paragraph("Recu du paiement client", font);
    p.setAlignment(Paragraph.ALIGN_CENTER);
    document.add(p);
    
    // date du recu
    font.setSize(12);
    font.setColor(Color.BLACK);
    Paragraph p2 = new Paragraph("Recu le : "+LocalDateTime.now().format(formatter), font);
    p2.setAlignment(Paragraph.ALIGN_LEFT);
    p2.setSpacingBefore(50);
    document.add(p2);
    

    String nature = "";
    String nom = "";
    String montant = "";
    String date = "";
    String type = "";
    String rrn = "";
    for(CustomerRequestPDF c : requests) {
        if(c.getPaymentNature().equals("L")) {
            nature = "Paiement effectué en masse";
        }
        else {
            nature = "Paiement effectué individuel";
        }
        nom = c.getName();
        montant = c.getAmount();
        date = c.getPaymentDate();
        type = c.getPaymentType();
        rrn = c.getPaymentReference();
    }
    
    Paragraph p3 = new Paragraph(nature, font);
    p3.setAlignment(Paragraph.ALIGN_LEFT);
    document.add(p3);
    
    // informations du paiement
    font.setSize(13);
    Paragraph p4 = new Paragraph("Nom Client    :    "+nom, font);
    p4.setSpacingBefore(50);
    document.add(p4);
    Paragraph p5 = new Paragraph("Montant    :    "+montant, font);
    document.add(p5);
    Paragraph p6 = new Paragraph("Date Paiement    :    "+date, font);
    document.add(p6);
    Paragraph p7 = new Paragraph("Nature Paiement    :    "+type, font);
    document.add(p7);
    Paragraph p8 = new Paragraph("Reference Paiement    :    "+rrn, font);
    document.add(p8);
    
    // 按顺序关闭资源,保证所有字节写入
    document.close();
    writer.close();
}

3. 前端AJAX调用适配(非表单直接提交场景)

如果是通过axios/fetch等异步方式调用接口,需指定blob响应类型并手动触发下载:

axios.post('/customer/generatePDF', requestParams, {
  responseType: 'blob' // 必须指定响应类型为blob,否则浏览器会按文本解析
}).then(res => {
  // 从响应头提取文件名
  const disposition = res.headers['content-disposition']
  let fileName = 'receipt.pdf'
  if (disposition) {
    const match = disposition.match(/filename\*=UTF-8''(.+)/)
    if (match) fileName = decodeURIComponent(match[1])
  }
  // 生成临时下载链接触发下载
  const blob = new Blob([res.data], { type: 'application/pdf' })
  const url = window.URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = fileName
  a.click()
  window.URL.revokeObjectURL(url)
})

内容的提问来源于stack exchange,提问作者MK S2M

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 13:39:00