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 = 60; // 1分钟 /** * 私有构造函数,防止外部实例化 */ 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 = buildDockerCommand(docx2pdfPath, profile, fileName, containerName); 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); } } /** * 构建Docker命令,确保包含容器名称 * @param docx2pdfPath docker命令模板路径 * @param profile 配置文件路径 * @param fileName 文件名 * @param containerName 容器名称 * @return 完整的Docker命令 */ private String buildDockerCommand(String docx2pdfPath, String profile, String fileName, String containerName) { // 如果命令模板中已经包含 --name 参数,则直接使用 if (docx2pdfPath.contains("--name")) { return MessageFormat.format(docx2pdfPath, profile, fileName); } // 如果命令模板中没有 --name 参数,则添加容器名称 // 假设命令模板格式为: docker run [其他参数] image_name // 我们需要在 docker run 后添加 --name 参数 String baseCommand = MessageFormat.format(docx2pdfPath, profile, fileName); // 检查是否包含 docker run if (baseCommand.contains("docker run")) { // 在 docker run 后插入 --name 参数 String[] parts = baseCommand.split("docker run", 2); if (parts.length == 2) { return "docker run --name " + containerName + parts[1]; } } // 如果无法解析,则直接返回原命令 log.warn("无法解析Docker命令模板,使用原始命令: {}", baseCommand); return baseCommand; } /** * 获取文件锁 */ 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 + "* --filter status=exited --format '{{.Names}}'"; Process findProc = Runtime.getRuntime().exec(findCommand); BufferedReader reader = new BufferedReader(new InputStreamReader(findProc.getInputStream())); String containerName; boolean hasCleaned = false; while ((containerName = reader.readLine()) != null) { if (!containerName.trim().isEmpty()) { log.info("清理已停止的旧容器: {}", containerName); // 删除已停止的容器 String rmCommand = "docker rm " + containerName; Runtime.getRuntime().exec(rmCommand); hasCleaned = true; } } reader.close(); findProc.waitFor(5, TimeUnit.SECONDS); if (!hasCleaned) { log.debug("没有发现需要清理的旧容器"); } } 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("强制终止进程"); } } // 强制清理所有相关容器 forceCleanupContainers(); } catch (Exception e) { log.error("强制终止进程失败", e); } } /** * 强制清理所有相关容器 */ private void forceCleanupContainers() { try { log.info("开始强制清理相关容器..."); // 清理带有特定前缀的容器 cleanupContainersByPrefix(); // 清理可能残留的headless-wps容器(基于镜像名称) cleanupContainersByImage("headless-wps-example"); log.info("容器强制清理完成"); } catch (Exception e) { log.error("强制清理容器失败", e); } } /** * 根据前缀清理容器 */ private void cleanupContainersByPrefix() { cleanupContainersByFilter("name=" + CONTAINER_NAME_PREFIX + "*", "前缀"); } /** * 根据镜像名称清理容器 */ private void cleanupContainersByImage(String imageName) { cleanupContainersByFilter("ancestor=" + imageName, "镜像 " + imageName); } /** * 通用的容器清理方法 * @param filter 过滤条件,如 name=prefix*" 或 "ancestor=imageName" * @param filterDesc 过滤条件描述,用于日志输出 */ private void cleanupContainersByFilter(String filter, String filterDesc) { try { log.info("清理{}容器", filterDesc); // 查找所有运行中的相关容器 String runningCommand = "docker ps --filter " + filter + " --format '{{.Names}}'"; Process runningProc = Runtime.getRuntime().exec(runningCommand); BufferedReader runningReader = new BufferedReader(new InputStreamReader(runningProc.getInputStream())); String containerName; while ((containerName = runningReader.readLine()) != null) { if (!containerName.trim().isEmpty()) { log.warn("强制停止运行中的容器: {}", containerName); // 强制停止容器 String killCommand = "docker kill " + containerName; Process killProc = Runtime.getRuntime().exec(killCommand); killProc.waitFor(10, TimeUnit.SECONDS); // 强制删除容器 String rmCommand = "docker rm -f " + containerName; log.info("执行命令: {}", rmCommand); Process rmProc = Runtime.getRuntime().exec(rmCommand); rmProc.waitFor(10, TimeUnit.SECONDS); log.info("已强制清理容器: {}", containerName); } } runningReader.close(); runningProc.waitFor(10, TimeUnit.SECONDS); // 查找所有停止的相关容器并删除 String stoppedCommand = "docker ps -a --filter " + filter + " --format '{{.Names}}'"; Process stoppedProc = Runtime.getRuntime().exec(stoppedCommand); BufferedReader stoppedReader = new BufferedReader(new InputStreamReader(stoppedProc.getInputStream())); while ((containerName = stoppedReader.readLine()) != null) { if (!containerName.trim().isEmpty()) { log.warn("强制删除停止的容器: {}", containerName); // 强制删除容器 String rmCommand = "docker rm -f " + containerName; log.info("执行命令: {}", rmCommand); Process rmProc = Runtime.getRuntime().exec(rmCommand); rmProc.waitFor(10, TimeUnit.SECONDS); log.info("已强制删除容器: {}", containerName); } } stoppedReader.close(); stoppedProc.waitFor(10, TimeUnit.SECONDS); } catch (Exception e) { log.error("根据{}清理容器失败", filterDesc, 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); } }