SpringBoot 实现 PDF 导出解决方案

导语

PDF 导出是企业应用中常见的功能需求,如生成报表、合同、发票、证书等。SpringBoot 作为主流的 Java 后端框架,提供了多种实现 PDF 导出的方案。本文将深入探讨 SpringBoot 中实现 PDF 导出的各种方法,包括技术选型、实现细节、性能优化和最佳实践。

一、PDF 导出技术选型

1.1 主流 PDF 库比较

库名称许可证特点适用场景
iText 7AGPL/商业功能强大,支持复杂文档企业级应用,复杂报表
OpenPDFLGPLiText 5 的开源分支中小型应用,简单报表
Apache PDFBoxApache 2.0功能丰富,支持 PDF 操作PDF 解析和生成
Flying SaucerLGPL基于 XHTML/CSS 生成 PDF基于模板的文档
JasperReportsLGPL强大的报表引擎复杂报表,数据可视化

1.2 技术选型建议

选择因素

  • 功能需求:是否需要复杂布局、表单、图表等
  • 许可证:是否需要商业使用
  • 性能要求:处理大量数据的效率
  • 学习曲线:开发和维护成本
  • 社区支持:文档和资源的丰富程度

推荐方案

  • 小型应用:OpenPDF 或 Flying Saucer
  • 中型应用:iText 7 社区版
  • 大型企业应用:iText 7 商业版或 JasperReports

二、基于 iText 7 的实现方案

2.1 核心依赖

<dependencies>
    <!-- iText 7 核心 -->
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>itext7-core</artifactId>
        <version>7.2.5</version>
    </dependency>
    
    <!-- 中文字体支持 -->
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>font-asian</artifactId>
        <version>7.2.5</version>
    </dependency>
    
    <!-- 条形码支持 -->
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>barcodes</artifactId>
        <version>7.2.5</version>
    </dependency>
</dependencies>

2.2 基础 PDF 生成

PdfGenerator.java

@Service
public class PdfGenerator {
    
    /**
     * 生成基础 PDF 文档
     */
    public byte[] generateBasicPdf(String title, String content) throws Exception {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        
        // 创建 PDF 文档
        PdfDocument pdfDoc = new PdfDocument(new PdfWriter(outputStream));
        Document document = new Document(pdfDoc);
        
        // 设置中文字体
        PdfFont font = PdfFontFactory.createFont("STSongStd-Light", "UniGB-UCS2-H", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
        document.setFont(font);
        
        // 添加标题
        Paragraph header = new Paragraph(title)
            .setFontSize(18)
            .setBold()
            .setAlignment(TextAlignment.CENTER);
        document.add(header);
        
        // 添加内容
        Paragraph body = new Paragraph(content)
            .setFontSize(12)
            .setLeading(20)
            .setFirstLineIndent(20);
        document.add(body);
        
        // 关闭文档
        document.close();
        
        return outputStream.toByteArray();
    }
}

2.3 复杂 PDF 生成

AdvancedPdfGenerator.java

@Service
public class AdvancedPdfGenerator {
    
    /**
     * 生成带表格的 PDF
     */
    public byte[] generatePdfWithTable(List<Order> orders) throws Exception {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        
        // 创建 PDF 文档
        PdfDocument pdfDoc = new PdfDocument(new PdfWriter(outputStream));
        Document document = new Document(pdfDoc);
        
        // 设置中文字体
        PdfFont font = PdfFontFactory.createFont("STSongStd-Light", "UniGB-UCS2-H", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
        document.setFont(font);
        
        // 添加标题
        document.add(new Paragraph("订单报表")
            .setFontSize(16)
            .setBold()
            .setAlignment(TextAlignment.CENTER)
            .setMarginBottom(20));
        
        // 创建表格
        Table table = new Table(5);
        table.setWidth(UnitValue.createPercentValue(100));
        
        // 添加表头
        table.addHeaderCell(new Cell().add(new Paragraph("订单号").setBold()));
        table.addHeaderCell(new Cell().add(new Paragraph("客户名称").setBold()));
        table.addHeaderCell(new Cell().add(new Paragraph("订单金额").setBold()));
        table.addHeaderCell(new Cell().add(new Paragraph("下单时间").setBold()));
        table.addHeaderCell(new Cell().add(new Paragraph("状态").setBold()));
        
        // 添加数据行
        for (Order order : orders) {
            table.addCell(order.getOrderId());
            table.addCell(order.getCustomerName());
            table.addCell(String.format("¥%.2f", order.getAmount()));
            table.addCell(order.getOrderTime().toString());
            table.addCell(order.getStatus());
        }
        
        // 添加表格到文档
        document.add(table);
        
        // 添加页脚
        document.add(new Paragraph("生成时间: " + new Date())
            .setFontSize(10)
            .setAlignment(TextAlignment.RIGHT)
            .setMarginTop(30));
        
        // 关闭文档
        document.close();
        
        return outputStream.toByteArray();
    }
}

三、基于模板的 PDF 导出

3.1 使用 Flying Saucer

依赖配置

<dependency>
    <groupId>org.xhtmlrenderer</groupId>
    <artifactId>flying-saucer-pdf-itext5</artifactId>
    <version>9.1.22</version>
</dependency>

实现代码

@Service
public class TemplatePdfGenerator {
    
