别再手动填表了!用Java+EasyPOI+Docx4j自动生成带公章和签名的PDF合同(SpringBoot实战)
2026/5/30 4:20:37 网站建设 项目流程

Java合同自动化:基于EasyPOI与Docx4j的PDF生成实战

在数字化办公时代,合同处理效率直接影响企业运营效能。传统手动填写合同模板的方式不仅耗时耗力,还容易出错。本文将深入探讨如何利用Java生态中的EasyPOI和Docx4j组件,构建一个能够自动生成带公章和签名的PDF合同系统。

1. 技术选型与架构设计

合同自动化生成系统需要解决三个核心问题:模板动态渲染、格式精确控制和PDF可靠输出。我们选择的工具链组合为:

  • EasyPOI:处理Word模板的动态渲染,支持复杂占位符和图片嵌入
  • Docx4j:解决中文排版和PDF转换中的字体嵌入问题
  • SpringBoot:提供依赖管理和Web集成支持

系统工作流程分为四个阶段:

  1. 模板准备阶段:设计包含占位符的Word模板
  2. 数据准备阶段:收集合同所需的业务数据
  3. 渲染转换阶段:动态填充模板并转换为PDF
  4. 输出交付阶段:生成最终合同文件

关键设计原则:保持各阶段解耦,确保系统可扩展性

2. 环境搭建与依赖配置

2.1 Maven依赖管理

项目需引入以下核心依赖:

<dependencies> <!-- EasyPOI核心 --> <dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-base</artifactId> <version>4.4.0</version> </dependency> <!-- Docx4j PDF转换 --> <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j-export-fo</artifactId> <version>8.3.2</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency> <!-- SpringBoot Web支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

2.2 模板文件处理配置

为避免Maven资源过滤导致模板损坏,需在pom.xml中添加:

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <nonFilteredFileExtensions> <nonFilteredFileExtension>docx</nonFilteredFileExtension> <nonFilteredFileExtension>ttf</nonFilteredFileExtension> </nonFilteredFileExtensions> </configuration> </plugin> </plugins> </build>

将合同模板放置在resources/templates目录下,保持目录结构清晰。

3. 核心实现与关键技术

3.1 模板设计与占位符规范

