一、介绍
使用 Java Apache POI 将文字渲染到 Word 模板是一种常见的文档自动化技术,广泛应用于批量生成或定制 Word 文档的场景。使用aspose可以将word转成pdf从而达到定制化pdf的目的。
参考文档:java实现Word转Pdf(Windows、Linux通用) - 故城归人 - 博客园
二、word模板准备
(1)pdf逆转
本次实践没有word模板,需从生成的pdf先还原成word。所以使用python将pdf逆转成word docx:
!pip install pdf2docx
from pdf2docx import Converter
pdf_file = '专业技术资格评审申报表_预览.pdf'
docx_file = '申报表模板.docx'
cv = Converter(pdf_file)
#cv.convert(docx_file, start=0, end=2) # 只转前三页
cv.convert(docx_file)# 转全部
cv.close()
创建word模板,在其中插入占位符,例如 {{name}}、{{workorg}}
(2)模版读取方式
绝对路径读取:缺点路径变化无法读出
String baseDir = System.getProperty("user.dir");//获取项目根目录
String templatePath = baseDir + "/peer-upms/peer-upms-biz/src/main/resources/file/title_template.docx";
File templateFile = new File(templatePath);
classpath读取:这样就不再依赖项目根目录的绝对路径,打包之后只要 title_template.docx 在 src/main/resources/file/ 下,就能自动被拷贝并使用。之后你再用 templateFile 打开 POI 进行替换就能正常运行了。
// 1. 从 classpath 加载模板资源(src/main/resources/file/title_template.docx)
ClassPathResource tplRes = new ClassPathResource("file/title_template.docx");
if (!tplRes.exists()) {
throw new FileNotFoundException("classpath:/file/title_template.docx 不存在");
}
// 2. 将模板复制到一个临时文件(因为 POI 需要 File)
File templateFile = File.createTempFile("title_cover_", ".docx");
try (InputStream in = tplRes.getInputStream();
FileOutputStream out = new FileOutputStream(templateFile)) {
in.transferTo(out);
}
三、Java相关代码
word部分(渲染数据写死)
(1)引入依赖:
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.1.0</version>
<scope>compile</scope>
</dependency>
(2)word下载接口:
@Operation(summary = "申请人DOCX下载",
responses = {
@ApiResponse(
responseCode = "200",
content = @Content(
mediaType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
schema = @Schema(type = "string", format = "binary")
)
)
}
)
@RequestMapping(value = "/download/docx", method = RequestMethod.GET)
public ResponseEntity<Resource> downloadDocx() {
Map<String, Object> data = new HashMap<>();
data.put("name", "XX1");
data.put("department", "XX111测试");
data.put("zhuanye", "作物育种");
data.put("jishuzige", "研究员");
data.put("nian", "2025");
data.put("yue", "03");
data.put("ri", "20");
data.put("datetime", "2025/03/20 14:11:44");
data.put("zhuanyejishu", "生命科学研究");
try {
File docxFile = wordToPdfService.generateDocx(data);
File file = new File(docxFile.getName());
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
if (!resource.exists() || !resource.isReadable()) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"));
String fileName = "专业技术资格申报表_" + data.get("name") + "_预览.docx";
// headers.setContentDisposition(ContentDisposition.builder("attachment").filename(fileName).build());
//解决中文乱码问题
ContentDisposition contentDisposition = ContentDisposition.builder("attachment")
.filename(fileName, StandardCharsets.UTF_8)
.build();
headers.setContentDisposition(contentDisposition);
headers.setContentLength(file.length());
return new ResponseEntity<>(resource, headers, HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@Service
public class WordToPdfService {
/**
*
* 根据word模板生成word文档
*
*/
public File generateDocx(Map<String, Object> data) throws Exception {
String baseDir = System.getProperty("user.dir");
String templatePath = baseDir + "/peer-upms/peer-upms-biz/src/main/resources/file/title_template.docx";
File templateFile = new File(templatePath);
if (!templateFile.exists()) {
throw new Exception("Template file does not exist at: " + templatePath);
}
try (FileInputStream fis = new FileInputStream(templateFile)) {
if (fis.available() <= 0) {
throw new Exception("Template file is empty.");
}
}
XWPFDocument document = WordExportUtil.exportWord07(templatePath, data);
try (FileOutputStream fos = new FileOutputStream("output.docx")) {
document.write(fos);
}
File docxFile = new File("output.docx");
return docxFile;
}
......
pdf部分(渲染数据从mysql中拉取)
(1) 引入jar包:
mvn install:install-file -Dfile=peer-upms/peer-upms-biz/libs/aspose-words-15.8.0-jdk16.jar -DgroupId=com.aspose -DartifactId=aspose-words -Dversion=15.8.0-jdk16 -Dpackaging=jar
(2)下载pdf接口(先转word再生成对应的pdf)
@Inner(value = false)
@RestController
@RequiredArgsConstructor
public class TitlePdfDownloadController {
@Autowired
private ApplyDataPdfPreviewService applyDataPdfPreviewService;
@GetMapping(value = "/downloadPdf")
@Inner(value = false)
public ResponseEntity<Resource> downloadWord() {
try {
Map<String, Object> data = new HashMap<>();
data = applyDataPdfPreviewService.getDataByApplyId(applyId);
// 1. Generate DOCX file
File docxFile = wordToPdfService.generateDocx(data);
// 2. Convert DOCX to PDF
File pdfFile = wordToPdfService.convertDocxToPdf(docxFile);
// 3. Read PDF file bytes
byte[] pdfBytes = Files.readAllBytes(pdfFile.toPath());
// 4. Set file name and response headers with PDF MIME type
String fileName = "专业技术资格申报表_" + data.get("name") + "_" + "预览.pdf";
Resource resource = new ByteArrayResource(pdfBytes);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("attachment", fileName);
headers.setContentLength(pdfBytes.length);
return new ResponseEntity<>(resource, headers, HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
(3)将动态从mysql获取数据写成一个service:
@Service
public class ApplyDataPdfPreviewService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private FindCitiidMapper findCitiidMapper;
/**
* 根据 applyId 拉取申报表、学历和奖惩信息,
* 并组装到一个 Map 中返回。
*/
public Map<String,Object> getDataByApplyId(long applyId) {
Map<String,Object> data = new HashMap<>();
// 0)拉取 title_apply_applypers 中的mobile字段
String citiid = findCitiidMapper.findCitiidByApplyId(applyId);
String sqlMobile = "select mobile from title_apply_applypers where citiid=?";
Map<String,Object> mobile = jdbcTemplate.queryForMap(sqlMobile, citiid);
data.put("mobile", mobile.get("mobile"));
// 1) 拉取 title_applytable 中的所有字段
String sqlMain = """
SELECT
exctypecd,
techqualtype_cd,
citiid,
name,
workorg,
gend_cd,
birthday,
job_dt,
presdivtechjob,
discactisitu,
presdivtechpost,
presdivtechpostbgn_tm,
apply_id,
apply_serie_nm,
applydiv_nm,
prestechqual_nm,
fillin_tm,
perm_nm,
permget_tm,
permcertnum,
divtype,
prestechqualget_tm,
prestechqualapprorg_nm,
presdivtechjobbyear,
applytechqualdetail_cd,
innovationteam
FROM title_applytable
WHERE apply_id = ?
""";
Map<String,Object> main = jdbcTemplate.queryForMap(sqlMain, applyId);
// 基本字段放入 map
String exctypecd = main.get("exctypecd").toString();
switch (exctypecd) {
case "1":
data.put("c1", "√");
data.put("c2", " ");
data.put("c3", " ");
data.put("c4", " ");
break;
case "2":
data.put("c1", " ");
data.put("c2", "√");
data.put("c3", " ");
data.put("c4", " ");
break;
case "5":
data.put("c1", " ");
data.put("c2", " ");
data.put("c3", "√");
data.put("c4", " ");
break;
case "6":
data.put("c1", " ");
data.put("c2", " ");
data.put("c3", " ");
data.put("c4", "√");
break;
default:
data.put("c1", " ");
data.put("c2", " ");
data.put("c3", " ");
data.put("c4", " ");
break;
}
String techqualtype_cd = main.get("techqualtype_cd").toString();
String techType ="";
switch (techqualtype_cd) {
case "1":
techType = "应用基础研究与技术开发人员";
break;
case "2":
techType = "试验发展与转化应用人员";
break;
case "3":
techType = "农业政策与科技管理研究人员";
break;
default:
techType = "";
break;
}
data.put("techType", techType);
......
// 2) 拉取学历信息列表
String sqlEdu = """
SELECT
school_nm,
edu_nm,
edutype_cd,
edu_cd,
digree_cd,
gradsitu_cd,
edubegin_tm,
grad_tm
FROM title_edurec
WHERE apply_id = ?
ORDER BY edubegin_tm
""";
List<Map<String,Object>> edus = jdbcTemplate.queryForList(sqlEdu, applyId);
for (int i = 0; i < 4; i++) {
Map<String,Object> e = i < edus.size() ? edus.get(i) : new HashMap<>();
int idx = i + 1;
data.put("school_nm" + idx, e.get("school_nm") != null ? e.get("school_nm") : " ");
data.put("edu_nm" + idx, e.get("edu_nm") != null ? e.get("edu_nm") : " ");
data.put("edutype_cd" + idx, e.get("edutype_cd") != null ? e.get("edutype_cd") : " ");
data.put("edu_cd" + idx, e.get("edu_cd") != null ? e.get("edu_cd") : " ");
data.put("digree_cd" + idx, e.get("digree_cd") != null ? e.get("digree_cd") : " ");
data.put("gradsitu_cd" + idx, e.get("gradsitu_cd") != null ? e.get("gradsitu_cd") : " ");
data.put("edubegin_tm" + idx, e.get("edubegin_tm") != null ? e.get("edubegin_tm") : " ");
data.put("grad_tm" + idx, e.get("grad_tm") != null ? e.get("grad_tm") : " ");
}
// 3) 拉取奖惩信息列表
String sqlBonus = """
SELECT
bonus_nm,
bonus_tm,
bonusorg
FROM title_bonus
WHERE apply_id = ?
ORDER BY bonus_tm DESC
""";
List<Map<String,Object>> bonuses = jdbcTemplate.queryForList(sqlBonus, applyId);
for (int i = 0; i < 3; i++) {
Map<String,Object> b = i < bonuses.size() ? bonuses.get(i) : new HashMap<>();
int idx = i + 1;
data.put("bonus_nm" + idx, b.size() != 0 ? b.get("bonus_nm") : " ");
data.put("bonus_tm" + idx, b.size() != 0 ? b.get("bonus_tm") : " ");
data.put("bonusorg" + idx, b.size() != 0 ? b.get("bonusorg") : " ");
}
data.put("nian", java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy")));
data.put("yue", java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("MM")));
data.put("ri", java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("dd")));
data.put("print_time", java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// 4) 继续教育情况
String sqlContEdu = """
SELECT begin_tm, end_tm, content, chargorg, studyaddr
FROM title_applycontedusitu
WHERE apply_id = ?
ORDER BY begin_tm
""";
List<Map<String,Object>> contEdus = jdbcTemplate.queryForList(sqlContEdu, applyId);
for (int i = 0; i < 4; i++) {
Map<String,Object> ce = i < contEdus.size() ? contEdus.get(i) : new HashMap<>();
int idx = i + 1;
data.put("cont_b" + idx, ce.size() != 0 ? ce.get("begin_tm") : " ");
data.put("cont_e" + idx, ce.size() != 0 ? ce.get("end_tm") : " ");
data.put("content" + idx, ce.size() != 0 ? ce.get("content") : " ");
data.put("contedu_char" + idx, ce.size() != 0 ? ce.get("chargorg") : " ");
data.put("caddr" + idx, ce.size() != 0 ? ce.get("studyaddr") : " ");
}
// 5) 专业考试成绩情况
String sqlExam = """
SELECT exam_tm, org, project, score, qualifycode
FROM title_professionalexaminfo
WHERE apply_id = ?
ORDER BY exam_tm
""";
List<Map<String,Object>> exams = jdbcTemplate.queryForList(sqlExam, applyId);
for (int i = 0; i < 4; i++) {
Map<String,Object> ex = i < exams.size() ? exams.get(i) : new HashMap<>();
int idx = i + 1;
data.put("exam_tm" + idx, ex.size() != 0 ? ex.get("exam_tm") : " ");
data.put("exam_org" + idx, ex.size() != 0 ? ex.get("org") : " ");
data.put("exam_project" + idx, ex.size() != 0 ? ex.get("project") : " ");
data.put("escore" + idx, ex.size() != 0 ? ex.get("score") : " ");
data.put("exam_qualifycode" + idx, ex.size() != 0 ? ex.get("qualifycode") : " ");
}
// 6) 工作经历
String sqlWork = """
SELECT begin_tm, end_tm, workposi, workcont, post
FROM title_workresu
WHERE apply_id = ?
ORDER BY begin_tm
""";
List<Map<String,Object>> works = jdbcTemplate.queryForList(sqlWork, applyId);
for (int i = 0; i < 4; i++) {
Map<String, Object> w = i < works.size() ? works.get(i) : new HashMap<>();
int idx = i + 1;
data.put("workresu_begin" + idx, w.size() != 0 ? w.get("begin_tm") : " ");
data.put("workresu_end" + idx, w.size() != 0 ? w.get("end_tm") : " ");
data.put("workresu_workposi" + idx, w.size() != 0 ? w.get("workposi") : " ");
data.put("workresu_workcont" + idx, w.size() != 0 ? w.get("workcont") : " ");
data.put("workresu_post" + idx, w.size() != 0 ? w.get("post") : " ");
}
// 7) 著作、论文及报告登记
String sqlArticle = """
SELECT artititle, publorg, charorg, volumes, publish_time, persduty
FROM title_applyarticle
WHERE apply_id = ?
ORDER BY publish_time
""";
List<Map<String,Object>> articles = jdbcTemplate.queryForList(sqlArticle, applyId);
for (int i = 0; i < 4; i++) {
Map<String, Object> w = i < articles.size() ? articles.get(i) : new HashMap<>();
int idx = i + 1;
data.put("article_artititle" + idx, !w.isEmpty() ? w.get("artititle") : " ");
data.put("article_publorg" + idx, !w.isEmpty() ? w.get("publorg") : " ");
data.put("article_charorg" + idx, !w.isEmpty() ? w.get("charorg") : " ");
data.put("article_volumes" + idx, !w.isEmpty() ? w.get("volumes") : " ");
data.put("article_publish_time" + idx, !w.isEmpty() ? w.get("publish_time") : " ");
data.put("article_persduty" + idx, !w.isEmpty() ? w.get("persduty") : " ");
}
// 8) 任现职以来考核情况
String sqlAssess = """
SELECT year, divtechpost, asserslt, asseorg, remark
FROM title_applyassesitu
WHERE apply_id = ?
ORDER BY year
""";
List<Map<String,Object>> assesses = jdbcTemplate.queryForList(sqlAssess, applyId);
for (int i = 0; i < 4; i++) {
Map<String, Object> as = i < assesses.size() ? assesses.get(i) : new HashMap<>();
int idx = i + 1;
data.put("assess_year" + idx, !as.isEmpty() ? as.get("year") : " ");
data.put("assess_divtechpost" + idx, !as.isEmpty() ? as.get("divtechpost") : " ");
data.put("assess_asserslt" + idx, !as.isEmpty() ? as.get("asserslt") : " ");
data.put("assess_asseorg" + idx, !as.isEmpty() ? as.get("asseorg") : " ");
data.put("assess_remark" + idx, !as.isEmpty() ? as.get("remark") : " ");
}
// 9) 工作总结(单值)
String sqlSumm = """
SELECT presjobsumm
FROM title_applytable
WHERE apply_id = ?
""";
String summary = jdbcTemplate.queryForObject(sqlSumm, String.class, applyId);
data.put("presjobsumm", summary);
return data;
}
(4)使用aspose将word转pdf
/**
* 商业版pdf许可 license,从 classpath 下读取 license.xml
*/
public static boolean getLicense() {
boolean result = false;
InputStream is = null;
try {
Resource resource = new ClassPathResource("license.xml");
is = resource.getInputStream();
License aposeLic = new License();
aposeLic.setLicense(is);
result = true;
} catch (Exception e) {
e.printStackTrace();
}finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
/**
* 将传入的 DOCX 文件转换成带完整样式的 PDF 并返回该 File。
*
* @param docxFile Word 模板(已填充占位符)的 File
* @return 生成的 PDF 临时文件
* @throws Exception 转换失败时抛出
*/
public File convertDocxToPdf(File docxFile, long applyId) throws Exception {
getLicense(); // 验证License 若不验证则转化出的pdf文档会有水印产生
// 1. 构造输出 PDF 的临时文件
String uuid = IdUtil.simpleUUID();
String fileName = uuid+".pdf";
Path targetPath = Paths.get(fileLocalPath, "PreviewPdf", fileName);
File pdfFile = new File(targetPath.toString());
pdfFile.getParentFile().mkdirs();
TitleApplyApplydataappe applyDataAppe = new TitleApplyApplydataappe();
applyDataAppe.setApplyId(applyId);
applyDataAppe.setAppefile(fileName);
applyDataAppe.setFilesize(pdfFile.length());
applyDataAppe.setAppefiledispnm("专业技术资格申报表预览_"+applyId+".pdf");
applyDataAppe.setOpTm(LocalDateTime.now());
applyDataAppe.setDir("PreviewPdf");
titleApplyApplydataappeService.save(applyDataAppe);
// 使用商业破解版 Aspose.Words 完成转换
try (FileOutputStream os = new FileOutputStream(pdfFile)) {
// Document 可以直接接收 File 或 String 路径
Document doc = new Document(docxFile.getAbsolutePath());
doc.save(os, SaveFormat.PDF);
}
return pdfFile;
}
四、部署
单体后端jar包部署
docker run -d --name peer-boot -v $PWD/peer-boot.jar:/app/peer-boot.jar -v /usr/share/fonts:/usr/share/fonts --net host eclipse-temurin:17-jdk java -Dfile.encoding=utf-8 -jar /app/peer-boot.jar