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