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();
}