java使用poi-tl模版+vform自定义表单生成word,使用LibreOffice导出为pdf

java使用poi-tl模版+vform自定义表单生成word,使用LibreOffice导出为pdf。

接上一篇,在Windows或者服务器上安装LibreOffice。java调用LibreOffice服务将生成的word文件转为pdf。

java 复制代码
@Override
    public 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");
    }
java 复制代码
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()) {
            // 自动生成:唯一标识 + 时间戳 + .pdf
            String 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();
    }
相关推荐
面向星辰3 小时前
扣子开始节点和结束节点
java·服务器·前端
烤麻辣烫3 小时前
黑马程序员苍穹外卖(新手)Day1
java·数据库·spring boot·学习·mybatis
失散134 小时前
分布式专题——51 ES 深度分页问题及其解决方案详解
java·分布式·elasticsearch·架构
FreeBuf_4 小时前
思科CCX软件曝高危RCE:攻击者可利用Java RMI和CCX Editor获取root权限
java·网络·安全
_esther_4 小时前
【字符串String类大集合】构造创建_常量池情况_获取方法_截取方法_转换方法_String和基本数据类型互转方法
java
lkbhua莱克瓦244 小时前
Java基础——集合进阶5
java·开发语言·集合·泛型
WZTTMoon4 小时前
Spring 配置解析与 @Value 注入核心流程详解
java·spring boot·spring
程序定小飞5 小时前
基于springboot的健身房管理系统开发与设计
java·spring boot·后端
wxin_VXbishe5 小时前
springboot在线课堂教学辅助系统-计算机毕业设计源码07741
java·c++·spring boot·python·spring·django·php