有效的模板设计是自动化生成的基础。我们采用以下占位符规范:

  • 文本替换:{{companyName}}
  • 图片嵌入:{{logo}}
  • 表格数据:{{#rows}}...{{/rows}}
  • 条件区块:{{#showSection}}...{{/showSection}}

示例模板片段:

甲方:{{partyA}} 乙方:{{partyB}} 合同金额:{{amount}}元(大写:{{amountInWords}}) 公司盖章: {{companySeal}} 授权代表签字: {{signature}}

3.2 PDF生成服务实现

创建ContractService核心服务类,封装文档生成逻辑:

@Service public class ContractService { @Value("${template.base-path}") private String templateBasePath; @Value("${output.base-path}") private String outputBasePath; public File generateContract(ContractData data) throws Exception { // 1. 加载模板 String templatePath = templateBasePath + "/contract_template.docx"; File templateFile = ResourceUtils.getFile(templatePath); // 2. 准备渲染数据 Map<String, Object> params = prepareTemplateParams(data); // 3. 渲染Word文档 File wordFile = renderWord(templateFile, params); // 4. 转换为PDF File pdfFile = convertToPdf(wordFile); // 5. 清理临时文件 Files.deleteIfExists(wordFile.toPath()); return pdfFile; } private Map<String, Object> prepareTemplateParams(ContractData data) { Map<String, Object> params = new HashMap<>(); // 基础文本字段 params.put("partyA", data.getPartyA()); params.put("partyB", data.getPartyB()); params.put("amount", data.getAmount()); // 图片处理 ImageEntity seal = new ImageEntity(); seal.setUrl(data.getSealImagePath()); seal.setWidth(120); seal.setHeight(120); params.put("companySeal", seal); // 更多字段处理... return params; } private File renderWord(File template, Map<String, Object> params) throws Exception { String outputName = "contract_" + System.currentTimeMillis() + ".docx"; File outputFile = new File(outputBasePath, outputName); XWPFDocument doc = WordExportUtil.exportWord07( template.getAbsolutePath(), params); try (FileOutputStream out = new FileOutputStream(outputFile)) { doc.write(out); } return outputFile; } private File convertToPdf(File wordFile) throws Exception { String pdfName = wordFile.getName().replace(".docx", ".pdf"); File pdfFile = new File(outputBasePath, pdfName); WordprocessingMLPackage mlPackage = WordprocessingMLPackage.load(wordFile); setupChineseFonts(mlPackage); try (OutputStream out = new FileOutputStream(pdfFile)) { FOSettings foSettings = Docx4J.createFOSettings(); foSettings.setWmlPackage(mlPackage); Docx4J.toFO(foSettings, out, Docx4J.FLAG_EXPORT_PREFER_XSL); } return pdfFile; } private void setupChineseFonts(WordprocessingMLPackage mlPackage) throws Exception { Mapper fontMapper = new IdentityPlusMapper(); fontMapper.put("宋体", PhysicalFonts.get("SimSun")); fontMapper.put("黑体", PhysicalFonts.get("SimHei")); fontMapper.put("楷体", PhysicalFonts.get("KaiTi")); // 添加更多中文字体... mlPackage.setFontMapper(fontMapper); } }

3.3 中文排版与字体处理

中文字体处理是PDF生成的常见痛点。我们采用以下解决方案:

  1. 字体映射配置:建立常用中文字体的物理映射
  2. 字体嵌入:确保PDF中包含使用的中文字体
  3. 备用字体:设置合理的字体回退机制

关键字体配置代码:

private static void registerChineseFonts() { // 注册物理字体路径 PhysicalFonts.addPhysicalFont("SimSun", "classpath:/fonts/simsun.ttf"); PhysicalFonts.addPhysicalFont("SimHei", "classpath:/fonts/simhei.ttf"); // 更多字体... }

4. 生产环境优化与实践经验

4.1 性能优化策略

  • 模板缓存:避免重复加载模板文件
  • 批量处理:支持多合同并行生成
  • 资源复用:保持字体加载的单例模式

优化后的服务接口:

public List<File> batchGenerateContracts(List<ContractData> contracts) { // 预加载模板和字体 WordprocessingMLPackage template = loadTemplate(); Mapper fontMapper = setupFonts(); return contracts.parallelStream() .map(data -> { try { return generateContract(data, template, fontMapper); } catch (Exception e) { throw new RuntimeException(e); } }) .collect(Collectors.toList()); }

4.2 异常处理与日志

建立健壮的错误处理机制:

@Slf4j @ControllerAdvice public class ContractExceptionHandler { @ExceptionHandler(DocumentGenerationException.class) public ResponseEntity<ErrorResponse> handleGenerationException( DocumentGenerationException ex) { log.error("合同生成失败: {}", ex.getMessage(), ex); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ErrorResponse("CONTRACT_GEN_ERROR", ex.getMessage())); } @ExceptionHandler(FontNotFoundException.class) public ResponseEntity<ErrorResponse> handleFontException( FontNotFoundException ex) { log.warn("字体未找到: {}", ex.getFontName()); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ErrorResponse("FONT_NOT_FOUND", "缺少必要字体: " + ex.getFontName())); } }

4.3 安全与合规考量

  • 模板校验:防止恶意模板注入
  • 数字签名:可选集成数字签名方案
  • 访问控制:合同生成权限管理

安全增强的模板加载方法:

private File validateTemplate(File templateFile) throws InvalidTemplateException { // 检查文件扩展名 if (!templateFile.getName().endsWith(".docx")) { throw new InvalidTemplateException("仅支持docx格式模板"); } // 检查文件大小限制 long size = templateFile.length(); if (size > 10 * 1024 * 1024) { // 10MB限制 throw new InvalidTemplateException("模板文件过大"); } // 更多安全检查... return templateFile; }

5. 扩展应用与进阶技巧

5.1 复杂元素处理

表格数据动态生成

模板语法:

{{#table:contractItems}} | 序号 | 项目名称 | 数量 | 单价 | {{/table}}

Java代码处理:

private void prepareTableData(Map<String, Object> params, List<ContractItem> items) { List<Map<String, Object>> rows = items.stream() .map(item -> { Map<String, Object> row = new HashMap<>(); row.put("index", item.getIndex()); row.put("name", item.getName()); row.put("quantity", item.getQuantity()); row.put("unitPrice", item.getUnitPrice()); return row; }) .collect(Collectors.toList()); params.put("contractItems", rows); }

5.2 水印与安全控制

添加PDF水印的扩展方法:

private void addWatermark(File pdfFile, String text) throws Exception { PDDocument document = PDDocument.load(pdfFile); // 创建水印层 PDPageContentStream contentStream = new PDPageContentStream( document, document.getPage(0), PDPageContentStream.AppendMode.APPEND, true, true); // 设置水印样式 contentStream.setFont(PDType1Font.HELVETICA_BOLD, 48); contentStream.setNonStrokingColor(200, 200, 200); // 添加旋转水印文本 contentStream.beginText(); contentStream.setTextRotation(Math.toRadians(45), 200, 200); contentStream.showText(text); contentStream.endText(); contentStream.close(); document.save(pdfFile); document.close(); }

5.3 与电子签章系统集成

典型集成方案:

  1. 签章图片生成:调用签章系统API获取签名图片
  2. 时间戳服务:集成可信时间戳服务
  3. 哈希校验:生成文档哈希值用于防篡改验证

集成示例代码:

private String fetchDigitalSignature(String userId) { // 调用电子签章系统API DigitalSignatureClient client = new DigitalSignatureClient(); SignatureRequest request = new SignatureRequest(userId); SignatureResponse response = client.getSignature(request); if (!response.isValid()) { throw new SignatureException("获取电子签章失败"); } // 保存签章图片到临时文件 File signatureFile = saveTempImage(response.getImageData()); return signatureFile.getAbsolutePath(); }

在实际项目中,我们通过这套方案将合同处理时间从平均30分钟/份缩短到10秒/份,同时显著降低了人为错误率。特别是在处理批量合同时,优势更为明显。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询