    /**
     * 基于 HTML 模板生成 PDF
     */
    public byte[] generatePdfFromTemplate(String htmlTemplate, Map<String, Object> data) throws Exception {
        // 填充模板数据
        String filledHtml = fillTemplate(htmlTemplate, data);
        
        // 创建 PDF
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ITextRenderer renderer = new ITextRenderer();
        
        // 注册中文字体
        renderer.getFontResolver().addFont("path/to/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
        
        // 设置 HTML 内容
        renderer.setDocumentFromString(filledHtml);
        renderer.layout();
        renderer.createPDF(outputStream);
        
        return outputStream.toByteArray();
    }
    
    /**
     * 填充模板数据
     */
    private String fillTemplate(String template, Map<String, Object> data) {
        // 使用 Freemarker 或 Thymeleaf 填充模板
        // 这里简化处理,实际项目中应使用模板引擎
        String result = template;
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            result = result.replace("${" + entry.getKey() + "}", entry.getValue().toString());
        }
        return result;
    }
}

3.2 HTML 模板示例

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <style>
        body {
            font-family: SimSun;
            font-size: 12px;
        }
        .header {
            text-align: center;
            margin-bottom: 20px;
        }
        .title {
            font-size: 16px;
            font-weight: bold;
        }
        .table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
        }
        .table th, .table td {
            border: 1px solid #000;
            padding: 8px;
            text-align: center;
        }
        .footer {
            margin-top: 30px;
            text-align: right;
            font-size: 10px;
        }
    </style>
</head>
<body>
    <div class="header">
        <div class="title">${title}</div>
        <div>${subtitle}</div>
    </div>
    
    <table class="table">
        <tr>
            <th>姓名</th>
            <th>部门</th>
            <th>职位</th>
            <th>入职时间</th>
        </tr>
        <tr>
            <td>${name}</td>
            <td>${department}</td>
            <td>${position}</td>
            <td>${hireDate}</td>
        </tr>
    </table>
    
    <div class="footer">
        生成时间: ${generateTime}
    </div>
</body>
</html>

四、SpringBoot 集成实现

4.1 控制器实现

PdfController.java

@RestController
@RequestMapping("/api/pdf")
public class PdfController {
    
    @Autowired
    private PdfGenerator pdfGenerator;
    
    @Autowired
    private AdvancedPdfGenerator advancedPdfGenerator;
    
    @Autowired
    private TemplatePdfGenerator templatePdfGenerator;
    
