简介kkFileView 4.4.0-SNAPSHOT 是一套面向开发者与运维人员的开源文档在线预览部署包定位为可替代部分付费 Office 预览方案的自建服务适合在 OA、网盘、文档管理及互联网项目中快速集成文件预览能力。压缩包共 8 个文件约 285.23MB主体由 4 个 Shell 脚本承担安装、启动、停止与日志查看另含核心 jar 运行包、txt 说明文档、ftl 页面模板和 properties 配置文件构成较完整的部署与参数调整入口运行环境需预留 JDK1.8 基础。已有 2675 人学习下载说明其在自建预览场景中具备一定参考价值。读者可获得可直接解压部署的服务端程序、脚本化启停流程与配置模板便于理解预览服务的目录组织、运行依赖和基础调优方式也能据此排查中文字体显示等常见问题对需要快速验证开源预览方案、控制软件采购成本或在内网环境中部署的团队较为友好。1. 一份能直接跑起来的在线预览服务省掉三周的格式适配Word、Excel、PPT、PDF、压缩包、CAD、音视频混在一个业务系统里要做成「点一下就能看」自己从零写转换链路基本是三周起步Office 得转 PDF、PDF 得分片加载、图片要防盗链、大文件要断点续传、中文字体还动不动渲染成方框。kkFileView-4.4.0-SNAPSHOT.tar.gz 就是把这条链路打包好的服务端一个 Spring Boot 进程加一个本地转换引擎对外只暴露一个 HTTP 预览入口前端拿 iframe 或新窗口指过去就行。它适合手上压着一堆历史附件、又不想逐个格式啃转换库的后端和运维如果你只需要预览纯 PDF用它属于杀鸡用牛刀。2. 从 tar.gz 到预览第一个文件解包、启动、拼对 URL 的三步拿到压缩包先别急着java -jar这个发行包是「脚本 外置配置 依赖库 主程序」的结构配置是放在 jar 外面的跑起来之后再改配置不用重新打包这一点对线上升级很关键。下面按顺序把三件事做完看清目录、选一种启动方式、把第一次预览请求的 URL 拼对。2.1 解包后先看什么发行包解压出来的目录大致是下面这个样子不同构建可能少一两个文件但骨架不会变tar -zxvf kkFileView-4.4.0-SNAPSHOT.tar.gz cd kkFileView-4.4.0-SNAPSHOT # 只看两层避免目录太深刷屏 find . -maxdepth 2 -type d | sort # 关键文件确认启动脚本、外置配置、主程序 ls -lh bin/ config/ *.jar 2/dev/nullbin/下是启停脚本和服务注册文件config/下是application.properties这个是全篇最该先读的文件端口、缓存目录、转换超时、水印全在里面。主程序 jar 里的代码基本不用动改行为优先改外置配置。file/目录是运行期生成的用来放上传的原始文件和转换出来的中间产物第一次启动前如果不存在服务一般会自己建但父目录的写权限要提前给足否则会在转换阶段才报错排查起来很绕。提示SNAPSHOT 版本意味着这是快照构建配置键名和行为可能跟正式版有出入。改配置前先grep -n cache\|office\|convert config/application.properties把实际存在的键列出来不要照抄网上的旧键名。2.2 三种启动方式与最低依赖最省事的是直接用 jar 跑适合本地验证生产上更推荐脚本或容器因为要固定工作目录和 JVM 参数。# 方式一直接用 java 起最快验证能不能跑 # -Dfile.encodingUTF-8 必加否则日志和部分文本预览会出乱码 java -Dfile.encodingUTF-8 -jar kkFileView-4.4.0-SNAPSHOT.jar # 方式二用自带脚本脚本里会带上工作目录和 JVM 参数 chmod x bin/*.sh bin/startup.sh # 方式三容器里跑前提是镜像里已经装了转换引擎和中文字体 docker run -d --name preview \ -p 8012:8012 \ -v /data/preview/file:/opt/kkFileView/file \ -v /data/preview/config:/opt/kkFileView/config \ kkfileview:4.4.0先说依赖这件事很多人卡在这一步。这个服务做 Office 转换靠的是本地安装的 LibreOffice或 OpenOffice无头进程不是纯 Java 库。所以机器上必须先装好转换引擎否则启动是成功的但一预览 docx 就报「转换失败」。判断方法很简单soffice --version或者libreoffice --version能输出版本号就说明引擎在。容器方案同理官方镜像一般自带自己打的镜像记得装。端口默认是 8012如果你的机器上还有别的服务占着改server.port即可。启动完成后不要只看进程在不在直接打健康检查接口确认容器内的 Spring 上下文真的起来了# 简单探活返回 200 说明 Web 层已就绪 curl -o /dev/null -s -w %{http_code}\n http://127.0.0.1:8012/ # 看启动日志里有没有转换引擎的探测结果 tail -n 100 logs/*.log 2/dev/null | grep -i office\|officehome\|convert日志里如果打印出了转换引擎的安装路径说明服务已经找到了soffice如果没有就要在配置里手工把office.home指过去Linux 下常见路径是/usr/lib/libreofficeWindows 下是C:/Program Files/LibreOffice路径分隔符按各自平台写。2.3 第一个预览请求URL 编码与 Base64 的双层处理接口层最容易被误解的一点onlinePreview的url参数不是明文地址而是「把目标文件地址做 Base64再把 Base64 结果做一次 URL 编码」。只做 Base64 不做 URL 编码是新手最常见的翻车点因为 Base64 字符集里的、/、在 query string 里会被转义或截断会被服务端当成空格解码出来就是一堆乱码路径表现是 404 或者「文件不存在」。// 前端构造预览地址 // 目标文件必须是预览服务自己能访问到的地址不能是用户浏览器本地路径 const fileUrl http://oss.internal.example.com/a/b/%E6%B5%8B%E8%AF%95.docx; // btoa 只能处理 Latin-1中文名必须先 encodeURI 再转 // 顺序错了就是经典的 InvalidCharacterError const base64 btoa(encodeURIComponent(fileUrl)); // 注意encodeURIComponent 必须加否则 / 会被 query 解析吃掉 const previewUrl http://127.0.0.1:8012/onlinePreview?url${encodeURIComponent(base64)}; window.open(previewUrl, _blank);这段代码有三个参数值得单独说明。第一是fileUrl它必须是服务端视角可达的地址也就是说你得在服务器上用curl能把这个文件拉下来。第二是encodeURIComponent(fileUrl)它解决的是中文和空格问题btoa遇到码点超过 255 的字符会直接抛异常先转成百分号编码就变成纯 ASCII 了。第三是最外层的encodeURIComponent(base64)它解决的是上面说的变空格问题少了这一步只要文件地址的 Base64 里恰好出现就会有一批文件预览失败、另一批正常这种「时好时坏」的现象最容易让人怀疑人生。后端生成也是一样的逻辑Java 里用Base64.getUrlEncoder()更稳因为 URL 安全的 Base64 用-和_替代了和/天然避开 query string 的坑// 后端拼预览地址用 URL 安全的 Base64省掉一次转义 String rawUrl http://oss.internal.example.com/a/b/test.docx; String token Base64.getUrlEncoder().withoutPadding() .encodeToString(encodeURIComponent(rawUrl).getBytes(StandardCharsets.UTF_8)); String preview http://127.0.0.1:8012/onlinePreview?url token;withoutPadding()去掉末尾的进一步减少转义麻烦getUrlEncoder()保证字符集落在A-Za-z0-9-_里整个 token 放 query 里是安全的。如果预览服务开启了server.servlet.context-path预览地址要相应带上这个前缀否则会 404这一点在对外网关闭、用网关统一转发时特别容易忘。3. 转换链路拆开看LibreOffice、字体、缓存三处最容易被忽略的配置预览服务本质上是一台「格式翻译机」输入五花八门输出统一成浏览器能直接渲染的东西。理解这条链路的每一跳配置才不会瞎改。Office 系走的是「LibreOffice 转 PDF → 前端 PDF 查看器渲染」文本类直接读取内容图片和音视频多数是直链或简单转码压缩包走列目录。链路里任何一跳出问题表现都是「转圈」或者空白页所以排错要先定位断在哪一跳。3.1 Office 转 PDF 这条主线Office 文档不会在浏览器里直接渲染中间必须经过一次格式转换。转换引擎以无头模式拉起一个进程把源文件另存为 PDF这个 PDF 落到缓存目录然后前端加载它。这个过程有两个后果一是转换是有耗时的几十页的 pptx 在低配机器上转十几秒很正常二是转换进程是外部进程不受 JVM 内存管理进程卡死不会抛 Java 异常只会让请求一直挂着。# 转换引擎的安装目录一般能自动探测探测不到再手工指定 office.home/usr/lib/libreoffice # 转换超时单位秒文件大、机器弱就往上调但别无限大 # 调太小的直接后果是大文件全部失败调太大则进程堆积 convert.timeout120 # 转换引擎的进程端口范围多端口可以并行多个转换实例 # 端口数决定了并发转换能力的上限不是越大越好要看 CPU 核数 office.plugin.server.ports2001,2002convert.timeout是最需要按实际情况调的一个值。默认值对普通文档够用但遇到带大量高清图、嵌入对象的 pptx 就顶不住。我的经验是拿业务里最大的那几个真实文件做基准测出最慢的转换耗时然后乘 2 作为超时同时把并发上限压住。因为并发转换数量超过 CPU 核数时转换时间会非线性增长超时阈值设得再大也没用只是把失败变成「很慢的失败」。另一个隐性约束是磁盘。转换出来的 PDF 是落在本地目录的跟原始文件一起占空间一个 20MB 的 pptx 转出来可能是 40MB 的 PDF。文件量大的系统如果没开清理磁盘撑爆只是时间问题而且爆的时候往往表现为「转换成功但预览空白」因为写文件失败了但异常被吞掉了。3.2 一份可对照的参数表改配置之前先把几个核心项对齐下面这张表列的是最常动的部分键名以你解压出来的application.properties为准配置项作用调整建议server.port服务监听端口冲突时改改了记得同步网关和前端file.dir上传文件与转换产物的根目录指向大容量数据盘别放系统盘cache.clean.enabled是否开启缓存清理定时任务生产必须开关着等于慢性自杀cache.clean.cron清理任务的 cron 表达式错开业务高峰一般放凌晨convert.timeout单次转换超时秒数按最大真实文件实测后乘 2watermark.txt预览页水印文案内部系统建议带上使用人标识spring.servlet.multipart.max-file-size单文件上传上限要大于业务里最大的附件表格里最该动手的是file.dir和清理两项。file.dir换了目录历史缓存不会自动迁过去等于把旧文件全部失效一次切换前要有心理准备。清理的 cron 如果写错任务会静默不执行日志里不会报错只能靠观察磁盘用量发现所以上线后第一周务必看一眼缓存目录的实际增长曲线。3.3 字体和水印中文字体缺失是最高频的翻车原因这是血泪经验里排名第一的坑英文文档转出来一切正常中文文档转出来全是方框或者乱码。原因跟编码没关系是转换引擎所在的操作系统没有装中文字体LibreOffice 找不到字形就用方框填充。容器镜像尤其容易踩因为精简镜像为了压体积通常只保留最基础的字体集。# Debian/Ubuntu 系装文泉驿和常用中文字体 apt-get update apt-get install -y fonts-wqy-zenhei fonts-wqy-microhei # CentOS/RHEL 系 yum install -y wqy-zenhei-fonts wqy-microhei-fonts # 手工放字体把 ttf/ttc 丢进目录 mkdir -p /usr/share/fonts/chinese cp /data/fonts/*.ttf /usr/share/fonts/chinese/ # 关键一步刷新字体缓存不执行这步等于没装 fc-cache -fv # 验证能列出中文字体名才算真正生效 fc-list :langzh | head -n 5fc-cache -fv这一步很多人会漏。字体文件被复制进了目录但 fontconfig 的缓存没更新LibreOffice 依然看不见新字体症状是「明明装了字体还是方框」。验证方式就是fc-list :langzh有输出说明系统层面认了如果这一步没问题但预览还是方框那问题就出在转换引擎自己的字体缓存上重启服务让引擎重新加载。还有一类容易被当成字体问题的现象预览页有水印但水印位置压在正文上、或者页眉页脚错位。这多半是 PDF 页面的坐标系处理跟字体无关改字体是白费功夫。区分方法很直接——把转出来的 PDF 从缓存目录里拿出来用本地的 PDF 阅读器打开看一眼PDF 本身正常那就是前端渲染层的事PDF 本身就歪那就是转换层的事别在错误的一层上反复调。4. 接进业务系统短时链接、前端封装与缓存回收本地能预览一个文件只是开始真正的工作量在「接进已有系统」。业务系统里的文件地址通常带鉴权要么是内网对象存储的私有桶要么需要登录态才能下载而预览服务是另一台机器、另一个进程它去拉文件时不带用户的 Cookie。直接把这串地址丢给预览服务结果就是 403 或者下载到一个登录页的 HTML然后转换报错说「文件格式不支持」。4.1 用短时签名链接打通鉴权主流做法有两类一类是让预览服务拿签名 URL存储层验证签名放行不需要用户态另一类是在业务侧开一个内部下载接口用一次性 token 换文件流。前者改造小后者控制力强。// 业务侧生成短时签名地址有效期 5 分钟 // 预览服务拿这个地址去拉文件不需要用户登录态 public String buildSignedUrl(String objectKey) { long expireAt System.currentTimeMillis() / 1000 300; // 签名串里必须带过期时间否则链接泄露就是永久可读 String toSign GET\n expireAt \n objectKey; String sign hmacSha256(toSign, SECRET); return String.format( http://oss.internal.example.com/%s?expire%dsign%s, objectKey, expireAt, sign); }有效期设多长是个权衡。太短用户点开预览时链接已经过期尤其是大文件转换本身要几十秒太长链接一旦泄露文件在有效期内可被任意拉取。五分钟到十分钟是常见区间。另外要注意签名串里必须包含路径和过期时间只签密钥不签内容等于没签攻击者换个路径就能读别的文件。如果业务侧不方便改造存储那就用反向代理做内网转发预览服务访问一个内网地址Nginx 在这层补上鉴权头。这种方案的好处是预览服务完全无感坏处是鉴权逻辑散落在代理配置里改起来比改代码还麻烦所以两个方案怎么选取决于你们有没有人维护那一堆 Nginx 配置。4.2 前端封装把编码逻辑收成一个函数前端不要每个页面都手写一遍 Base64 加转义封装成统一函数顺便把错误兜住。因为预览是在新窗口打开的如果 URL 拼错用户看到的就是一片空白前端拿不到任何反馈。// 统一封装所有预览入口都走这里 function buildPreviewUrl(fileUrl, baseUrl http://127.0.0.1:8012) { if (!fileUrl) throw new Error(fileUrl is required); // encodeURI → btoa → encodeURIComponent顺序不能颠倒 const encoded encodeURIComponent(btoa(encodeURIComponent(fileUrl))); return ${baseUrl}/onlinePreview?url${encoded}; } // 打开前先自己探一次拼错能立刻发现 async function openPreview(fileUrl) { const url buildPreviewUrl(fileUrl); try { await fetch(url, { method: HEAD, mode: no-cors }); } catch (e) { console.warn(preview probe failed, check url encoding, e); } window.open(url, _blank); }三层编码看着冗余其实每层解决一个问题内层encodeURIComponent处理中文和空格让btoa不抛错btoa做 Base64外层再转义让 token 能安全放进 query。少任何一层都会在某类文件上翻车而且因为大多数文件恰好正常问题会被长期掩盖。openPreview里的探活请求用no-cors模式拿不到状态码但至少能确认请求发出去了属于聊胜于无的兜底。真正可靠的做法是在业务侧加一个「预览失败」的前端埋点把失败的文件地址记下来跑一周就能看出到底是哪类文件在翻车。4.3 缓存回收与容量规划转换产物是缓存不是资产必须能被清掉。清理策略一般按「距今超过 N 天」执行N 取多少取决于文件被回看的概率分布合同、报表这类会被反复打开N 可以设 7 天聊天附件这类基本只看一次设 1 天都嫌长。容量估算可以用一个粗略公式日新增附件数 × 平均转换产物大小 × 保留天数 × 2留一倍余量。转换产物普遍是原始文件的 1 到 2 倍ppt 类因为图片被重编码有时能到 3 倍。按这个公式算出来的数字直接决定数据盘该开多大比拍脑袋靠谱。注意清理任务删文件时如果同时有转换请求在写会出现「文件被删但请求还在读」的情况。生产环境建议清理时间窗放在业务低峰或者让清理逻辑跳过最近一小时内被访问过的文件具体实现取决于你们的存储布局。5. 排查与避坑五个翻车现场的现象、原因和解决这一章按「现象 → 原因 → 解决」写全部是在实际部署里真遇到过的问题。遇到同类现象时先对照这里能省掉大量从日志翻到源码的时间。5.1 中文文档预览全是方框现象英文 docx 转出来完全正常中文 docx 每一页都是小方块页数、版式都对就是字认不出来。原因转换引擎所在的操作系统缺少中文字体LibreOffice 找不到可用字形时用方框占位。这里最容易误判成编码问题然后去改file.encoding改完发现一点用没有。解决按 3.3 节装字体并执行fc-cache -fv然后用fc-list :langzh验证。容器环境要把字体安装写进 Dockerfile不能只在运行中的容器里手动装否则下次重启又回到原样。装完重启服务让转换引擎重新加载字体表。5.2 部分文件 404部分正常现象同一批文件里有些能预览有些打开是白页或者 404重试也是一样的结果跟文件大小、格式都没关系。原因Base64 结果里包含或/没有做 URL 编码就拼进 query string服务端解析时被当成空格解码出来的路径自然不存在。这个坑的隐蔽之处在于是否踩中完全取决于文件地址的字符构成所以表现为「随机失败」。解决按 2.3 节的顺序做三层编码或者后端直接改用Base64.getUrlEncoder()从源头上避免和/。排查时最直接的办法是把失败文件拼出来的 URL 复制到浏览器里看如果地址栏里出现了空格基本就能确诊。5.3 大文件转换超时、进程越积越多现象小文件秒开几十页的 pptx 或者上百页的 pdf 一直转圈服务重启后暂时正常过一阵子top里一堆转换进程CPU 打满。原因转换是外部进程超时阈值太小会让大文件必然失败并发数没有限制则会让进程数量随请求线性增长每个转换进程都吃 CPU 和内存互相拖慢之后连小文件也开始超时。解决先把convert.timeout调到实测最大文件耗时乘以 2然后在入口层做并发限制超过阈值的请求直接返回「排队中」而不是硬扛。排队可以通过消息队列做也可以用一个简单的信号量。同时确认转换引擎的端口数量和 CPU 核数匹配端口开得比核数多只会让进程互相抢 CPU。5.4 磁盘写满之后预览全部空白现象服务进程正常、接口返回 200、日志里没有明显报错但所有文件预览都是空白页重启服务短暂恢复很快又空白。原因缓存目录被写满转换产物写不进去。因为写文件失败抛出的异常在转换链路里被捕获后只记了 debug 级别日志默认日志级别下看不到所以从表面看像是「转换成功但没内容」。解决挂个磁盘监控告警阈值设在 80%。同时把cache.clean.enabled打开并核对 cron 表达式用du -sh定期看缓存目录的增长速度。已经写满的话先清缓存再重启不要只重启不清缓存因为重启后第一次转换还是会失败。5.5 页面是 HTTPS预览框一片空白现象业务系统上了 HTTPS预览窗口打开后空白浏览器控制台里有一串「mixed content」相关的报错。原因预览服务还是 HTTP浏览器认为在 HTTPS 页面里加载 HTTP 资源不安全直接把请求拦掉了。这不是服务端的问题服务端日志里可能一条相关记录都没有。解决给预览服务配一个同域的反向代理让预览地址和业务页面同协议同域。这样既解决了混合内容问题也顺便绕开了跨域。反代配置里要注意大文件不走缓冲、超时时间要够长location /preview/ { proxy_pass http://127.0.0.1:8012/; # 大文件转换耗时长这几个超时值要给足 proxy_read_timeout 300s; proxy_send_timeout 300s; # 关闭缓冲避免大文件先落 Nginx 本地磁盘 proxy_buffering off; proxy_set_header Host $host; }proxy_buffering off对大文件很关键开着的话 Nginx 会先把整个响应落到自己的临时目录遇到磁盘小或者并发高的时候容易雪崩。proxy_read_timeout要大于convert.timeout否则会出现「转换还在跑网关先断了」用户那边看到的是超时实际转换白做了一遍。6. 上线前的自检与调优把这段脚本固化成流程部署完能打开不算完真正省事的是把上线前的检查固化下来。我一般会把下面这段脚本存成precheck.sh每次发版前跑一遍五分钟能挡掉八成低级问题。脚本不复杂胜在每次都执行不靠记性。#!/usr/bin/env bash # 预览服务上线前自检失败即退出 set -euo pipefail HOST${1:-http://127.0.0.1:8012} echo [1/6] 服务存活 curl -sf -o /dev/null $HOST/ || { echo web 层不可用; exit 1; } echo [2/6] 转换引擎存在 command -v soffice /dev/null || command -v libreoffice /dev/null \ || { echo 未找到转换引擎; exit 1; } echo [3/6] 中文字体可用 fc-list :langzh | grep -q . || { echo 缺少中文字体; exit 1; } echo [4/6] 缓存目录可写且余量充足 DIR${FILE_DIR:-/opt/kkFileView/file} [ -w $DIR ] || { echo 缓存目录不可写: $DIR; exit 1; } df -h $DIR | awk NR2 {gsub(%,,$5); if ($50 80) exit 1} echo [5/6] 预览接口连通性放一个已知文件地址 FILENAME${SAMPLE_FILE:-test.docx} curl -sf -o /dev/null --get --data-urlencode url$FILENAME \ $HOST/onlinePreview || echo 抽样预览未通过需人工确认 echo [6/6] 磁盘水位与进程数 df -h $DIR | tail -n 1 ps -ef | grep -c [s]office || true脚本里第 4 步和第 6 步是串起来的磁盘水位超过 80% 直接判定失败同时把当前转换进程数打出来。如果进程数在空闲时就已经是个很大的数字说明有僵尸转换进程没退这种状态上线后必然雪崩。第 5 步用--data-urlencode而不是手工拼字符串就是为了绕开前面反复提到的转义问题让自检脚本本身不会因为编码再翻一次车。调优这块优先级从高到低排是三件事先把convert.timeout按真实大文件校准再把并发上限压到 CPU 核数以内最后才是考虑加机器。顺序反了的话加多少机器都会被无节制的并发吃干净。另外别忽略server.tomcat.max-threads这类容器线程参数预览接口的响应时间比其他接口长一个数量级线程池小了会在业务高峰期把整个服务的线程占满连健康检查都探不进来所以生产上最好让预览服务独立部署不跟业务服务混部。有一个习惯我一直保持着每接一种新格式进来先把该类真实文件丢进自检清单跑一遍转换把耗时和产物大小记到一张表里。表里攒够几十行之后你对这台服务的容量边界就有直觉了——什么样的文件会慢、慢到什么程度、需要预留多少磁盘。从那以后我每次上线新实例都强制把这份自检脚本走一遍包括字体那一步因为方框问题在容器环境里出现的概率远比想象中高而且它是唯一一个「看起来服务完全正常、实际输出全废」的故障类型。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?