Java下修改PDF模板生成PDF的最优方案及DOCX转PDF问题咨询
问题背景与技术咨询
需要向前端返回20+页的PDF文件,其中大部分内容为静态,仅2页的部分文本需根据日期、姓名、编号等参数动态修改。因此倾向于基于模板修改而非从头生成PDF,目前考虑两种方案,存在以下疑问:
- 直接修改PDF模板:了解到只能用白色矩形覆盖原有文本再新增内容,会导致用户选中文本异常、文本宽度无法自适应,这个认知是否正确?
- 使用DOCX模板:通过Apache POI修改内容后转PDF,但使用fr.opensagres.xdocreport库转换时出现水印丢失、列表编号异常、文本重叠等问题,求更精准的转换库,或是否存在更优的混合方案?
以下是当前实现的Java服务方法代码及build.gradle配置,寻求相关实践经验与建议。
当前Java服务实现代码
@Override public byte[] getContract() { byte[] pdfContent; String docxPath = "doc/contract.docx"; InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(docxPath); try (XWPFDocument doc = new XWPFDocument(Objects.requireNonNull(in))) { for (XWPFParagraph par : doc.getParagraphs()) { List<XWPFRun> runs = par.getRuns(); if (!CollectionUtils.isEmpty(runs)) { for (XWPFRun run : runs) { String text = run.getText(0); if (!StringUtils.isEmpty(text)) { if (text.contains(PH_DAY)) { text = text.replace(PH_DAY, "23"); } if (text.contains(PH_MONTH)) { text = text.replace(PH_MONTH, "Fevral"); } if (text.contains(PH_YEAR)) { text = text.replace(PH_YEAR, "2023"); } if (text.contains(PH_ORDINAL_SUFFIX)) { text = text.replace(PH_ORDINAL_SUFFIX, "cü"); } if (text.contains(PH_PIN)) { text = text.replace(PH_PIN, "№ AF233031"); } if (text.contains(PH_NAME)) { text = text.replace(PH_NAME, "Surname Name"); } run.setText(text, 0); } } } } try (ByteArrayOutputStream docxOut = new ByteArrayOutputStream()) { doc.write(docxOut); try (ByteArrayOutputStream pdfOut = new ByteArrayOutputStream()) { PdfOptions pdfOptions = PdfOptions.create(); PdfConverter.getInstance().convert(doc, pdfOut, pdfOptions); pdfContent = pdfOut.toByteArray(); } } } catch (Exception e) { String message = e.getMessage(); log.error(message); throw new GeneralTechnicalException(GeneralTechErrorEnum.SERVER_ERROR, message, e); } return pdfContent; }
build.gradle配置
buildscript { repositories { mavenCentral() } } plugins { id 'org.springframework.boot' version "$springBootVersion" id 'io.spring.dependency-management' version "$springDependencyVersion" id 'java' id 'org.sonarqube' version "$sonarqubeVersion" id 'jacoco' id 'checkstyle' id 'pmd' } group = 'my.project' version = '$projectVersion' sourceCompatibility = '$sourceCompatibility' repositories { mavenCentral() maven { url "${nexusUrl}/repository/maven-public/" allowInsecureProtocol = true } } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-cache' implementation "org.springframework.cloud:spring-cloud-starter-openfeign:$openFeignVersion" implementation "com.oracle.ojdbc:ojdbc8:$oracleVersion" implementation "junit:junit:$junitVersion" implementation "org.redisson:redisson:$redissonVersion" implementation 'org.apache.commons:commons-lang3' implementation "org.springdoc:springdoc-openapi-ui:$springDocVersion" implementation "net.logstash.logback:logstash-logback-encoder:$logbackEncoderVersion" implementation 'ch.qos.logback:logback-classic' implementation "org.springframework.cloud:spring-cloud-dependencies:$releaseTrainVersion" implementation "com.ibam:ibam-error-handling:$ibamErrorHandlerVersion" implementation group: 'org.codehaus.jettison', name: 'jettison', version: "$jettisonVersion" implementation group: 'redis.clients', name: 'jedis', version: "${jedis}" implementation group: 'com.esotericsoftware', name: 'kryo', version: "${kryo}" implementation 'org.liquibase:liquibase-core' // implementation "com.github.librepdf:openpdf:$openpdfVersion" implementation "org.apache.poi:poi-ooxml:5.2.3" implementation "org.apache.logging.log4j:log4j-to-slf4j:2.20.0" implementation "org.apache.logging.log4j:log4j-api:2.20.0" implementation "fr.opensagres.xdocreport:fr.opensagres.poi.xwpf.converter.pdf:2.0.4" // developmentOnly 'org.springframework.boot:spring-boot-devtools' compileOnly "org.projectlombok:lombok:$lombokVersion" annotationProcessor "org.projectlombok:lombok:$lombokVersion" annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' implementation "com.auth0:java-jwt:$jwtAuthVersion" testImplementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.4' testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } } tasks.named('test') { useJUnitPlatform() } pmd { toolVersion = '6.10.0' ruleSets = [] ignoreFailures = false ruleSetFiles = files("${rootProject.projectDir}/pmd/rules.xml") } checkstyle { toolVersion = "$checkstyleToolVersion" configFile = file("${rootProject.projectDir}/checkstyle/checkstyle.xml") ignoreFailures = false } tasks.withType(Checkstyle) { reports { xml.enabled true html.enabled true } } jacoco { toolVersion = "0.8.8" reportsDir = file("build/reports/jacoco") } jacocoTestReport { reports { xml.enabled true csv.enabled false } } sonarqube { properties { property "sonar.java.coveragePlugin", "jacoco" property 'sonar.coverage.exclusions', sonarExclude property "sonar.jacoco.reportPath", "build/jacoco/test.exec" property "sonar.junit.reportsPath", "build/test-results/test" property "sonar.scm.forceReloadAll", "true" property "sonar.java.checkstyle.reportPaths", "build/reports/checkstyle/main.xml" } } bootJar { archiveName 'app.jar' }
实践经验与建议
关于直接修改PDF模板的认知验证
你的部分认知是对的,但并非只能用覆盖的方式:
- 老旧的PDF修改方式确实常用白色矩形覆盖原有文本再叠加新内容,这会导致选中文本时出现原文本和新文本重叠的问题,且新文本无法自适应原有文本框宽度。
- 更优的做法是使用支持PDF表单域的模板:提前在PDF模板中需要动态修改的位置添加文本表单域,后端通过PDF操作库(如iText、Apache PDFBox)直接填充表单域值。这种方式不会破坏原有文本结构,选中文本正常,且表单域可设置自动换行/自适应宽度,完全规避覆盖法的问题。
DOCX转PDF的替代方案
fr.opensagres.xdocreport对复杂DOCX格式的支持确实有限,推荐以下更可靠的转换库:
- Aspose.Words for Java:对DOCX格式的兼容性极强,能完美保留水印、列表、格式排版,转换精度很高。但它是商业库,需要授权。
- LibreOffice/OpenOffice Headless模式:通过调用外部Office程序的无头模式进行转换,支持所有Office格式,转换效果和手动保存PDF一致。需要在服务器部署LibreOffice,后端通过ProcessBuilder调用命令行执行转换。
- Apache POI + Flying Saucer:如果DOCX结构不复杂,可先将DOCX转换为HTML,再用Flying Saucer转PDF。但需要处理HTML样式还原的问题,适合格式简单的场景。
最优混合方案
结合你的场景(大部分静态,仅2页动态),推荐PDF模板+表单域填充的方案:
- 提前将整个合同导出为PDF模板,在需要动态修改的位置添加文本表单域(如日期、姓名、编号等)。
- 后端使用iText或PDFBox加载PDF模板,直接填充表单域的值,生成最终PDF。
这种方案无需处理DOCX转PDF的格式丢失问题,性能更高,且输出的PDF完全符合预期,不会出现选中文本异常的情况。
内容的提问来源于stack exchange,提问作者Oleg Sandro
相关产品推荐
相关产品推荐

