New file |
| | |
| | | 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); |
| | | } |
| | | } |