ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot整合LibreOffice实现文档在线预览与编辑系统

SpringBoot整合LibreOffice实现文档在线预览与编辑系统 简介基于Spring Boot与LibreOffice的文档在线预览编辑系统源码包定位给Java后端开发者、毕业设计学生及需要集成文档预览/编辑能力的技术团队可快速解决Word、Excel、PPT、PDF等格式在线转换、预览与编辑的落地问题。整套源码共81个文件56个Java文件覆盖上传、转换、编辑、保存等核心业务逻辑5个HTML配合JS/CSS形成前端操作界面2个JAR包内置jodconverter与jacob转换依赖另有pom.xml、resources配置文件及mvnw构建脚本压缩包仅376KB目录结构规范便于导入IDE直接改造。项目已吸引408人学习完整实现文档上传与列表管理、自动转HTML预览、单击编辑、保存新版本或放弃修改、水印添加等功能并保留test测试目录供拓展验证。拿到这份资源可掌握Spring Boot组织LibreOffice转换服务、前端预览交互与文件持久化的完整链路很适合作为企业工具类模块的参考基座或毕设项目二次开发起点。1. 文档在线预览编辑系统先用一个 headless 命令解决 80% 的预览需求做过 OA、网盘或知识库系统的后端应该都遇到过同一个需求用户上传了一个 .docx老板要求不下载就能在浏览器里看最好还能顺手改两笔。第一反应可能是接在线转换 API 或者引入 OnlyOffice但这两条路都有成本与改造上的硬约束。这份基于 SpringBoot 和 LibreOffice 的文档在线预览编辑系统源码走的是一条更可控的路线后端用 LibreOffice 无头模式做格式转换前端渲染预览结果编辑层再按业务深度选择轻量改或对接协作服务。它适合已经在做 Java 后端、想把文档处理能力掌握在自己手里的团队也适合课程设计想做点出圈功能的同学。接下来我按实际拆包顺序把这套系统的核心链路、落地代码和踩过的坑完整过一遍。2. 预览链路选型在线 API、OnlyOffice、LibreOffice 自建怎么选先讲结论我拿到任何一个文档预览需求会先问三个问题——数据能不能出网、预算多少、要留多大的二次开发空间。第一个问题直接砍掉一大批在线转换 API。很多业务文档带合同编号、客户姓名公司安全评审根本不会同意让文件经过第三方转换服务。第二个问题决定上不上 OnlyOffice它做在线编辑确实强但部署形态偏重对前端框架接管较多授权协议里也有商用边界小团队容易踩线。LibreOffice 走的是 LGPL 系协议集成方式本质是命令行工具SpringBoot 工程里加一个 service 就能调度改造空间最大。2.1 三条路线先摆平数据安全、成本、二次开发空间把三条路线摊开看各有各的适用场景直接看这张表更直观。路线转换质量数据是否出网二次开发成本授权风险典型场景在线转换 API高厂商调过字体包出网有合规风险低接个 HTTP 接口按量付费长期贵公网小工具、无敏感数据的 DemoOnlyOffice Document Server很高编辑体验接近 Office可不出网自部署中高前端接管重GPL/商业双授权改动内置代码需谨慎带多人协作的成熟产品LibreOffice headless中高依赖系统字体和参数不出网低一个命令行 一个 ServiceLGPL/Mozilla宽松自家 OA、网盘、课程设计我一般建议业务系统优先选自建 LibreOffice。理由很朴素预览需求的核心是“排版保真”LibreOffice 本身就是完整的字处理和表格渲染引擎doc 转 pdf 是它内部排版引擎直接输出的结果不是拿 POI 拼 HTML 那种还原度看运气的做法。OnlyOffice 的强项在实时协同编辑如果需求只是“打开看看 偶尔改改”投入产出比不划算。2.2 为什么 Apache POI 做不了在线预览这是最容易踩的第一个认知坑。POI 能解析 docx 里的 XML、能读出文字和表格、能从单元格里取数但它没有排版引擎不能把页面渲染成大 A4 的固定版式。预览系统要的是“看起来和 Word 里一模一样”POI 输出的 HTML 样式还原基本靠玄学遇到分页、页眉页脚、图片浮动就直接翻车。LibreOffice 不一样。它内部是一个完整的字处理和渲染引擎--headless只是让它不弹窗口、纯后台执行排版逻辑一点没少。这也是为什么soffice --convert-to pdf能成为整个预览链路的基石它把一个 docx 当成一份真实文档重新排版而不是当成一堆 XML 节点做字符串拼接。这条原理决定了下面所有代码的参数选择——我们不是要“提取内容”而是要“完整渲染”。2.3 这套源码的骨架先分清是 ProcessBuilder 还是 JODConverter你拿到源码包后先别急着跑打开pom.xml看一眼依赖。如果里面有jodconverter-local说明作者是用 JODConverter 管理 soffice 进程的转换逻辑里会有OfficeManager、LocalOfficeManager这类 API如果只有 SpringBoot 基础依赖那大概率是自己写ProcessBuilder调命令。两种写法我都见过后者的代码更直白适合新手读懂全链路前者能自动处理进程池和超时生产环境更稳。顺着源码里的src/main/java往下走项目结构基本是固定的controller层收文件、service层做转换、config层配路径和格式白名单。这里要提醒一个版本坑现在很多新拉出来的 SpringBoot 脚手架已经是 3.xjavax.servlet换成了jakarta.servlet。如果这份源码还是基于 2.x 写的javax包而你本地环境是 JDK17 SpringBoot 3.x项目会直接启动失败报一堆ClassNotFoundException。处理方式是两个要么把 pom 里 SpringBoot 版本锁回 2.7.x要么全局替换 import 头。这个锅不全是项目的是 SpringBoot 版本太高带来的兼容性变化。3. 从上传到预览soffice 无头转换的落地代码与参数细节把原理说清楚了接下来落到代码。这一章是整个系统的核心SpringBoot 接收上传文件调用 soffice 无头命令把 Office 文档转成 PDF然后把 PDF 路径扔给前端预览。看起来就三步实际写起来全是细节漏一个参数都可能让你在部署时怀疑人生。3.1 别用 Runtime.exec 拼命令ProcessBuilder 传参的正确姿势很多网上教程给的是这种写法Runtime.getRuntime().exec(soffice --headless --convert-to pdf --outdir /tmp fileName)。这种写法在 Linux 上能用但有两个隐患一是文件名里有空格或特殊字符时命令被截断二是存在命令注入风险哪怕后端只处理内部文件也不该留这种口子。我在这个项目里一律用ProcessBuilder参数按列表传不做字符串拼接。// ConvertService.java Service public class ConvertService { private static final String SOFFICE_PATH /usr/bin/soffice; private static final String CONVERT_DIR /data/convert; public boolean toPdf(String sourcePath, String targetName) throws IOException, InterruptedException { String targetDir CONVERT_DIR / targetName; Files.createDirectories(Paths.get(targetDir)); ProcessBuilder pb new ProcessBuilder( SOFFICE_PATH, --headless, --norestore, // 不恢复上次会话防止启动时卡在弹窗 -env:UserInstallationfile:///tmp/lo_install_ targetName, --convert-to, pdf:writer_pdf_Export, // 显式指定导出过滤器 --outdir, targetDir, sourcePath ); pb.redirectErrorStream(true); // 把 stderr 合并到 stdout日志好查 Process process pb.start(); // 阻塞读取输出避免子进程因管道写满而卡死 String log new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); if (!process.waitFor(5, TimeUnit.MINUTES)) { process.destroyForcibly(); return false; } return process.exitValue() 0 Files.exists(Paths.get(targetDir, targetName .pdf)); } }这段代码有几个参数值得单独解释。-env:UserInstallationfile:///tmp/lo_install_xxx是每个转换任务给 LibreOffice 指定独立的用户配置目录这是最容易被忽略的一个参数。LibreOffice 首次运行会在用户目录下初始化配置文件如果你直接用默认配置并发转换时多个 soffice 进程会去抢同一个 profile 锁表现就是第二个任务卡住不动。按任务 ID 隔离配置目录能让每个转换任务互不干扰。--norestore是为了防止意外退出后弹出文档恢复界面在 headless 模式下这个弹窗永远无法被用户点击会直接卡死整个转换。pdf:writer_pdf_Export显式指定了输出过滤器避免 LibreOffice 根据扩展名推断格式时出现偏差。超时控制在 5 分钟超过就强杀防止坏文档把线程池拖死。3.2 Controller 里的上传与预览接口白名单与路径安全Service 写完Controller 就简单了。但简单不代表能随便写文件上传接口有两条安全红线扩展名白名单和文件重命名。原文件名一律不直接落盘避免路径穿越也避免中文文件名在 Linux 和 Windows 之间传递时编码不一致。// PreviewController.java RestController public class PreviewController { private static final SetString SUPPORTED_EXTS Set.of(doc, docx, xls, xlsx, ppt, pptx, odt, ods, odp); private final ConvertService convertService; public PreviewController(ConvertService convertService) { this.convertService convertService; } PostMapping(/preview) public ResponseEntityString preview(RequestParam(file) MultipartFile file) throws IOException { if (file.isEmpty()) { return ResponseEntity.badRequest().body(empty file); } String id UUID.randomUUID().toString().replace(-, ); String ext extractExtension(Objects.requireNonNull(file.getOriginalFilename())); if (!SUPPORTED_EXTS.contains(ext.toLowerCase())) { return ResponseEntity.status(415).body(unsupported type: ext); } Path temp Path.of(/data/uploads, id . ext); Files.createDirectories(temp.getParent()); file.transferTo(temp.toAbsolutePath()); if (!convertService.toPdf(temp.toString(), id)) { return ResponseEntity.status(500).body(convert failed, check server log); } return ResponseEntity.ok(/files/ id .pdf); } private String extractExtension(String fileName) { int dot fileName.lastIndexOf(.); return dot 0 ? fileName.substring(dot 1) : ; } }白名单只放纯 Office 格式和 ODF 格式PDF 本身不需要转换直接走静态资源映射。UUID 重命名把业务文件名和存储文件名解耦后续做权限控制也更方便用户拿到 /files/xxxx.pdf 这个地址时没有原文件名信息减少信息泄露面。前端预览我习惯用embed :srcpdfUrl或者 pdf.js 渲染具体选哪个看业务要不要缩放标注单纯预览前者足够。3.3 并发转换LibreOffice 不是线程安全的要限流这是从“能跑”到“能扛”必须要过的一关。LibreOffice 的 soffice 进程不是线程安全的多个实例同时拿同一个配置目录会互相锁死。更隐蔽的是它第一次启动后会有后台驻留进程加速后续启动这个驻留进程和显式启动的进程之间也可能冲突。常见做法是用信号量把并发数控制在 2 到 4 个。我一般让转换接口变成排队执行宁可多等几秒也不要并发时全军覆没。// ConvertTask.java Component public class ConvertTask { private final Semaphore slots new Semaphore(2); // 最多允许两个并发转换 private final ConvertService convertService; public ConvertTask(ConvertService convertService) { this.convertService convertService; } public boolean convertWithLimit(String sourcePath, String targetName) throws Exception { boolean acquired false; try { slots.acquire(); acquired true; return convertService.toPdf(sourcePath, targetName); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw e; } finally { if (acquired) { slots.release(); } } } }为什么并发数设 2 而不是更大的数字因为 LibreOffice 转换是 CPU 密集型任务每个转换进程会吃掉 300MB 到 500MB 内存4 核 8G 的机器开 4 个并发基本就把资源占满了后续请求会大面积超时。如果你的机器是 8 核 16G可以调到 4但不建议更多。参数怎么调压测说了算先用 JMeter 或ab打 50 个并发请求观察转换成功率和平均耗时再决定信号量数字。4. 在线编辑的两种实现路线HTML 轻改与 Collabora 重协作预览只是“读”标题里还有“编辑”两个字。编辑怎么做取决于源码里实现了哪条路线以及你的业务到底要改到什么程度。这一章把两条路都讲清楚你先看手里的包属于哪一种再决定要不要补另一条。4.1 轻量路线转 HTML 后做填空与批注如果只是改字、填空、写批注不需要严格保留 Word 分页效果转 HTML 再让前端做contenteditable是最快的方案。soffice 本身就支持 HTML 输出命令如下。soffice --headless --norestore \ -env:UserInstallationfile:///tmp/lo_edit \ --convert-to html \ --outdir /data/html \ /data/contract.docx转出来的 HTML 会带一段内联 CSS页面结构基本保留标题、段落、表格和列表图片会被转成 base64 嵌在 HTML 里前端拿到后可以直接渲染。编辑完保存回写时SpringBoot 提供一个收 HTML 的接口。// EditController.java PostMapping(/save) public ResponseEntityString save(RequestParam String id, RequestBody String htmlContent) throws IOException { Path htmlPath Path.of(/data/html, id .html); Files.writeString(htmlPath, htmlContent, StandardCharsets.UTF_8); // 如果需要下载 docx再用 soffice 把 html 转回 docx return ResponseEntity.ok(saved); }这条路线的边界要提前想清楚。HTML 回写 Word 时分页符、页眉页脚、域代码比如自动页码基本都会丢排版会肉眼可见地“松掉”。它适合的场景是合同填写、审批意见、课程设计里“能编辑”这个功能的演示不适合正式出版文稿的在线修改。如果你的系统面向的是法务这种对格式敏感的岗位直接看下一条。4.2 重型路线Collabora Online 与 WOPI 协议对接要对 Word 做接近原生的在线编辑开源方案基本绕不开 Collabora Online也就是 LibreOffice Online 的继任者。它的架构是Collabora 服务端跑一个coolwsd进程负责把文档渲染到浏览器并提供编辑工具SpringBoot 这边要接入 WOPI 协议把自己伪装成文件服务端。WOPI 协议的核心对接点就三个接口看这张表就明白工作量在哪。WOPI 接口作用SpringBoot 侧要做的事CheckFileInfo返回文件元数据、权限、编辑人GET /wopi/files/{fileId}返回 JSON 描述文件GetFile把文件流交给 Collabora 渲染GET /wopi/files/{fileId}/contents返回原始字节PutFile保存编辑结果POST /wopi/files/{fileId}/contents接收新文件流落盘也就是说文档的解析、渲染、编辑工具全在 Collabora 那边SpringBoot 只负责三件事鉴权、读文件、写文件。前端浏览器打开的是 Collabora 提供的页面地址通常是https://collabora.example.com/lool/...前端把文件 ID 拼进 URL 就行。如果拿到的源码包只做了预览、没有编辑功能想补的话我建议按顺序来先实现 4.1 的 HTML 轻改业务验证通了再考虑上 Collabora。后者对机器要求不低实测 4 核 8G 的虚拟机跑coolwsd再加一个文档会话内存已经比较紧张多人同时编辑同一个大文档会明显变慢。另外它和 SpringBoot 的集成不是加个依赖那么简单需要单独维护一个协同服务进程这已经是运维层面的工作了。5. 避坑排查进程残留、字体缺失、并发锁冲突的 5 个实战案例这个章节全是血泪经验。LibreOffice 转换本身不复杂复杂的是它在不同环境下的各种“灵异现象”。我挑 5 个高频案例每个都按现象、原因、解决三段写你部署时直接对照查。5.1 命令执行成功输出 PDF 却是 0 字节现象日志里能看到 soffice 进程正常退出退出码是 0但生成的 PDF 文件大小是 0 字节。原因LibreOffice 转换时会把原文件复制到内部临时目录处理转换完成后写到 --outdir 指定目录。如果磁盘空间不足或者输出目录的属主不是运行 Java 进程的用户它不会报错直接生成了一个空文件然后退出。解决先df -h看分区剩余空间转换目录要预留至少原文件 2 倍的空间。然后chown给 Java 进程的运行用户或者把/data/convert权限设为 755 且属主正确。最后在代码里加一个校验Files.size(output) 0不满足就算转换失败。5.2 高频调用后进程越来越多内存被吃满现象ps -ef | grep soffice看到几十个 soffice.bin 进程服务器内存飙升接口开始超时。原因LibreOffice 为加速启动会在首次执行后常驻一个后台进程。如果转换任务用的 UserInstallation 目录各不相同它就不会复用主进程而是每次新建一个独立实例用完又不完全释放进程就累积起来了。解决转换队列的信号量控好并发任务结束后用process.waitFor()确保回收同时启动一个定时任务每小时检查一次soffice.bin数量超过阈值就pkill -9 soffice。最干净的办法是用 3.1 里那种按任务隔离 UserInstallation 目录的方式再配合 limit 让同时运行的实例不超过两个。5.3 本地 Windows 转得好好的Linux 部署后中文全变方块现象Windows 开发环境转换一切正常部署到 Linux 服务器后转出来的 PDF 中文全是空心方块。原因LibreOffice 渲染靠系统字体Linux 服务器默认没有宋体、黑体、微软雅黑这些中文字体它找不到可用字体就 fallback 到一个空白字形。解决装中文字体包Debian/Ubuntu 下执行apt-get install -y fonts-noto-cjk fonts-wqy-zenhei。装完后运行fc-cache -fv刷新字体缓存再用fc-list | grep -i cjk确认中文字体已被识别。这一步必须写进部署脚本否则每次换机器都要手动来一遍而且不是所有基础镜像都带这些字体包Docker 部署时尤其容易踩。5.4 加了信号量限流后反而全挂所有转换任务超时现象并发 10 个文件转换时全部失败日志里显示每个任务都在等待或超时。原因这是 3.1 里-env:UserInstallation的坑。如果所有任务都指向同一个配置目录第二个 soffice 进程启动时检测到 profile 被锁会一直等锁直到超时。信号量管住了并发度但没解决配置目录冲突两个机制叠加反而把本可正常排队的任务也拖死了。解决每个任务用独立的 UserInstallation 目录用完删除代码里就是Paths.get(/tmp/lo_install_ targetName)这样按 ID 拼路径。如果你不想自己管这个逻辑可以直接依赖jodconverter-local它内部的 OfficeManager 已经封装了进程池和 profile 隔离省去不少细节功夫。5.5 文件名带空格和括号转换结果文件名错乱现象上传一个叫“施工合同(最终版).docx”的文件转出来的 PDF 文件名字符错乱甚至直接失败。原因用字符串拼接命令的方式空格和括号会被 Shell 解析括号还有可能触发 Shell 的语法错误。Windows 环境下 JVM 默认编码如果不是 UTF-8中文文件名传到 soffice 参数里也会乱码。解决落盘时统一重命名为 UUID 原扩展名源头消灭特殊字符。Controller 里我已经写了UUID.randomUUID()的做法这里再补一句JVM 启动参数也要加-Dfile.encodingUTF-8否则文件内容的中文没问题但文件名和日志会出乱码。6. 验证与加固用 Docker 钉死运行环境把转换服务变成可监控微服务最后一章讲验证和交付。LibreOffice 这个软件对系统环境非常敏感字体、依赖库、用户目录稍有不同转换结果就不一样。为了让“在我机器上好好的”变成“在任何机器上都好好的”我会把整个转换环境塞进 Docker。# Dockerfile FROM ubuntu:22.04 RUN apt-get update apt-get install -y --no-install-recommends \ libreoffice-writer libraoffice-calc libreoffice-impress \ fontconfig fonts-noto-cjk fonts-wqy-zenhei \ openjdk-17-jre-headless ca-certificates \ rm -rf /var/lib/apt/lists/* ENV LANGC.UTF-8 \ JAVA_OPTS-Dfile.encodingUTF-8 -Xmx2g WORKDIR /app COPY target/doc-viewer.jar /app/app.jar EXPOSE 8080 CMD [sh, -c, java $JAVA_OPTS -jar /app/app.jar]这里的libreoffice-writer、libreoffice-calc、libreoffice-impress对应 Writer、Calc、Impress 三个组件按源码支持的文件类型选装不需要装全家桶。fonts-noto-cjk是 5.3 案例的预防针镜像里直接带好避免部署时临时补字体。启动容器时有两个参数必须带上内存上限和转换目录挂载。docker run -d \ -m 4g \ -p 8080:8080 \ -v /data/convert:/data/convert \ -v /data/uploads:/data/uploads \ doc-viewer:latest内存限制 4g 是给镜像里的 LibreOffice 和 JVM 一起留的余量容器内的 soffice 单个转换任务吃 300~500MBJVM 堆设 2g剩余留给系统开销。转换目录挂卷到宿主机这样容器重启后之前生成的预览文件不会丢。部署完先别急着接业务我习惯按下面几张表验一遍再交付。验证项操作预期结果中文渲染上传含宋体中文的 docx转 PDF 后用pdftotext抽取文本中文文字完整无乱码方块特殊文件名上传施工合同(最终版).docx转换成功返回 /files/xxx.pdf并发稳定性ab -n 50 -c 10 -F filetest.docx POST /preview成功率 100%无超时容器重启docker restart后访问已有 PDF 地址文件仍可访问转换服务自动恢复还有一个技巧可以放进交付文档SpringBoot 的 Actuator 里加一个自定义健康检查端点定时在转换目录放一个测试 docx跑一次转换如果失败就把/actuator/health置为 DOWN。这样监控系统能第一时间发现 soffice 进程异常不用等用户反馈“预览打不开”才排查。我从那以后每次部署这套 SpringBoot 整合 LibreOffice 的预览系统都会强制走一遍这个 Dockerfile 和验证清单确认中文字体、并发压测、容器重启三项都过了再交付。至于编辑接口是轻量还是重量先看 controller 里有没有/save再决定要不要补 Collabora。希望帮到你。本文还有配套的精品资源点击获取
返回列表