1. Java操作PDF的常见场景与工具选型在Java生态中处理PDF文档是开发者的高频需求。根据我过去五年在企业级项目中的实践经验最常见的应用场景包括动态生成包含业务数据的报表如订单、发票将系统日志或分析结果归档为可打印格式为Web应用添加PDF导出功能批量处理现有PDF文档如添加水印、合并拆分目前主流的Java PDF处理库有以下几种Apache PDFBox推荐首选开源免费Apache 2.0许可证支持创建、读取、编辑PDF全流程最新版本3.0.22023年发布显著提升了处理大文件时的内存效率iText商业项目慎用功能强大但商业授权严格AGPL开源版有法律风险适合有预算购买商业许可证的企业OpenPDFiText分支LGPL协议更友好兼容iText API维护活跃度不如PDFBox重要提示如果项目需要商用且无法接受AGPL协议建议优先考虑PDFBox或购买iText商业授权。我曾亲历过团队因误用iText开源版导致的法律纠纷。2. 基于PDFBox的PDF写入实战2.1 环境准备与基础配置以Maven项目为例需添加以下依赖dependency groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version3.0.2/version /dependency内存管理是PDF处理的关键点。建议在启动时配置// 设置PDFBox使用的内存阈值单位MB System.setProperty(pdfbox.rendering.memory.threshold, 50);2.2 创建新PDF文档基础写入示例try (PDDocument document new PDDocument()) { PDPage page new PDPage(PDRectangle.A4); document.addPage(page); try (PDPageContentStream contentStream new PDPageContentStream(document, page)) { contentStream.beginText(); contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12); contentStream.newLineAtOffset(100, 700); contentStream.showText(Hello PDFBox!); contentStream.endText(); } document.save(output.pdf); }2.3 高级文本处理技巧实际项目中常遇到的复杂需求中文支持// 需要加载外部字体 PDType0Font font PDType0Font.load(document, new File(SimHei.ttf)); contentStream.setFont(font, 12); contentStream.showText(中文内容);自动换行算法String longText 这是一段很长的文本...; float width 200; // 行宽 float startX 100; float startY 700; float fontSize 12; ListString lines new ArrayList(); int lastSpace -1; int startIdx 0; for (int i 0; i longText.length(); i) { if (Character.isWhitespace(longText.charAt(i))) { lastSpace i; } String subStr longText.substring(startIdx, i 1); float stringWidth font.getStringWidth(subStr) / 1000 * fontSize; if (stringWidth width) { if (lastSpace 0) { lines.add(longText.substring(startIdx, lastSpace)); startIdx lastSpace 1; lastSpace -1; } else { lines.add(longText.substring(startIdx, i)); startIdx i; } } } if (startIdx longText.length()) { lines.add(longText.substring(startIdx)); } // 绘制多行文本 for (int i 0; i lines.size(); i) { contentStream.beginText(); contentStream.newLineAtOffset(startX, startY - (i * (fontSize 2))); contentStream.showText(lines.get(i)); contentStream.endText(); }3. 复杂元素写入实战3.1 表格生成方案PDF本身没有表格概念需要通过绘制实现// 定义表格参数 float margin 50; float yStart 700; float rowHeight 20f; float tableWidth page.getMediaBox().getWidth() - 2 * margin; int cols 3; float colWidth tableWidth / (float) cols; // 绘制表头 contentStream.setLineWidth(1f); contentStream.setStrokingColor(Color.BLACK); contentStream.addRect(margin, yStart, tableWidth, rowHeight); contentStream.stroke(); // 填充表头文本 String[] headers {ID, Name, Value}; for (int i 0; i headers.length; i) { float x margin (i * colWidth) 5; float y yStart 5; drawText(contentStream, headers[i], x, y, PDType1Font.HELVETICA_BOLD, 12); } // 绘制数据行 ListString[] data getSampleData(); // 获取示例数据 for (int row 0; row data.size(); row) { float y yStart - ((row 1) * rowHeight); contentStream.addRect(margin, y, tableWidth, rowHeight); contentStream.stroke(); String[] rowData data.get(row); for (int col 0; col rowData.length; col) { float x margin (col * colWidth) 5; drawText(contentStream, rowData[col], x, y 5, PDType1Font.HELVETICA, 10); } }3.2 图片插入与优化处理图片时的关键点// 加载图片 BufferedImage image ImageIO.read(new File(logo.png)); PDImageXObject pdImage LosslessFactory.createFromImage(document, image); // 计算保持宽高比的缩放比例 float scale 0.5f; // 默认缩放 float maxWidth 200f; if (pdImage.getWidth() maxWidth) { scale maxWidth / pdImage.getWidth(); } float scaledHeight pdImage.getHeight() * scale; // 绘制图片 contentStream.drawImage(pdImage, margin, yStart - scaledHeight - 10, pdImage.getWidth() * scale, scaledHeight);性能提示处理大图时建议先使用ImageIO进行预处理避免PDFBox内存溢出。我曾处理过一张10MB的JPG导致OOM的案例最终解决方案是先用Thumbnails库压缩到合理尺寸。4. 生产环境中的最佳实践4.1 内存管理与性能优化PDF处理是内存密集型操作关键策略文档分块处理// 每100页保存一个临时文件 int pageThreshold 100; ListFile tempFiles new ArrayList(); PDDocument currentDoc new PDDocument(); for (int i 0; i totalPages; i) { // 添加页面到currentDoc if (i 0 i % pageThreshold 0) { File tempFile File.createTempFile(pdf-part-, .tmp); currentDoc.save(tempFile); tempFiles.add(tempFile); currentDoc.close(); currentDoc new PDDocument(); } } // 最终合并所有临时文件 PDDocument finalDoc new PDDocument(); for (File f : tempFiles) { PDDocument part PDDocument.load(f); for (PDPage page : part.getPages()) { finalDoc.addPage(page); } part.close(); f.delete(); // 清理临时文件 } finalDoc.save(final.pdf);使用内存映射文件Java NIOFile file new File(large.pdf); try (RandomAccessFile raf new RandomAccessFile(file, r); FileChannel channel raf.getChannel(); ByteBuffer buf channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); PDDocument doc PDDocument.load(buf)) { // 处理文档 }4.2 常见问题排查中文乱码问题确保使用支持中文的字体如思源黑体检查字体文件加载路径是否正确验证系统是否安装了所需字体内存溢出(OOM)处理// JVM参数建议 -XX:UseG1GC -Xms512m -Xmx2048m -XX:MaxRAMPercentage70.0跨平台渲染差异在Linux服务器上测试字体渲染使用Docker容器保持环境一致对关键文档生成MD5校验值4.3 安全注意事项防止PDF注入攻击// 消毒用户输入 String sanitized input.replaceAll([^\\x20-\\x7e], );敏感信息处理// 自动检测并移除信用卡号等敏感信息 Pattern creditCardPattern Pattern.compile(\\b(?:\\d[ -]*?){13,16}\\b); Matcher matcher creditCardPattern.matcher(text); if (matcher.find()) { throw new SecurityException(文档包含敏感信息); }在最近的一个银行项目中我们实现了PDF生成的异步队列处理。当用户请求生成对账单时系统会将任务放入RabbitMQ队列由后台工作进程处理。关键优化点包括使用Redis缓存已生成的PDF实现断点续生成功能添加水印和数字签名限制单个用户并发生成任务数这种架构每天能稳定处理超过50万份PDF文档平均生成时间从原来的3秒降低到800毫秒。其中最大的性能提升来自于将字体预加载到内存缓存避免了重复读取字体文件的IO开销。