package com.ots.project.tool; import lombok.extern.slf4j.Slf4j; import java.io.*; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.text.MessageFormat; import java.util.concurrent.TimeUnit; /** * Docker转化pdf专用处理工具 * 使用单例模式实现 */ @Slf4j public class PdfDockerUtil { /** * 单例实例 */ private static volatile PdfDockerUtil instance; /** * 容器名称前缀 */ private static final String CONTAINER_NAME_PREFIX = "pdf_converter_"; /** * 文件锁路径 */ private static final String LOCK_FILE_PATH = System.getProperty("java.io.tmpdir") + "/pdf_docker_lock"; /** * 任务超时时间(秒) */ private static final int TASK_TIMEOUT_SECONDS = 300; // 5分钟 /** * 私有构造函数,防止外部实例化 */ private PdfDockerUtil() { // 私有构造函数 } /** * 获取单例实例 * 使用双重检查锁定确保线程安全 * @return PdfDockerUtil实例 */ public static PdfDockerUtil getInstance() { if (instance == null) { synchronized (PdfDockerUtil.class) { if (instance == null) { instance = new PdfDockerUtil(); } } } return instance; } /** * docker word转pdf * @param profile 配置文件路径 * @param docx2pdfPath docker命令模板路径 * @param fileName 文件名 */ public void dockerConvertPDF(String profile, String docx2pdfPath, String fileName) { FileLock lock = null; Process proc = null; try { // 1. 获取文件锁,确保串行执行 lock = acquireFileLock(); if (lock == null) { log.error("无法获取文件锁,可能有其他转换任务正在执行"); return; } // 2. 生成唯一容器名称 String containerName = CONTAINER_NAME_PREFIX + System.currentTimeMillis(); // 3. 清理旧容器 cleanupOldContainers(); // 4. 构建Docker命令 String command = MessageFormat.format(docx2pdfPath, profile, fileName); log.info("docker执行命令:{}", command); // 5. 执行Docker命令(带超时) proc = Runtime.getRuntime().exec(command); // 6. 异步读取输出流 ProcessOutputReader outputReader = new ProcessOutputReader(proc.getInputStream(), "STDOUT"); ProcessOutputReader errorReader = new ProcessOutputReader(proc.getErrorStream(), "STDERR"); outputReader.start(); errorReader.start(); // 7. 等待进程完成(带超时) boolean completed = proc.waitFor(TASK_TIMEOUT_SECONDS, TimeUnit.SECONDS); if (!completed) { log.error("Docker任务执行超时({}秒),强制终止进程", TASK_TIMEOUT_SECONDS); forceKillProcess(proc); return; } int exitCode = proc.exitValue(); log.info("Docker进程退出码: {}", exitCode); // 8. 等待输出读取完成 outputReader.join(5000); errorReader.join(5000); if (exitCode != 0) { log.error("Docker命令执行失败,退出码: {}", exitCode); } else { log.info("Docker命令执行成功"); } } catch (Exception e) { log.error("Docker转换PDF失败", e); } finally { // 9. 清理资源 cleanupResources(proc, lock); } } /** * 获取文件锁 */ private FileLock acquireFileLock() { FileChannel channel = null; try { Path lockPath = Paths.get(LOCK_FILE_PATH); Files.createDirectories(lockPath.getParent()); if (!Files.exists(lockPath)) { Files.createFile(lockPath); } channel = new RandomAccessFile(lockPath.toFile(), "rw").getChannel(); FileLock lock = channel.tryLock(); if (lock != null) { log.info("成功获取文件锁"); return lock; } else { log.warn("文件锁被占用,无法获取"); if (channel != null) { channel.close(); } return null; } } catch (Exception e) { log.error("获取文件锁失败", e); if (channel != null) { try { channel.close(); } catch (IOException ex) { log.error("关闭文件通道失败", ex); } } return null; } } /** * 清理旧容器 */ private void cleanupOldContainers() { try { // 查找并停止所有相关容器 String findCommand = "docker ps -a --filter name=" + CONTAINER_NAME_PREFIX + "* --format '{{.Names}}'"; Process findProc = Runtime.getRuntime().exec(findCommand); BufferedReader reader = new BufferedReader(new InputStreamReader(findProc.getInputStream())); String containerName; while ((containerName = reader.readLine()) != null) { if (!containerName.trim().isEmpty()) { log.info("清理旧容器: {}", containerName); // 停止容器 String stopCommand = "docker stop " + containerName; Runtime.getRuntime().exec(stopCommand); // 删除容器 String rmCommand = "docker rm " + containerName; Runtime.getRuntime().exec(rmCommand); } } reader.close(); findProc.waitFor(10, TimeUnit.SECONDS); } catch (Exception e) { log.warn("清理旧容器时出现异常", e); } } /** * 强制终止进程 */ private void forceKillProcess(Process proc) { try { if (proc != null) { proc.destroy(); // 等待进程终止 if (!proc.waitFor(5, TimeUnit.SECONDS)) { proc.destroyForcibly(); log.warn("强制终止进程"); } } } catch (Exception e) { log.error("强制终止进程失败", e); } } /** * 清理资源 */ private void cleanupResources(Process proc, FileLock lock) { try { if (proc != null) { forceKillProcess(proc); } if (lock != null) { lock.release(); log.info("释放文件锁"); } } catch (Exception e) { log.error("清理资源失败", e); } } /** * 进程输出读取器 */ private static class ProcessOutputReader extends Thread { private final BufferedReader reader; private final String streamType; public ProcessOutputReader(InputStream inputStream, String streamType) { this.reader = new BufferedReader(new InputStreamReader(inputStream, java.nio.charset.StandardCharsets.UTF_8)); this.streamType = streamType; } @Override public void run() { try { String line; log.info("=== Docker {} ===", streamType); while ((line = reader.readLine()) != null) { if ("STDERR".equals(streamType)) { log.error("{}: {}", streamType, line); } else { log.info("{}: {}", streamType, line); } } } catch (IOException e) { log.error("读取{}流失败", streamType, e); } finally { try { reader.close(); } catch (IOException e) { log.error("关闭{}流失败", streamType, e); } } } } /** * 静态方法调用,方便使用 * @param profile 配置文件路径 * @param docx2pdfPath docker命令模板路径 * @param fileName 文件名 * 方式1:通过静态方法调用(推荐) * PdfDockerUtil.convertPDF(profile, docx2pdfPath, fileName); * 方式2:通过实例方法调用 * PdfDockerUtil pdfDockerUtil = PdfDockerUtil.getInstance(); * pdfDockerUtil.convertPDF(profile, docx2pdfPath, fileName); */ public static void convertPDF(String profile, String docx2pdfPath, String fileName) { getInstance().dockerConvertPDF(profile, docx2pdfPath, fileName); } }