# 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
```