外观
服务端 SDK 参考
服务端 SDK(cn.atkon:atkonoffice-sdk)是一个 Spring Boot Starter,加依赖后自动装配。接入只需做三件事:告诉它字节在哪(解析器)、挂 host-page 路由(AtkonofficeCtrl)、挂 save 路由(AtkonofficeFileSaver)。其余(拉起链签发、下载端点、下载凭据、WebSocket 通道、授权校验)由 SDK 自动挂载。
安装与最小示例见开始。本页是完整参考。
集成方实现的解析器
AtkonofficeDocumentResolver
接入数据面下载的唯一触点:实现这一个函数,告诉 SDK「某文档的字节在哪取」。SDK 拥有下载端点、凭据校验、流式吐字节与错误码,无需手写下载 controller / multipart / token。
java
@FunctionalInterface
public interface AtkonofficeDocumentResolver {
AtkonofficeDocument resolve(String docId);
}- 入参
docId:进入本方法前 SDK 已校验下载凭据,非空。 - 返回
AtkonofficeDocument地址描述符;返回null视同「未找到」(→ 404)。 - 契约:
resolve须幂等、无副作用——同一 docId 在一次请求内可能被调用多次(判形态拼 URL 一次、取字节一次),流 / 字节形态每次调用都须能重新取得可读资源。
| 情况 | 做法 | 客户端看到 |
|---|---|---|
| 文档不存在 | 返回 null 或抛 AtkonofficeDocumentNotFoundException | 404 |
| 无权访问 | 抛 AtkonofficeAccessDeniedException | 403 |
按 docId 的授权判定可直接折进 resolver(读当前请求的业务会话头再决定放行)。
注册为普通 Bean(@Component + 构造器注入即可,它属于集成方代码),SDK 就用它接管下载:
java
@Component
public class MyDocumentResolver implements AtkonofficeDocumentResolver {
@Override
public AtkonofficeDocument resolve(String docId) {
if (docId.startsWith("s3:")) {
// 对象存储:返回预签名 URL,客户端直连,应用服务器不在字节路径上
return AtkonofficeDocument.ofUrl(myObjectStore.presign(docId.substring(3)));
}
byte[] bytes = myDatabase.loadBytes(docId); // 或 DB / 文件系统
if (bytes == null) return null; // → 404
return AtkonofficeDocument.ofBytes(bytes).withFileName(docId);
}
}不想写代码? 文档以「一 docId 一文件」放本地目录时,只配
atkonoffice.storage.local.root即可零代码启用内置本地盘解析器。自定义 Bean 一旦存在即覆盖内置默认。
AtkonofficeDocument
resolve 的返回值——地址描述符(说「字节在哪」,而非把字节搬出来)。四种形态由静态工厂构造:
| 工厂方法 | 形态 | 取用方式 |
|---|---|---|
AtkonofficeDocument.ofUrl(String url) | 远端下载 URL(典型对象存储预签名) | 客户端直连,应用服务器不在字节路径上 |
AtkonofficeDocument.ofLocalFile(Path file) | 本地磁盘文件 | 经 SDK 下载端点流式吐 |
AtkonofficeDocument.ofBytes(byte[] bytes) | 内存 / DB 字节 | 经 SDK 下载端点吐 |
AtkonofficeDocument.ofStream(InputStream s, long length) | 输入流(带长度,length < 0 未知) | 经 SDK 下载端点吐 |
可链式附加元信息(供下载端点设响应头):withFileName("报告.docx") → Content-Disposition;withContentType(...) → Content-Type。
AtkonofficeCtrl — 生成宿主页
每请求构造一个,链式配置后调 getHostPage()(单文档)或 getComparePage()(Word 比对)返回整页 HTML。
构造与打开
| 方法 | 说明 |
|---|---|
AtkonofficeCtrl(HttpServletRequest request) | 构造。须在装配了 SDK 的 Web 上下文内使用。 |
webOpen(String location, AtkonofficeOpenMode mode, String user) | 推荐。传文档逻辑定位符(docId / 远端 URL),SDK 内部现签成取用地址。链式。 |
open(String docUrl, AtkonofficeOpenMode mode, String user) | 低阶入口:docUrl 是已拼好的下载 URL,原样使用。返回 void(不链式)。 |
webCreate(DocFamily family, String user) | 新建空白文档。绕过解析器,未配解析器也能新建,仅保存需存储。链式。 |
setCompareDocument(String location) | 设 Word 比对目标(右栏 B),配合 getComparePage()。链式。 |
webOpen/webCreate/setCompareDocument上下文缺失时快速失败抛IllegalStateException,绝不产出半成品页。
配置
| 方法 | 说明 |
|---|---|
setSaveFilePage(String saveFilePage) | 保存回写地址。传相对路径(以 / 起始,可带 query,且不得含未编码的 ://——query 里要放完整地址请百分号编码)时 SDK 用本次打开的有效地址补全成绝对 URL,与该文档下载地址同源(子路径 / 节点前缀等部署形态下前缀不丢);也可传绝对 URL,原样使用。相对路径不合法时在渲染前即抛异常、不产出半成品页面。未设则该文档不保存。 |
setWriter(WordDocumentWriter writer) | 打开前给 Word 数据区域填充业务数据(见下)。 |
setOcxClsid(String ocxClsid) | 覆盖编辑组件的注册标识(CLSID)(一般无需,用默认冻结值)。 |
外观配置
打开时应用;未调用则保持默认、零影响。均链式返回 this。
| 方法 | 作用 |
|---|---|
setZoomPercent(int) | 缩放百分比 |
setViewType(int) | 视图类型序号 |
setDocumentMap(boolean) | 是否显示导航结构图窗格 |
setOfficeToolbars(boolean) | 是否显示 Office 原生功能区(false = 隐藏,收敛为简洁 viewer) |
setCaption(String) | 控件标题文本 |
setAllowCopy(boolean) | 是否允许把内容复制出去 |
setDisableCopyOnly(boolean) | 是否禁用仅复制限制 |
setSaveAsReadOnly(boolean) | 另存为是否产出只读结果 |
setOfficeVendor(String) | Office 引擎厂商选择提示 |
setClientCertName(String) | 双向认证传输的客户端 TLS 证书名 |
setWindowTitle(String) | 客户端窗口标题栏文本 |
setWindowBorder(AtkonofficeBorderStyle) | 客户端窗口边框样式 |
setWindowTheme(AtkonofficeTheme) | 客户端窗口外观主题 |
生命周期回调
注册业务函数名,事件到达浏览器业务页时由浏览器端 SDK 调同名函数(与前端 on(...) 二选一即可):
| 方法 | 触发时机 |
|---|---|
setAfterDocumentOpened(String functionName) | 文档打开后 |
setBeforeDocumentSaved(String functionName) | 即将保存前(通知式,不能取消) |
setAfterDocumentSaved(String functionName) | 保存完成后 |
出页
| 方法 | 说明 |
|---|---|
String getHostPage() | 返回单文档宿主页整页 HTML。须先调过 open/webOpen/webCreate,否则抛 IllegalStateException。 |
String getComparePage() | 返回 Word 两栏比对页整页 HTML。须先调过 webOpen(栏 A)与 setCompareDocument(栏 B)。 |
AtkonofficeFileSaver — 接收保存
在 save 路由里每请求构造一个,解析上传的 multipart 体并封装回写响应。一次保存可承载一组产物(每个带文件名的 part 即一个产物,part 名 = role:source 原件 + 可选派生件如 pdf)——用 getFiles() / getFile(role) 按 role 取;只存原件时直接用主产物便捷口(getFileBytes() 等,语义 = 主产物 source)。
| 方法 | 说明 |
|---|---|
AtkonofficeFileSaver(HttpServletRequest req, HttpServletResponse resp) | 构造并即时解析上传体。 |
Map<String, Artifact> getFiles() | 本次上传的全部产物(role → 产物,保序、只读)。遍历它逐 role 落盘。 |
Artifact getFile(String role) | 按 role 点名取产物(无该 role 返回 null)。 |
byte[] getFileBytes() | 主产物 source 的原始字节(便捷口)。 |
InputStream getFileStream() | 主产物 source 字节上的新流(便捷口)。 |
void saveToFile(String filePath) | 便捷:把主产物 source 直接写到本地路径。 |
String getFormField(String name) | 取一个附带表单字段(缺失返回 null)。 |
String getFileName() | 主产物 source 的 basename(如 report.docx)。 |
String getFileExtName() | 主产物 source 不含前导点的扩展名(如 docx)。 |
long getFileSize() | 主产物 source 字节大小。 |
void setResult(String result) | 写响应体(纯文本),客户端读回为保存结果。 |
void close() | 释放缓冲;此后不可再用。 |
其中 Artifact 是一个 (role, 文件名, 字节) 三元组:getRole() / getBytes() / getStream() / getFileName() / getExtName() / getSize()(取字节口同样过授权闸)。
表单字段:前端
session.save({ 自定义字段 })传的非formats/savePath键作为表单字段到达这里,用getFormField(name)取(产物走getFiles()、不混入表单字段)。
WordDocumentWriter — 数据区域填值
在带命名数据区域的 Word 模板上,打开前把业务数据灌进对应区域(用法见 Word · 数据区域)。数据区域为锁定填充位——填入的值终端用户不能手动改动或删除。
| 方法 | 说明 |
|---|---|
DataRegion openDataRegion(String name) | 打开一个数据区域,返回可 setValue 的句柄。 |
DataRegion.setValue(Object value) | 设值并缓冲,链式返回 WordDocumentWriter 以接着开下一个区域。 |
- 区域名为空 → 该条 no-op;值
null→ 写空串; - 承载的是业务数据值(非文档字节),不改变「文档字节不出域」。
配置项
全部写在 application.yml(或 .properties),加依赖后即被绑定,只填需覆盖的项。
atkonoffice.launch.*
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
backend-origin | String | (空,单实例常见部署可不配) | 业务后端对外地址的源(无结尾路径,可带路径前缀),如 https://api.example.com,用作 RPC / 数据面 / 下载 / 保存的基址,实时通道(WebSocket)地址也由它派生。单实例常见部署无需配置:浏览器端 SDK 已配的 backendBase 会随打开请求上送,服务端据此按请求推导。显式配置时,该值必须同时对浏览器和终端用户机器可达——业务页自己要连的实时通道地址就派生自它,配一个只有客户端可达的地址会让业务页收不到任何事件。若部署确实存在「浏览器可达地址 ≠ 客户端可达地址」(如经 Web VPN / 代理),须在配置本项之外一并显式配置 control-plane.ws-url 指向浏览器可达的地址。多实例部署必配(每实例一个指向自己的值),见多实例部署。 |
app-id | String | demo-app | 注册应用标识([A-Za-z0-9._-]{1,64})。 |
client-download-url | String | (空) | 客户端安装包下载页地址,供未安装时的引导使用。 |
ws-ticket-ttl-sec | int | 120 | 一次性 WebSocket 票据存活秒数。 |
地址来源优先级:显式配置的
backend-origin> 浏览器端 SDK 随打开请求上送的backendBase。两者皆无时不再于启动期拒绝启动,而是在签发拉起链的那一刻带配置指引报错。
atkonoffice.storage.local.*
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
root | String | (无) | 内置本地盘解析器的文档根目录(一 docId 一文件)。配了它、且没有自定义解析器 Bean 时零代码启用下载。 |
atkonoffice.download.*
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
secret | String | (空) | 签发一次性下载 token 的共享密钥。多实例 / 负载均衡须显式配同一值;留空则降级为启动期内存随机密钥(单实例有效,进程重启后在途 token 作废)。⚠️ 它只覆盖文档下载凭据这一面——多实例部署整体还须满足多实例部署一节的地址与路由要求,仅配本项不构成多实例支持。 |
ttl-sec | int | 600 | 下载 token 存活秒数。 |
atkonoffice.control-plane.*
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
session-ttl-sec | int | 3600 | 会话存活秒数。 |
max-drift-sec | int | 900 | 接受的时钟漂移窗口秒数。该窗口用于容忍自然时钟漂移(未配 NTP 的机器长期运行会偏出几分钟、虚拟机挂起迁移等),不是用来容忍把日期 / 时区设错——小时级以上的偏差仍会报错,那属于配置错误、报出来才对。⚠️ 不建议调大:放宽窗口会等比放宽连接凭据可被重放的时长,部署要求里的两条安全前置正是它的承重条件。 |
ws-url | String | (空) | 对外通告的 WebSocket 端点。留空则从有效地址(显式 backend-origin 或浏览器上送的 backendBase)自动推导(http→ws / https→wss,同 host:port);分离 WS 面 / 独立 ingress / 带 servlet context-path 时须显式配。⚠️ 多实例部署下不得配全局同一值——要么不配(由各实例的 backend-origin 自动派生),要么每实例配一把指向自己的,见多实例部署。 |
atkonoffice.ws-broker.*
WebSocket 推送面的调优项。默认值适用于直连、或已按部署要求正确转发 WS 控制帧的部署;若前置反向代理 / 网关不转发 ping-pong 控制帧,idle-timeout-sec 会让连接被周期性判死——此时应先修反向代理的转发,而不是调大该值(调大只是把一轮误杀的周期拉长):
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
ping-interval-sec | int | 30 | Ping 周期。 |
idle-timeout-sec | int | 90 | 空闲判死上界。 |
shell-gone-grace-sec | int | 120 | 编辑器异常消失(进程崩溃 / 被强杀,或仅编辑器那条连接被单独掐断)后,服务端等多久才告知业务页「编辑器已不在」(shellClosed 事件,reason: "shell_gone")。实际告知时刻 = 该值 + 至多一个 ping-interval-sec;若连接是被静默掐断而非关闭,还要再加一个 idle-timeout-sec(那条连接须先被判死)——它挂在心跳节拍上,不是精确定时器。⚠️ 不建议调小:默认值刻意大于编辑器自身的快速重连视野,调小会把「还在自动重连的编辑器」误判成「已消失」、把业务页误复位。 |
per-sid-msg-rate | int | 50 | 每会话入站消息速率上限(msg/s)。 |
per-ip-conn-rate | int | 5 | 每 IP 建连速率上限(conn/s)。 |
max-frame-bytes | int | 65536 | 单帧字节上限。 |
atkonoffice.license.*
见产品授权。
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
path | String | atkonoffice-license.lic | 激活后固化 license 文件的路径;相对路径解析到运行 jar 所在目录,可改绝对路径。 |
preset-code | String | (空) | 管理员预置授权码:填入后启动即固化,免走激活端点(适合自动化部署)。 |
部署要求
业务后端之前有反向代理 / 网关 / 负载均衡时(生产部署几乎都有),以下要求属于部署前置。文档下载与保存走普通 HTTP、只需常规反代;业务页的事件回推(保存完成通知等实时事件)走 WebSocket——WS 面有三条硬性转发要求与两条安全前置。以下配方以 nginx 为例,要求本身对任何反代 / 网关成立。
WebSocket 面的转发要求
三条缺一即坏,且坏法各不相同——只列配置项不足以自查,故连同「缺了会怎样」一起给出:
| # | 要求 | 缺了会怎样 |
|---|---|---|
| 1 | 透传升级头:WS 端点须以 HTTP/1.1 转发 Upgrade / Connection 头 | 文档能打开、能编辑、能保存落盘,但业务页永远收不到任何事件回推。最容易被误判成「前端事件没接对」,真因在反向代理 |
| 2 | 读写超时须覆盖整段编辑会话(nginx 默认 proxy_read_timeout 仅 60 秒) | 约每分钟失联一轮、随后自愈。最容易被当成「网络不稳定」长期带病运行 |
| 3 | 转发 WS 的 ping / pong 控制帧(多数反代默认透传,个别安全设备 / 网关会拦下不转发) | 服务端按 idle-timeout-sec(默认 90 秒)周期性判死并断开连接。⚠️ 此时应修转发,不要调大 idle-timeout-sec——那只是把一轮误杀的周期拉长 |
可直接抄的 nginx 形状(http 块一份 map + WS 端点单列一条 location):
nginx
# http 块:把 Upgrade 请求映射为升级连接
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# server 块:WS 端点单列一条,放行升级并放长超时
location /atkonoffice/ws {
proxy_pass http://your-backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $http_host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}(路径按实际部署前缀调整;超时取值覆盖预期的单次编辑会话时长即可。)
SDK 自动挂载的端点全部在
/atkonoffice/一个前缀下,所以完整的反代配置只有两条 location:上面这条 WS 的,加一条覆盖其余/atkonoffice/请求的普通转发。可直接抄的完整形状见开始 · 反向代理转发。
自查入口:「文档能打开也能保存,但业务页收不到任何事件回推 / 保存后页面没反应」是第 1 条缺失的典型症状,完整排查顺序见常见问题。
安全前置(生产环境必须满足)
这两条不是「配错会不好用」,而是做不到就不该上生产:
- WS 端点的访问日志不得记录查询串。编辑器接入实时通道的一次性凭据经 URL 查询串出示,而反向代理默认的访问日志格式(如 nginx 的
$request)会把查询串原样落盘。给 WS location 单独指定一个不含查询串的log_format,或在该 location 内access_log off。 - 生产必须使用 wss(TLS)。明文 ws 链路上,这条凭据等于向整条网络路径广播。
另一条与 TLS 相关的提示:客户端对出站 HTTPS 默认执行证书吊销检查。使用内部 CA / 自签证书时,证书应携带可达的 OCSP / CRL 分发点,或在服务端开启 ssl_stapling(一处配置对所有客户端生效);吊销端点不可达 / 缺失时,部分环境会出现握手报错或握手明显变慢——排查这类现象时以此为第一怀疑点。
多实例部署
支持多实例负载分担 / 横向扩容:每个实例把指向自己的地址签发给它受理的编辑会话,此后该会话的每一步都回到受理它的那个实例——不依赖负载均衡的会话保持,无新增配置项。
集成方要做的全部:
- 每实例显式配置一个指向自己的
atkonoffice.launch.backend-origin,值 = 对外域名 + 该实例专属的路径前缀(如https://doc.example.com/api/n1)。该地址须同时对浏览器和终端用户机器可达、且负载均衡能把它映射回该实例。⚠️ 必须采用「同一域名 + 每实例一个路径前缀」的形态;给每个实例单独一个域名不受支持——按实例定向会静默失效、退回轮询(只剩下方那条自查告警)。 - 负载均衡要两类规则,不是一类:
- 带实例前缀的路径 → 精确转发到对应实例(其中 WS 端点因须放行 Upgrade 单列一条,其余一条);
- 无前缀的
POST /atkonoffice/rpc/launch→ 仍须轮询全部健康实例。它是「哪个实例受理本次会话」的分流点——把它也钉到一台,等于单实例 + N 台热备。集成方自己的业务端点照常走无前缀腿。
- 不得配置全局统一的
atkonoffice.control-plane.ws-url:要么不配(由各实例的backend-origin自动派生),要么每实例配一把指向自己的。全局同一值会让所有实例通告同一个实时通道地址,按实例定向当场不成立(首次打开即失败——属响亮失败,不难发现)。
承诺边界(如实):
| ✅ 覆盖 | 多实例负载分担 / 横向扩容 |
| ❌ 不覆盖 | 节点重启 / 摘除 / 滚动更新不中断在编会话——该节点上正在编辑的会话会中断,终端用户需关闭编辑窗口后重新打开(与单实例重启后端的行为一致)。请把节点变更安排在维护窗口内 |
| 成本 | 加 / 减节点是配置事件:每加一台须配置该实例的完整对外地址(含域名与前缀)+ 增补负载均衡规则,不是加副本自动生效。换域名 / 改部署前缀时须同步更新全部实例配置 |
两格最容易配错(症状形状相反):
- 实例地址配成集群内部名(如
http://backend-1:8080):不会启动失败,但浏览器与终端用户机器都够不到它——表现为点「编辑」后编辑器窗口根本起不来(还可能弹出本不该出现的系统确认框),不是「打开后取不到文档」。 - 只配了带前缀的精确路由、漏了无前缀
launch的轮询:一切正常工作,但永远只有一台实例受理会话、其余实例是热备——负载没有被分担。
部署自查:浏览器端 SDK 检测到「本会话的续订请求被另一个实例受理」时,会经 on('error') 派发错误码 ws_ticket_node_mismatch(category: 'config')。它是「带前缀的精确路由没配对」这一格唯一的可观测信号——该格的典型表现是文档照常编辑、照常保存,只有业务页再也收不到任何回推、且没有任何报错。联调多实例部署时建议监听它。
为什么不用负载均衡的会话保持替代:常见 sticky 方案是 cookie 型,而一条编辑会话里除浏览器外还有终端用户机器上的客户端进程参与,后者不携带 cookie——粘住的只有浏览器那半。源 IP 亲和同样不可靠:经 Web VPN / 代理时浏览器与客户端出口不同;企业统一出口 NAT 时全员共用一个源 IP,负载全压一台。