    /**
     * 生成基础 PDF
     */
    @GetMapping("/basic")
    public ResponseEntity<byte[]> generateBasicPdf() {
        try {
            byte[] pdfBytes = pdfGenerator.generateBasicPdf(
                "测试文档",
                "这是一个使用 SpringBoot 和 iText 生成的 PDF 文档示例。\n\n" +
                "PDF 导出是企业应用中常见的功能需求,如生成报表、合同、发票等。\n\n" +
                "本文档展示了基础的 PDF 生成功能。"
            );
            
            return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_PDF)
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=basic.pdf")
                .body(pdfBytes);
                
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
    
    /**
     * 生成带表格的 PDF
     */
    @GetMapping("/table")
    public ResponseEntity<byte[]> generatePdfWithTable() {
        try {
            // 模拟订单数据
            List<Order> orders = generateMockOrders();
            byte[] pdfBytes = advancedPdfGenerator.generatePdfWithTable(orders);
            
            return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_PDF)
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=orders.pdf")
                .body(pdfBytes);
                
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
    
    /**
     * 基于模板生成 PDF
     */
    @GetMapping("/template")
    public ResponseEntity<byte[]> generatePdfFromTemplate() {
        try {
            // 读取 HTML 模板
            String htmlTemplate = readTemplateFile("templates/employee.html");
            
            // 准备数据
            Map<String, Object> data = new HashMap<>();
            data.put("title", "员工信息表");
            data.put("subtitle", "人力资源部");
            data.put("name", "张三");
            data.put("department", "技术部");
            data.put("position", "高级工程师");
            data.put("hireDate", "2024-01-01");
            data.put("generateTime", new Date().toString());
            
            byte[] pdfBytes = templatePdfGenerator.generatePdfFromTemplate(htmlTemplate, data);
            
            return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_PDF)
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=employee.pdf")
                .body(pdfBytes);
                
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
    
    private List<Order> generateMockOrders() {
        List<Order> orders = new ArrayList<>();
        // 模拟数据...
        return orders;
    }
    
    private String readTemplateFile(String path) throws IOException {
        // 读取模板文件...
        return "";
    }
}

4.2 异步 PDF 生成

AsyncPdfService.java

@Service
public class AsyncPdfService {
    
    @Autowired
    private AdvancedPdfGenerator pdfGenerator;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 异步生成 PDF
     */
    @Async
    public CompletableFuture<String> generatePdfAsync(List<Order> orders, String taskId) {
        try {
            // 生成 PDF
            byte[] pdfBytes = pdfGenerator.generatePdfWithTable(orders);
            
            // 存储 PDF 到文件系统或对象存储
            String filePath = savePdfFile(pdfBytes, taskId);
            
            // 更新任务状态
            redisTemplate.opsForHash().put(taskId, "status", "COMPLETED");
            redisTemplate.opsForHash().put(taskId, "filePath", filePath);
            
            return CompletableFuture.completedFuture(filePath);
            
        } catch (Exception e) {
            // 更新任务状态为失败
            redisTemplate.opsForHash().put(taskId, "status", "FAILED");
            redisTemplate.opsForHash().put(taskId, "error", e.getMessage());
            
            return CompletableFuture.failedFuture(e);
        }
    }
    
    private String savePdfFile(byte[] pdfBytes, String taskId) throws IOException {
        // 保存文件到指定位置
        String directory = "./pdfs/" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
        File dir = new File(directory);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        
        String fileName = taskId + ".pdf";
        String filePath = directory + "/" + fileName;
        
        try (FileOutputStream fos = new FileOutputStream(filePath)) {
            fos.write(pdfBytes);
        }
        
        return filePath;
    }
}

五、性能优化与最佳实践

5.1 性能优化策略

1. 内存管理

  • 使用 ByteArrayOutputStream 避免频繁 I/O
  • 及时关闭资源,使用 try-with-resources
  • 对于大文件,考虑流式处理

2. 线程处理

  • 对于大文件生成,使用异步处理
  • 配置合理的线程池大小
  • 避免阻塞主线程

3. 缓存策略

  • 缓存字体和模板
  • 对于重复生成的内容,使用缓存
  • 考虑使用 Redis 缓存中间结果

4. 资源优化

  • 优化图片大小和质量
  • 减少 PDF 中的冗余内容
  • 使用适当的压缩级别

5.2 错误处理与监控

1. 异常处理

  • 捕获并记录 PDF 生成过程中的异常
  • 提供友好的错误信息给客户端
  • 实现重试机制

2. 监控指标

  • 记录 PDF 生成时间
  • 监控内存使用情况
  • 跟踪生成失败率

3. 日志记录

  • 记录关键操作日志
  • 实现请求追踪
  • 保存生成历史记录

5.3 安全考虑

1. 输入验证

  • 验证用户输入,防止注入攻击
  • 限制文件大小和生成频率
  • 防止恶意模板注入

2. 权限控制

  • 限制 PDF 生成权限
  • 实现访问控制
  • 加密敏感 PDF 文档

3. 数据保护

  • 处理敏感数据时注意保护
  • 实现数据脱敏
  • 遵循数据保护法规

六、生产环境部署

6.1 容器化部署

Dockerfile

FROM openjdk:11-jre-slim

WORKDIR /app

COPY target/pdf-export-demo-1.0.0.jar app.jar

# 安装中文字体
RUN apt-get update && apt-get install -y fonts-wqy-zenhei

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

docker-compose.yml

version: '3.8'

services:
  pdf-export:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - ./pdfs:/app/pdfs
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - TZ=Asia/Shanghai

6.2 配置优化

application-prod.yml

# 生产环境配置
spring:
  application:
    name: pdf-export-demo
  
  # 线程池配置
  task:
    execution:
      pool:
        core-size: 10
        max-size: 50
        queue-capacity: 100

# PDF 配置
pdf:
  # 字体路径
  font-path: /usr/share/fonts/truetype/wqy/wqy-zenhei.ttc
  
  # 生成配置
  generate:
    timeout: 30000  # 超时时间(毫秒)
    max-size: 10485760  # 最大文件大小(10MB)
    max-records: 10000  # 最大记录数
  
  # 存储配置
  storage:
    path: ./pdfs
    base-url: http://localhost:8080/pdf

6.3 监控与告警

Prometheus 配置

scrape_configs:
  - job_name: 'pdf-export'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['pdf-export:8080']

Grafana 仪表板

  • PDF 生成请求数
  • 平均生成时间
  • 失败率
  • 内存使用情况

七、常见问题与解决方案

7.1 中文字体问题

问题:PDF 中中文显示乱码或不显示

解决方案

  • 嵌入中文字体
  • 确保字体文件存在
  • 正确设置字体编码

代码示例

// 方法 1:使用 iText 内置字体
PdfFont font = PdfFontFactory.createFont("STSongStd-Light", "UniGB-UCS2-H", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);

// 方法 2:使用系统字体
PdfFont font = PdfFontFactory.createFont("path/to/simsun.ttc", PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);

7.2 大文件生成问题

问题:生成大文件时内存溢出或超时

解决方案

  • 使用异步处理
  • 分批处理数据
  • 优化 PDF 结构
  • 增加 JVM 内存

代码示例

// 分批处理数据
public byte[] generateLargePdf(List<Data> dataList) throws Exception {
    // 分批处理
    int batchSize = 1000;
    List<List<Data>> batches = new ArrayList<>();
    
    for (int i = 0; i < dataList.size(); i += batchSize) {
        int end = Math.min(i + batchSize, dataList.size());
        batches.add(dataList.subList(i, end));
    }
    
    // 生成 PDF
    // ...
}

7.3 性能问题

问题:PDF 生成速度慢

解决方案

  • 缓存字体和模板
  • 优化数据查询
  • 使用更高效的 PDF 库
  • 考虑使用预生成策略

代码示例

// 字体缓存
private static Map<String, PdfFont> fontCache = new ConcurrentHashMap<>();

public PdfFont getFont(String fontPath) throws Exception {
    return fontCache.computeIfAbsent(fontPath, path -> {
        try {
            return PdfFontFactory.createFont(path, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    });
}

八、案例分析

8.1 报表生成案例

需求:生成包含大量数据的销售报表

解决方案

  1. 使用 iText 7 生成复杂表格
  2. 实现异步生成
  3. 分页处理大数据
  4. 添加图表和汇总信息

关键代码

// 分页处理
public void addTableWithPagination(Document document, List<SalesData> data) {
    int pageSize = 50;
    int totalPages = (data.size() + pageSize - 1) / pageSize;
    
    for (int i = 0; i < totalPages; i++) {
        int start = i * pageSize;
        int end = Math.min(start + pageSize, data.size());
        List<SalesData> pageData = data.subList(start, end);
        
        // 添加表格
        addTable(document, pageData);
        
        // 分页
        if (i < totalPages - 1) {
            document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
        }
    }
}

8.2 合同生成案例

需求:基于模板生成个性化合同

解决方案

  1. 使用 HTML 模板
  2. 填充动态数据
  3. 添加电子签名
  4. 加密 PDF

关键代码

// 添加电子签名
public void addDigitalSignature(PdfDocument pdfDoc, String signatureFieldName, String signerName) {
    // 创建签名外观
    Rectangle rect = new Rectangle(36, 748, 200, 100);
    PdfSignatureFormField field = PdfFormField.createSignature(pdfDoc, rect, signatureFieldName);
    
    // 添加签名字段
    PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDoc, true);
    form.addField(field);
    
    // 签名外观
    PdfCanvas canvas = new PdfCanvas(pdfDoc.getPage(pdfDoc.getNumberOfPages()));
    canvas.rectangle(rect);
    canvas.stroke();
    
    // 添加签名信息
    Document doc = new Document(pdfDoc);
    doc.showTextAligned(new Paragraph("签名: " + signerName), 
        rect.getLeft() + 10, rect.getBottom() + 10, 
        pdfDoc.getNumberOfPages(), TextAlignment.LEFT, VerticalAlignment.BOTTOM, 0);
}

九、未来发展趋势

9.1 技术演进

1. 云原生 PDF 服务

  • 基于 Serverless 的 PDF 生成服务
  • 容器化部署和自动扩缩容
  • 与云存储集成

2. AI 辅助 PDF 生成

  • 智能内容布局
  • 自动数据填充
  • 个性化模板生成

3. 交互式 PDF

  • 可填写表单
  • 数字签名
  • 多媒体内容

9.2 工具链发展

1. 低代码 PDF 生成

  • 可视化模板设计
  • 拖拽式布局
  • 自动数据绑定

2. 集成开发工具

  • IDE 插件
  • 代码生成器
  • 测试工具

3. 标准规范

  • PDF/A 合规性
  • 无障碍支持
  • 数字签名标准

十、总结与展望

10.1 核心要点

  1. 技术选型:根据需求选择合适的 PDF 库
  2. 实现方案:基础生成、模板生成、异步处理
  3. 性能优化:内存管理、线程处理、缓存策略
  4. 生产部署:容器化、配置优化、监控告警
  5. 最佳实践:错误处理、安全考虑、代码质量

10.2 实施建议

  1. 评估需求:明确 PDF 复杂度和性能要求
  2. 技术选型:选择适合的 PDF 库
  3. 架构设计:考虑异步处理和扩展性
  4. 测试验证:充分测试各种场景
  5. 监控运维:建立完善的监控体系

10.3 未来展望

随着业务需求的不断增长,PDF 导出功能将变得更加重要和复杂。未来的发展方向包括:

  • 智能化:利用 AI 技术优化 PDF 生成过程
  • 云原生:基于云服务的 PDF 生成解决方案
  • 标准化:遵循行业标准和最佳实践
  • 生态化:与其他系统的深度集成

通过本文介绍的技术方案,您可以在 SpringBoot 应用中实现高效、可靠的 PDF 导出功能,满足各种业务场景的需求。

小结

本文介绍了 SpringBoot 实现 PDF 导出的完整解决方案,包括:

  • 技术选型:主流 PDF 库的比较和选择建议
  • 核心实现:基于 iText 7 的基础和高级 PDF 生成
  • 模板方案:使用 Flying Saucer 基于 HTML 模板生成 PDF
  • SpringBoot 集成:控制器实现和异步处理
  • 性能优化:内存管理、线程处理和缓存策略
  • 生产部署:容器化配置和监控告警
  • 常见问题:中文字体、大文件生成和性能优化
  • 案例分析:报表生成和合同生成的实际应用
  • 未来趋势:云原生、AI 辅助和交互式 PDF

通过这些技术方案,您可以在 SpringBoot 应用中实现高质量的 PDF 导出功能,为用户提供专业、美观的文档体验。

互动话题

  1. 您在项目中使用过哪些 PDF 生成库?有什么使用心得?
  2. 您遇到过哪些 PDF 生成的性能问题?是如何解决的?
  3. 您对本文介绍的技术方案有什么改进建议?
  4. 您认为未来 PDF 生成技术会向哪些方向发展?

欢迎在评论区分享您的经验和看法!


标题:SpringBoot 实现 PDF 导出解决方案
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/04/1772516664298.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消