[测评系统]--测评系统核心代码库
fix:修复docker转化pdf文件卡顿和IO暴涨问题。
用单例控制docker转化,并监控docker状态
1个文件已添加
3个文件已修改
317 ■■■■■ 已修改文件
src/main/java/com/ots/common/utils/poi/WordUtil.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ots/project/tool/PdfDockerUtil.java 292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ots/project/tool/PdfUtil.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ots/project/tool/report/LAQ/chart/LAQChart.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ots/common/utils/poi/WordUtil.java
@@ -12,6 +12,7 @@
import com.ots.project.exam.domain.TReportTemplate;
import com.ots.project.exam.dto.JAQTableStyle;
import com.ots.project.exam.dto.WordParam;
import com.ots.project.tool.PdfDockerUtil;
import com.ots.project.tool.PdfUtil;
import com.ots.project.tool.ShellTool;
import com.ots.project.tool.exam.ExamUtil;
@@ -1236,7 +1237,7 @@
        } catch (Exception e) {
            e.printStackTrace();
        }
        //删除文件
        deleteFileByStr(deleteFileStrList);
        return AjaxResult.success(fileName + ".zip");
    }
@@ -1287,7 +1288,8 @@
            }else if(ReportTypeEnum.LAQ.getCode().equals(reportType)){
                // 获取开始时间
                long startTime = System.currentTimeMillis();
                PdfUtil.dockerConvertPDF(EssConfig.getProfile(),EssConfig.getDocx2pdfPath(),reportName);
//                PdfUtil.dockerConvertPDF(EssConfig.getProfile(),EssConfig.getDocx2pdfPath(),reportName);
                PdfDockerUtil.convertPDF(EssConfig.getProfile(),EssConfig.getDocx2pdfPath(),reportName);
                // 获取结束时间
                long endTime = System.currentTimeMillis();
src/main/java/com/ots/project/tool/PdfDockerUtil.java
New file
@@ -0,0 +1,292 @@
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);
    }
}
src/main/java/com/ots/project/tool/PdfUtil.java
@@ -84,6 +84,7 @@
     * @param profile
     * @param docx2pdfPath
     * @param fileName
     * 这个有时候docker转化会卡死,2025.7.15拟采用pdfDockerUtil新方法处理
     */
    public static void dockerConvertPDF(String profile,String docx2pdfPath,String fileName){
        try {
src/main/java/com/ots/project/tool/report/LAQ/chart/LAQChart.java
@@ -48,17 +48,17 @@
    public static void main(String[] args) throws Exception {
        String dataPath = "D:\\测评系统\\需求\\LAQ\\report\\LAQ表头数据文件.xlsx";
        // 获取导入数据
        File file = new File(dataPath);
        InputStream in = new FileInputStream(file);
        ExcelUtil<LAQTemplate> util = new ExcelUtil<>(LAQTemplate.class);
        List<LAQTemplate> laqTemplateList = util.importExcel(in);
//        String dataPath = "D:\\测评系统\\需求\\LAQ\\report\\LAQ表头数据文件.xlsx";
//        // 获取导入数据
//        File file = new File(dataPath);
//        InputStream in = new FileInputStream(file);
//        ExcelUtil<LAQTemplate> util = new ExcelUtil<>(LAQTemplate.class);
//        List<LAQTemplate> laqTemplateList = util.importExcel(in);
        // 生成文件集合
        List<String> fileNameList = new ArrayList<>();
        List<String> deleteStrList = new ArrayList<>();
        for (int i = 0; i < laqTemplateList.size(); i++) {
            LAQTemplate laqTemplate = laqTemplateList.get(i);
        for (int i = 0; i < 1; i++) {
//            LAQTemplate laqTemplate = laqTemplateList.get(i);
            //组装报告所用数据
            Map<String, Object> textMap = new HashMap<>();
//            textMap.put("sendEmailFileName",laqTemplate.getName());
@@ -83,7 +83,7 @@
//            String zipFileName = WordUtil.makeReportFile("D:\\测评系统\\需求\\LAQ\\report\\LAQ英文报告-20230926.docx", template, textMap, new HashMap<>(), deleteStrList);
//            fileNameList.add(zipFileName);
            ZipSecureFile.setMinInflateRatio(0.001);
            try (OutputStream os = new FileOutputStream("D:\\测评系统\\需求\\LAQ\\report\\LAQ英文模板-wzp-修改版-1.docx");XWPFDocument document = new XWPFDocument(POIXMLDocument.openPackage("D:\\测评系统\\需求\\LAQ\\report\\LAQ英文模板-wzp-修改版.docx"))){
            try (OutputStream os = new FileOutputStream("D:\\TaiOS\\LAQ-1.docx");XWPFDocument document = new XWPFDocument(POIXMLDocument.openPackage("D:\\TaiOS\\LAQ.docx"))){
                Map<Integer,List<SeriesData>> seriesDatas = initData(textMap);
                changeChart(document,seriesDatas,textMap);
                document.write(os);