# doc2pdf-service **Repository Path**: aistudy_3/doc2pdf-service ## Basic Information - **Project Name**: doc2pdf-service - **Description**: 基于 Spring Boot + LibreOffice 实现的文档转 PDF 服务,支持 DOC/DOCX/PPT/PPTX 等多种格式,提供任务状态管理、进程监控、超时重试等企业级特性。 - **Primary Language**: Java - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 3 - **Created**: 2026-04-30 - **Last Updated**: 2026-04-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 文档转PDF服务(doc2pdf-service) 基于 Spring Boot + LibreOffice 实现的文档转 PDF 服务,支持 DOC/DOCX/PPT/PPTX 等多种格式,提供任务状态管理、进程监控、超时重试等企业级特性。 ## 项目特性 - **多格式支持**:适配 DOC/DOCX/PPT/PPTX/XLS/XLSX/TXT/RTF/ODT 等常见文档格式 - **高可用性**: - LibreOffice 进程池化管理,核心保障机制针对 soffice(LibreOffice 核心进程)特性设计:支持内存超限自动销毁重启(因 soffice 进程在长期处理多文档、大体积文档场景下,存在内存回收不彻底、占用持续攀升的固有问题,当进程内存占用超过设定阈值时,系统将强制杀死该异常进程并重启新进程,避免内存泄漏导致服务不可用);同时支持空闲超时回收(进程闲置超过配置时间后自动销毁并补充新进程),减少资源无效占用。 - 任务超时联动处理机制,当文档处理任务超出预设时间时,将强制重启对应 LibreOffice 进程,从根源规避 soffice 进程因异常阻塞、死锁等问题持续占用 CPU、内存资源,保障整体服务稳定性。 - **安全可靠**: - 文档内容有效性校验(防止恶意构造的畸形文件、含恶意代码的文档进入处理流程,避免因文件异常导致 soffice 进程崩溃或引发系统安全风险) - **易扩展**: - 多 LibreOffice 服务器节点配置,支持基于任务量的负载均衡策略,可根据业务规模灵活增加节点数量,解决单节点下 soffice 进程资源瓶颈问题,提升整体文档处理吞吐量。 - **便捷接口**:提供 HTTP API 用于任务提交、状态查询、PDF 下载,接口设计遵循 RESTful 规范,参数清晰、调用逻辑简单,便于第三方系统快速集成。 - **轻量化**:纯接口极简设计,系统仅通过 RESTful API 对外交互,无独立前端 UI,所有操作(任务提交、状态查询等)均通过 HTTP 接口完成,降低系统冗余,聚焦文档处理核心服务能力输出,减少非必要资源消耗。 ## 核心流程 ### 1. 文档转换流程 ```mermaid graph TD A["提交文档转换请求"] --> B["计算源URL的MD5标识"] B --> C{"查询是否存在历史转换记录"} C -->|无记录| D["创建新任务记录
状态:PENDING
存储sourceUrl、MD5等信息"] D --> E["提交任务至处理流程"] C -->|有记录| F{"判断任务当前状态"} F -->|PROCESSING| G{"是否超时"} G -->|是(早于超时时间)| H["重置任务状态为PENDING
记录重试时间、错误信息"] G -->|否| I["返回当前PROCESSING状态信息"] F -->|PENDING| J{"是否超时"} J -->|是(首次/重试后超时)| H J -->|否| K["返回当前PENDING状态信息"] F -->|FAILED| L["重置任务状态为PENDING
清除错误信息、记录重试时间"] F -->|COMPLETED| M["返回完成状态+PDF访问地址"] H --> E L --> E E --> N{"检查任务是否已入队"} N -->|已入队| O["返回「任务待处理」,流程结束"] N -->|未入队| P["加入线程池队列"] P --> Q["线程池执行任务:验证状态是否为PENDING"] Q -->|状态异常| R["日志提示「无需重复执行」,流程结束"] Q -->|状态正常| S["更新任务状态为PROCESSING
记录处理开始时间"] S --> T["下载源文件至临时目录"] T --> U["调用LibreOffice服务转PDF"] U -->|转换成功| V["更新任务记录:
状态=COMPLETED
PDF路径、完成时间"] U -->|转换失败/超时| W["更新任务记录:
状态=FAILED
错误信息、完成时间"] V --> X["根据配置清理临时文件"] W --> X X --> Y["触发回调通知(若配置)"] Y --> Z["流程结束"] ``` ### 2. 任务处理流程 ```mermaid graph TD A["定时触发(默认60秒间隔)"] --> B{"清理功能是否启用?"} B -- 否 --> C["日志记录:清理器已禁用,结束本次执行"] B -- 是 --> D{"队列使用率是否低于阈值?"} D -- 否 --> E["日志记录:队列容量不满足条件,跳过清理"] D -- 是 --> F["日志记录:开始清理卡住的转换任务"] F --> G["计算最大可处理任务数(基于队列剩余容量)"] %% 处理PROCESSING超时任务分支 G --> H["查询PROCESSING状态且超时的任务(限制最大处理数)"] H --> I{"是否存在PROCESSING超时任务?"} I -- 否 --> J["日志记录:未发现PROCESSING超时任务"] I -- 是 --> K["更新任务状态为FAILED,记录错误信息和完成时间"] K --> L["保存任务状态到数据库"] L --> M["日志记录:清理完成PROCESSING超时任务"] M --> N["重新提交该类任务到转换队列"] %% 处理PENDING超时任务分支(首次PENDING) J --> O["查询首次PENDING(未重试)且超时的任务(限制剩余处理数)"] O --> P{"是否存在首次PENDING超时任务?"} P -- 否 --> Q["日志记录:未发现首次PENDING超时任务"] P -- 是 --> R["更新任务状态为FAILED,记录错误信息和完成时间"] R --> S["保存任务状态到数据库"] S --> T["日志记录:清理完成首次PENDING超时任务"] T --> U["更新剩余可处理数,重新提交该类任务"] %% 处理PENDING超时任务分支(重试后PENDING) Q --> V{"剩余可处理数是否大于0?"} V -- 否 --> W["日志记录:队列容量不足,无法处理重试后PENDING任务"] V -- 是 --> X["查询重试后PENDING且超时的任务(限制剩余处理数)"] X --> Y{"是否存在重试后PENDING超时任务?"} Y -- 否 --> Z["日志记录:未发现重试后PENDING超时任务"] Y -- 是 --> AA["更新任务状态为FAILED,记录错误信息和完成时间"] AA --> AB["保存任务状态到数据库"] AB --> AC["日志记录:清理完成重试后PENDING超时任务"] AC --> AD["重新提交该类任务到转换队列"] %% 流程结束 N --> AE["日志记录:卡住的转换任务清理完成"] U --> AE W --> AE Z --> AE AD --> AE ``` ### 3. 进程监控流程 ```mermaid flowchart TD A["ProcessMonitor初始化"] -->|"读取配置:检查间隔/阈值等"| B["启动定时线程池(libreoffice-process-monitor)"] B --> C["定时触发监控周期(间隔=serverCheckInterval)"] C --> D["执行核心监控任务"] D --> D1["任务1:进程存活检查"] D --> D2["任务2:空闲超时检查"] D --> D3["任务3:内存超限检查"] D --> D4["任务4:CPU过高检查"] D --> D5["任务5:处理超时检查"] %% 任务1:进程存活检查 D1 --> E["遍历所有进程(allProcesses)"] E --> F{"进程存活?
(进程alive + 端口可连)"} F -->|"否"| G["停止失效进程"] G --> H{"端口是否释放?"} H -->|"是"| I["重启进程(维持初始数量)"] H -->|"否"| J["记录错误,等待下次周期"] F -->|"是"| K["跳过,保持进程运行"] %% 任务2:空闲超时检查 D2 --> L["遍历进程最后使用时间(lastUsedTime)"] L --> M{"进程状态=RUNNING?
且空闲时间>maxIdleTime?"} M -->|"是"| N["停止超时空闲进程"] N --> O{"当前进程数<初始进程数?"} O -->|"是"| P["启动新进程补充"] O -->|"否"| Q["不补充,维持当前数量"] M -->|"否"| R["跳过,保持进程运行"] %% 任务3:内存超限检查 D3 --> S["遍历所有存活进程"] S --> T["获取进程内存使用量(MB)"] T --> U{"内存使用>maxMemoryMB?"} U -->|"是"| V["停止超限进程"] V --> W{"端口是否释放?"} W -->|"是"| X["重启进程"] W -->|"否"| Y["记录错误,等待下次周期"] U -->|"否"| Z["跳过,保持进程运行"] %% 任务4:CPU过高检查 D4 --> AA["遍历所有存活进程"] AA --> AB["获取进程CPU使用率(%)"] AB --> AC{"CPU使用率>highCpuThreshold?"} AC -->|"是"| AD["高CPU计数+1"] AD --> AE{"连续计数>=highCpuMaxCount?"} AE -->|"是"| AF["停止高CPU进程"] AF --> AG{"端口是否释放?"} AG -->|"是"| AH["重启进程,重置计数"] AG -->|"否"| AI["记录错误,等待下次周期"] AE -->|"否"| AJ["保持进程运行,保留计数"] AC -->|"否"| AK["重置高CPU计数,保持进程运行"] %% 任务5:处理超时检查 D5 --> AL["遍历所有进程"] AL --> AM{"进程状态=BUSY?"} AM -->|"是"| AN["计算使用时长(当前时间-acquiredTime)"] AN --> AO{"使用时长>processingTimeout?"} AO -->|"是"| AP["停止超时进程"] AP --> AQ{"当前进程数<初始进程数?"} AQ -->|"是"| AR["启动新进程补充"] AQ -->|"否"| AS["不补充,维持当前数量"] AO -->|"否"| AT["跳过,保持进程运行"] AM -->|"否"| AU["跳过,保持进程运行"] %% 流程汇合与循环 I & J & K & P & Q & R & X & Y & Z & AH & AI & AJ & AK & AR & AS & AT & AU --> AV["当前监控周期结束"] AV -->|"等待下一次定时触发"| C ``` ## 技术栈 - 后端框架:Spring Boot 2.x(兼容 JDK 8/11,推荐使用 JDK 11 以获得更优性能和长期支持) - 文档转换:LibreOffice 7.6+(Headless 模式,需确保系统依赖库完整) - 数据存储:JPA + MySQL 8.0+(任务状态持久化,需提前初始化表结构) - 进程监控:Oshi 6.4.0+(系统进程/内存监控,依赖 Linux `proc` 文件系统权限) - 线程管理:自定义线程池(基于 Spring ThreadPoolTaskExecutor,支持配置化核心参数) - 文档校验:Apache POI 4.1.2+(校验 DOC/DOCX/PPTX 文件有效性,避免恶意文件上传) - HTTP 工具:Java 原生 HttpClient(JDK 11+ 内置,用于源文件下载,低版本 JDK 需额外引入依赖) ## 环境准备 ### 1. 基础环境要求 - **Java 版本**:JDK 11或更高(推荐 JDK 11) 验证命令: ```bash java -version # 输出需包含"11.",例如:openjdk version "11.0.18" 2023-01-17 ``` - **Maven 版本**:3.6.x 及以上(推荐 3.6.3 或 3.8.6) 说明:低版本 Maven(如 3.5.x 及以下)可能存在依赖解析漏洞、与 Spring Boot 2.x 插件不兼容问题,导致打包失败或依赖下载超时。 验证命令: ```bash mvn -v # 输出需包含 "Apache Maven 3.6." 及以上,例如:Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63) ``` ### 2. LibreOffice 依赖安装(CentOS 示例) ```bash # 卸载旧版本 LibreOffice yum remove -y libreoffice* # 清理残留文件 rm -rf /usr/lib64/libreoffice/ rm -rf ~/.config/libreoffice/ rm -rf /tmp/libreoffice* # 安装新版本(7.6+) yum install -y libreoffice libreoffice-headless libreoffice-impress # 验证安装 soffice --version ``` ### 3. 环境变量配置(可选) ```bash # 查找 soffice 路径 find / -name soffice 2>/dev/null # 示例:将 /opt/libreoffice7.6/program/soffice 添加到环境变量 echo 'export PATH=$PATH:/opt/libreoffice7.6/program' >> ~/.bashrc source ~/.bashrc ``` ### 4. 依赖包安装(本地仓库) 项目依赖的部分 jar 包已放置在项目根目录的 `lib` 目录下,需先安装到本地 Maven 仓库,执行以下命令即可完成全部安装: ```bash # 进入项目根目录(包含 pom.xml 和 lib 文件夹的目录) cd /path/to/project # 执行安装命令(自动处理 lib 目录下所有 jar 包,跳过测试加速流程) mvn initialize ``` ### 4. 数据库初始化 #### 4.1 前置要求 - **MySQL 版本**:必须为 8.0+(项目使用 `MySQL8Dialect` 方言,不兼容 MySQL 5.7 及以下版本,低版本会出现语法错误如 `DATE_FORMAT` 函数差异、索引类型不支持)。 验证命令: ```bash mysql -V # 输出需包含 "8.0.",例如:mysql Ver 8.0.32 for Linux on x86_64 (MySQL Community Server - GPL) ``` - **数据库权限**:配置文件中 `spring.datasource.username` 需具备以下权限(避免因权限不足导致任务状态更新失败): - `CREATE TABLE`(首次启动自动生成表结构,若手动执行SQL则需 `ALTER TABLE`); - `SELECT/INSERT/UPDATE/DELETE`(查询、创建、更新、删除转换任务记录); - `INDEX`(维护 `source_url_md5` 唯一索引,避免重复任务提交)。 授权命令示例(**需在 MySQL 终端执行**,而非 Linux Bash 终端;生产环境建议创建专用账号,而非直接使用 root): 1. 先登录MySQL(Linux Bash终端执行此命令,进入MySQL交互模式) ```bash mysql -u root -p (执行后输入MySQL root密码,进入mysql> 提示符界面) ``` 2. 在MySQL交互模式下,执行以下授权命令 ```sql GRANT CREATE, SELECT, INSERT, UPDATE, DELETE ON file_conversion.* TO 'root'@'localhost'; FLUSH PRIVILEGES; -- 刷新权限,使授权立即生效 ``` 3. 验证授权是否成功(可选) ```sql SHOW GRANTS FOR 'root'@'localhost'; -- 查看root账号的权限列表,确认包含file_conversion库的权限 ``` #### 4.2 表结构初始化 项目需先创建 `file_conversion` 数据库(若未创建),再执行表结构初始化: ##### 手动执行 SQL(推荐生产环境) **执行步骤**: 1. 登录 MySQL 终端(Linux Bash 终端执行,输入密码后进入 `mysql>` 交互模式): ```bash mysql -u root -p # 生产环境替换为专用账号(如 conv_user) ``` 2. 创建数据库(若尚未创建,数据库名需与配置文件中 spring.datasource.url 一致): ```sql CREATE DATABASE IF NOT EXISTS file_conversion DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; USE file_conversion; # 切换到目标数据库 ``` 3. 表结构初始化语句位于项目根目录下的 sql/schema.sql 文件中,请执行该文件完成表结构创建 4. 验证表是否创建成功(可选): ```sql SHOW TABLES; # 若输出 `file_conversion` 表名,说明创建成功 DESC file_conversion; # 查看表结构,确认字段/注释正确 ``` ## 快速开始 ### 步骤 1:修改核心配置 1. 进入项目源代码目录:`src/main/resources/` 2. 编辑配置文件 `application.properties`(你的项目实际使用的配置文件),修改以下关键配置(其他配置可默认,根据实际需求调整): ```properties # ============================== # 1. 数据库配置(必填) # ============================== spring.datasource.url=jdbc:mysql://localhost:3306/file_conversion?useUnicode=true&characterEncoding=utf8 spring.datasource.username=数据库用户名 spring.datasource.password=数据库密码 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # ============================== # 2. LibreOffice 服务器配置(必填) # ============================== # 至少配置1个节点,多节点用逗号分隔 libreoffice.servers[0].host=localhost libreoffice.servers[0].port=2002 # ============================== # 3. 存储配置(必填) # ============================== # PDF文件本地存储目录(需提前创建并赋予读写权限) storage.pdf-directory=/data/apps/file-conversion/pdf # PDF文件访问基础路径(URL路径) storage.pdf-access-path=http://localhost:8080/api/conversion/download/ ``` ### 步骤 2:本地开发环境启动(可选,用于开发调试) 运行FileConverterApplication类的main方法,启动服务,控制台输出类似日志表示启动成功: ```text Started FileConverterApplication in 12.347 seconds (JVM running for 13.068) ``` ### 步骤 3:项目打包与启动 ```bash # 打包(跳过测试) mvn clean package -Dmaven.test.skip=true # 赋予脚本执行权限 chmod +x start.sh stop.sh # 启动 ./start.sh # 停止服务 ./stop.sh ``` ## API 文档 所有 API 均为 HTTP GET 请求,返回格式为 JSON,编码统一为 UTF-8。 ### 1. 发起文件转换任务 - **功能**:提交一个文档转 PDF 的任务(重复 URL 会自动复用或重试任务) - **请求方式**:GET - **URL**:`/api/conversion/submit` - **请求参数**: | 参数名 | 类型 | 必需 | 说明 | 示例 | |-----------|--------|------|--------------------------|-------------------------------| | sourceUrl | String | 是 | 待转换的源文件公共 URL | http://example.com/test.docx | - **成功响应**(HTTP 200): ```json { "taskId": 1, // 任务唯一ID "status": "PENDING", // 任务状态(PENDING/PROCESSING/COMPLETED/FAILED) "message": "转换任务已提交" } ``` ### 2. 发起文件转换任务(支持回调,适用于调用方是后台程序) - **功能**:提交一个文档转 PDF 的任务(重复 URL 会自动复用或重试任务) - **请求方式**:GET - **URL**:`/api/conversion/submit/with-callback` - **请求参数**: | 参数名 | 类型 | 必需 | 说明 | 示例 | |-------------|--------|------|--------------------------|------------------------------| | sourceUrl | String | 是 | 待转换的源文件公共 URL | http://example.com/test.docx | | businessId | String | 否 | 调用方业务ID | | | callerId | String | 否 | 调用方唯一标识 | | | callbackUrl | String | 否 | 回调地址 | | - **成功响应**(HTTP 200): ```json { "taskId": 1, // 任务唯一ID "status": "PENDING", // 任务状态(PENDING/PROCESSING/COMPLETED/FAILED) "message": "转换任务已提交" } ``` ### 3. 查询转换任务状态 - **功能**:查询指定源文件 URL 的转换任务状态,若完成则返回 PDF 访问 URL,若失败则返回错误信息 - **请求方式**:GET - **URL**:/api/conversion/status - **请求参数**: | 参数名 | 类型 | 必需 | 说明 | 示例 | |-----------|--------|------|--------------------------|-------------------------------| | sourceUrl | String | 是 | 待转换的源文件公共 URL | http://example.com/test.docx | - **成功响应**(HTTP 200): ```json { "taskId": 1, "status": "COMPLETED", // 任务状态(PENDING/PROCESSING/COMPLETED/FAILED) "createdTime": "2024-05-20T10:00:00", // 任务创建时间 "pdfUrl": "http://example.com/api/conversion/download/2024/05/20/document.pdf", // 仅当状态为COMPLETED时返回 "completedTime": "2024-05-20T10:05:00", // 仅当状态为COMPLETED时返回 "errorMessage": null // 仅当状态为FAILED时返回具体错误信息 } ``` - **失败响应**(HTTP 404): ### 4. 文件下载接口 - **功能**:提供转换完成的 PDF 文件下载服务 - **请求方式**:GET - **URL**:/api/conversion/download/** - **请求参数**:无需显式请求参数(如:2024/05/20/sample.pdf) - **成功响应**(HTTP 200): PDF 文件二进制流 - **失败响应**: 1. 文件不存在 - **状态码**:404 - **响应体**:无 2. 服务器内部错误 - **状态码**:500 - **响应体**:无 ## 常见问题 ### 如何查看核心线程是否卡死? 1. **使用JDK自带工具监控线程状态** - 执行命令查看进程ID ```bash jps -l | grep doc2pdf-service ``` - 执行`jstack <进程ID>`命令获取线程堆栈信息,搜索线程名包含`Conversion-`关键字的线程 ```bash jstack 3312134 | grep -A 20 "Conversion-" ``` - 若线程状态长时间处于`RUNNABLE`且堆栈信息不变,或处于`BLOCKED`状态且持有锁资源,可能存在卡死情况 ### 如何清理 LibreOffice 残留的临时目录? LibreOffice 在处理文档过程中可能会在/tmp目录下生成临时文件(如lu*.tmp格式目录),长期积累可能占用磁盘空间。可通过以下脚本定期清理 2 天前的残留临时目录: ```bash #!/bin/bash # 清理LibreOffice残留的临时目录(包括非空目录) # 目标:删除/tmp下2天前的lu*.tmp目录及soffice_user_*下的相关临时目录 # 执行删除操作(强制递归删除非空目录) # 删除/tmp下直接的lu*.tmp临时目录 find /tmp -type d -name "lu*.tmp" -mtime +2 -exec rm -rf {} + # 删除soffice_user_*子目录中的lu*.tmp临时目录 find /tmp -type d -path "/tmp/soffice_user_*/user/extensions/*/lu*.tmp" -mtime +2 -exec rm -rf {} + # 输出清理结果 echo "清理完成!已删除/tmp下2天前的LibreOffice临时目录(包括非空目录)" ``` 建议通过crontab设置定时任务(如每天凌晨执行),避免临时文件堆积: ```bash # 编辑定时任务 crontab -e # 添加以下内容(每天3点执行) 0 3 * * * /path/to/clean-libreoffice-tmp.sh >> /var/log/clean-libreoffice-tmp.log 2>&1 ```