java使用poi-tl模版+vform自定义表单生成word,使用LibreOffice导出为pdf
java使用poi-tl模版+vform自定义表单生成word,使用LibreOffice导出为pdf。
接上一篇,在Windows或者服务器上安装LibreOffice。java调用LibreOffice服务将生成的word文件转为pdf。
@Overridepublic void exportMeetingRecord(Long id, HttpServletResponse response) {TbProjectStartHold tbProjectStartHold = selectTbProjectStartHoldById(id, null);Map<String, Object> data=new HashMap<>();data.put("projectName",Optional.ofNullable(tbProjectStartHold.getProjectName()).orElse(""));data.put("acceptanceNumber",Optional.ofNullable(tbProjectStartHold.getAcceptanceNumber()).orElse(""));data.put("meetingPlace",Optional.ofNullable(tbProjectStartHold.getMeetingPlace()).orElse(""));data.put("recorderName",Optional.ofNullable(tbProjectStartHold.getRecorderName()).orElse(""));data.put("hostName",Optional.ofNullable(tbProjectStartHold.getHostName()).orElse(""));data.put("meetingDate","");if(tbProjectStartHold.getMeetingDate()!=null){data.put("meetingDate",DateUtils.parseDateToStr("yyyy-MM-dd",tbProjectStartHold.getMeetingDate()));}data.put("meetingRecord","");HtmlRenderPolicy htmlRenderPolicy = new HtmlRenderPolicy();Configure configure = Configure.builder().bind("meetingRecord", htmlRenderPolicy).build();//会议记录是富文本 需要处理if(tbProjectStartHold.getMeetingRecord()!=null){data.put("meetingRecord", tbProjectStartHold.getMeetingRecord());}//模板选择String url = "classpath:templates/start_jilu.docx";ProjectInitiation initiation = projectInitiationService.findById(tbProjectStartHold.getProjectInitiationId(), null);String fileName=initiation.getAcceptanceNumber().replace(" ", "")+ "_会议记录_" + DateUtils.dateTimeNow();//响应返回 pdf 文件WordToPdf.generatePdfRespose(configure,response,url,data,resourceLoader,libreoffice,fileName + ".pdf");}
public class WordToPdf {/*** 生成并保存pdf文件 返回url* @param url* @param data* @param resourceLoader* @param libreoffice* @param pdfFileName* @param targetDirPath* @return*/public static String generatePdfToPath(String url, Map<String, Object> data,ResourceLoader resourceLoader, String libreoffice,String pdfFileName, String targetDirPath) {// ========== 1. 初始化目标目录和文件名 ==========// 校验并创建目标目录(不存在则自动创建)File targetDir = new File(targetDirPath);if (!targetDir.exists()) {boolean mkdirsSuccess = targetDir.mkdirs();if (!mkdirsSuccess) {throw new RuntimeException("创建 PDF 目标目录失败:" + targetDirPath);}}// 生成最终 PDF 文件名(若传入为空则自动生成唯一名称)String finalPdfName;if (pdfFileName == null || pdfFileName.trim().isEmpty()) {// 自动生成:唯一标识 + 时间戳 + .pdfString uniqueName = UUID.randomUUID().toString().replace("-", "");String timestamp = String.valueOf(System.currentTimeMillis());finalPdfName = "PDF_" + uniqueName + "_" + timestamp + ".pdf";} else {// 传入文件名:补充 .pdf 后缀(若未带)finalPdfName = pdfFileName.endsWith(".pdf") ? pdfFileName : pdfFileName + ".pdf";}// 最终 PDF 完整路径File finalPdfFile = new File(targetDir, finalPdfName);// ========== 2. 生成临时 Word 文件(转换中间文件) ==========File tempDir = new File(System.getProperty("java.io.tmpdir"));String uniqueName = UUID.randomUUID().toString().replace("-", "");File tempWordFile = new File(tempDir, uniqueName + ".docx");try {// 2.1 用 poi-tl 生成 Word 到临时文件Configure config = Configure.builder().build();Resource resource = resourceLoader.getResource(url);try (InputStream inputStream = resource.getInputStream();OutputStream wordOut = new FileOutputStream(tempWordFile)) {XWPFTemplate template = XWPFTemplate.compile(inputStream, config).render(data);template.write(wordOut);PoitlIOUtils.closeQuietly(template); // 关闭模板资源}// ========== 3. 调用 LibreOffice 转换 Word → PDF(核心步骤) ==========// 转换后的 PDF 先存到临时目录,转换成功后再移动到目标目录(避免转换失败污染目标目录)File tempPdfFile = new File(tempDir, uniqueName + ".pdf");boolean convertSuccess = convertWordToPdfByLibreOffice(libreoffice, tempWordFile, tempPdfFile);if (!convertSuccess || !tempPdfFile.exists() || tempPdfFile.length() == 0) {throw new RuntimeException("Word 转 PDF 失败,生成的 PDF 文件为空");}// ========== 4. 将临时 PDF 移动到目标目录 ==========FileUtils.moveFile(tempPdfFile, finalPdfFile);System.out.println("PDF 生成成功,保存路径:" + finalPdfFile.getAbsolutePath());// 返回 PDF 完整路径(或仅返回文件名,按需调整)return finalPdfFile.getAbsolutePath();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("导出 PDF 失败:" + e.getMessage(), e);} finally {// ========== 5. 清理临时文件(关键:避免磁盘残留) ==========new Thread(() -> {try {TimeUnit.SECONDS.sleep(3); // 等待3秒,确保文件操作完成FileUtils.deleteQuietly(tempWordFile); // 删除临时 Word// 若转换失败,临时 PDF 可能未移动,也需删除File tempPdfFile = new File(tempDir, uniqueName + ".pdf");FileUtils.deleteQuietly(tempPdfFile);} catch (InterruptedException ignored) {}}).start();}}/*** 在响应流中返回生成的文件* @param response* @param url* @param data* @param resourceLoader* @param libreoffice* @param pdfFileName*/public static void generatePdfRespose(HttpServletResponse response, String url, Map<String, Object> data,ResourceLoader resourceLoader, String libreoffice, String pdfFileName) {Configure config = Configure.builder().build();generatePdfRespose(config, response, url, data, resourceLoader, libreoffice, pdfFileName);}/*** 生成pdf 在压缩包内* @param zipOut* @param url* @param data* @param resourceLoader* @param libreoffice* @param pdfFileName*/public static void generatePdfForZip(ZipArchiveOutputStream zipOut, String url, Map<String, Object> data,ResourceLoader resourceLoader, String libreoffice, String pdfFileName) {Configure config = Configure.builder().build();generatePdfForZip(config, zipOut, url, data, resourceLoader, libreoffice, pdfFileName);}/*** 在响应流中返回生成的文件* @param response* @param url* @param data* @param resourceLoader* @param libreoffice* @param pdfFileName*/public static void generatePdfRespose( Configure config,HttpServletResponse response, String url, Map<String, Object> data,ResourceLoader resourceLoader, String libreoffice, String pdfFileName) {// ========== 2. 生成临时 Word 文件(关键修改:不再直接写入响应) ==========// 临时文件目录(跨平台兼容:Windows是C:\Users\XXX\AppData\Local\Temp,Linux是/tmp)System.out.println("****************");;System.out.println(System.getProperty("java.io.tmpdir"));File tempDir = new File(System.getProperty("java.io.tmpdir"));// 生成唯一文件名(避免并发冲突)String uniqueName = UUID.randomUUID().toString().replace("-", "");File tempWordFile = new File(tempDir, uniqueName + ".docx");File tempPdfFile = new File(tempDir, uniqueName + ".pdf");try {// 2.1 用 poi-tl 生成 Word 到临时文件Resource resource = resourceLoader.getResource(url);try (InputStream inputStream = resource.getInputStream();OutputStream wordOut = new FileOutputStream(tempWordFile)) {XWPFTemplate template = XWPFTemplate.compile(inputStream, config).render(data);template.write(wordOut);PoitlIOUtils.closeQuietly(template); // 关闭模板资源}// ========== 3. 调用 LibreOffice 转换 Word → PDF(核心步骤) ==========boolean convertSuccess = convertWordToPdfByLibreOffice(libreoffice,tempWordFile, tempPdfFile);if (!convertSuccess || !tempPdfFile.exists() || tempPdfFile.length() == 0) {throw new RuntimeException("Word 转 PDF 失败,生成的 PDF 文件为空");}// ========== 4. 将 PDF 写入响应流(供前端下载) ==========
// String pdfFileName = initiation.getAcceptanceNumber().replace(" ", "") + "_启动会预约确认单_" + DateUtils.dateTimeNow() + ".pdf";// 设置响应头(PDF格式)response.setContentType("application/pdf");response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(pdfFileName, StandardCharsets.UTF_8));response.setContentLengthLong(tempPdfFile.length()); // 可选:设置文件大小,优化下载体验// 读取临时 PDF 并写入响应流try (InputStream pdfIn = new FileInputStream(tempPdfFile);BufferedInputStream bis = new BufferedInputStream(pdfIn);OutputStream out = response.getOutputStream();BufferedOutputStream bos = new BufferedOutputStream(out)) {byte[] buffer = new byte[1024 * 8];int len;while ((len = bis.read(buffer)) != -1) {bos.write(buffer, 0, len);}bos.flush();out.flush();}} catch (Exception e) {e.printStackTrace();throw new RuntimeException("导出 PDF 失败:" + e.getMessage(), e);} finally {// ========== 5. 清理临时文件(关键:避免磁盘残留) ==========// 异步删除(防止响应未完成时文件被占用)new Thread(() -> {try {TimeUnit.SECONDS.sleep(3); // 等待3秒,确保响应已完成FileUtils.deleteQuietly(tempWordFile); // 静默删除,失败不抛异常FileUtils.deleteQuietly(tempPdfFile);} catch (InterruptedException ignored) {}}).start();}}/*** 生成pdf 在压缩包内* @param zipOut* @param url* @param data* @param resourceLoader* @param libreoffice* @param pdfFileName*/public static void generatePdfForZip(Configure config,ZipArchiveOutputStream zipOut, String url, Map<String, Object> data,ResourceLoader resourceLoader, String libreoffice, String pdfFileName) {// ========== 2. 生成临时 Word 文件(关键修改:不再直接写入响应) ==========// 临时文件目录(跨平台兼容:Windows是C:\Users\XXX\AppData\Local\Temp,Linux是/tmp)System.out.println("****************");;System.out.println(System.getProperty("java.io.tmpdir"));File tempDir = new File(System.getProperty("java.io.tmpdir"));// 生成唯一文件名(避免并发冲突)String uniqueName = UUID.randomUUID().toString().replace("-", "");File tempWordFile = new File(tempDir, uniqueName + ".docx");File tempPdfFile = new File(tempDir, uniqueName + ".pdf");// 标记是否生成成功(用于最终清理临时文件)boolean generateSuccess = false;try {// 2.1 用 poi-tl 生成 Word 到临时文件Resource resource = resourceLoader.getResource(url);try (InputStream inputStream = resource.getInputStream();OutputStream wordOut = new FileOutputStream(tempWordFile)) {XWPFTemplate template = XWPFTemplate.compile(inputStream, config).render(data);template.write(wordOut);PoitlIOUtils.closeQuietly(template); // 关闭模板资源}// ========== 3. 调用 LibreOffice 转换 Word → PDF(核心步骤) ==========boolean convertSuccess = convertWordToPdfByLibreOffice(libreoffice,tempWordFile, tempPdfFile);if (!convertSuccess || !tempPdfFile.exists() || tempPdfFile.length() == 0) {throw new RuntimeException("Word 转 PDF 失败,生成的 PDF 文件为空");}// 3. 创建ZIP条目(指定PDF在压缩包中的名称/路径)// 支持子目录格式,例如:"2024年报表/销售数据.pdf"ZipArchiveEntry pdfEntry = new ZipArchiveEntry(pdfFileName);// 设置文件大小(优化ZIP压缩效率)pdfEntry.setSize(tempPdfFile.length());zipOut.putArchiveEntry(pdfEntry);// 4. 将PDF文件写入ZIP条目try (InputStream pdfIn = new FileInputStream(tempPdfFile);BufferedInputStream bis = new BufferedInputStream(pdfIn)) {byte[] buffer = new byte[1024 * 8];int len;while ((len = bis.read(buffer)) != -1) {zipOut.write(buffer, 0, len);}}// 5. 关闭当前ZIP条目(必须调用,否则后续条目无法添加)zipOut.closeArchiveEntry();// 刷新ZIP流,确保数据写入(外部最终需调用zipOut.close())zipOut.flush();generateSuccess = true;System.out.println("PDF文件[" + pdfFileName + "]已成功添加到ZIP包");} catch (Exception e) {e.printStackTrace();throw new RuntimeException("导出 PDF 失败:" + e.getMessage(), e);} finally {// ========== 5. 清理临时文件(关键:避免磁盘残留) ==========// 6. 清理临时文件(异步延迟删除,确保ZIP写入完成)boolean finalGenerateSuccess = generateSuccess;new Thread(() -> {try {// 延迟3秒删除(如果生成失败,缩短延迟)TimeUnit.SECONDS.sleep(finalGenerateSuccess ? 3 : 1);// 静默删除,失败不抛异常(避免影响主流程)FileUtils.deleteQuietly(tempWordFile);FileUtils.deleteQuietly(tempPdfFile);System.out.println("临时文件清理完成:" + tempWordFile.getName() + "、" + tempPdfFile.getName());} catch (InterruptedException ignored) {Thread.currentThread().interrupt();}}).start();}}/*** 核心工具方法:调用 LibreOffice 命令行转换 Word 到 PDF* @param wordFile 源 Word 文件* @param pdfFile 目标 PDF 文件* @return 转换是否成功*/public static boolean convertWordToPdfByLibreOffice(String libreoffice, File wordFile, File pdfFile) {// 命令行参数说明:// --headless:无界面模式(服务器必须用,避免弹出窗口)// --convert-to pdf:指定转换格式为 PDF// --outdir:指定输出目录// 最后跟源文件路径(必须是绝对路径)List<String> command = new ArrayList<>();command.add(libreoffice); // LibreOffice 执行文件路径command.add("--headless");command.add("--convert-to");command.add("pdf");command.add("--outdir");command.add(pdfFile.getParent()); // PDF 输出目录(临时目录)command.add(wordFile.getAbsolutePath()); // 源 Word 绝对路径// 处理中文路径/文件名:设置系统编码(关键!避免乱码)ProcessBuilder pb = new ProcessBuilder(command);Map<String, String> env = pb.environment();env.put("LC_ALL", "zh_CN.UTF-8"); // Linux/Mac 中文编码
// env.put("PATH", env.get("PATH") + ":" + libreoffice.substring(0, libreoffice.lastIndexOf("/"))); // Linux 补充环境变量File libreOfficeFile = new File(libreoffice);String libreOfficeDir = libreOfficeFile.getParent(); // 自动解析父目录(无需手动处理 /)// 拼接 PATH(原有 PATH + 新目录)String newPath = env.get("PATH") + ":" + libreOfficeDir;env.put("PATH", newPath);Process process = null;try {process = pb.start(); // 执行命令// 必须读取进程的输入流和错误流(否则进程会阻塞,导致转换失败)String errorMsg = readStream(process.getErrorStream());String inputMsg = readStream(process.getInputStream());System.out.println("LibreOffice 转换日志(输入流):" + inputMsg);System.out.println("LibreOffice 转换日志(错误流):" + errorMsg);// 等待进程执行完成(超时1分钟,防止无限阻塞)boolean finished = process.waitFor(60, TimeUnit.SECONDS);int exitCode = process.exitValue();return finished && exitCode == 0; // 退出码 0 表示成功} catch (Exception e) {e.printStackTrace();System.err.println("LibreOffice 转换失败:" + e.getMessage());return false;} finally {if (process != null) {process.destroy(); // 销毁进程,释放资源}}}/*** 工具方法:读取流内容(避免进程阻塞)*/private static String readStream(InputStream stream) {StringBuilder sb = new StringBuilder();try (BufferedReader br = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {String line;while ((line = br.readLine()) != null) {sb.append(line).append("\n");}} catch (IOException e) {e.printStackTrace();}return sb.toString();}
