简介《Domino开发指南精华》是一部聚焦Lotus Domino平台Java集成开发的PDF电子书面向企业级应用开发者以及对Notes协作生态感兴趣的Java工程师。书中系统讲解如何利用lotus.domino包完成数据库操作、文档管理、视图控制与代理自动化并延伸至邮件发送管理、富文本处理、网络请求头信息获取等实用场景大量可直接应用的代码示例让读者在构建高效协作系统时少走弯路。资源包仅含1个PDF文件大小3.82MB轻量易携适合通勤或碎片时间研读。目前已有1936人参与学习内容覆盖从入门到高级的完整路径既照顾初次接触Domino的新人也为有经验的开发者提供了企业环境下的落地思路与优化建议。1. Domino 开发指南精华先放下“数据库思维”再谈代码第一次打开 Domino Designer 的人多数会盯着导航器里的 NSF 文件愣几秒这个平台没有独立数据库服务没有建表脚本表单就是表结构文档就是数据记录业务代码散在设计元素里连“外键”都没有。如果你带着 MySQL 那套习惯进来第一周基本都在折腾“为什么我的视图不显示数据”这类问题。我把这份Domino 开发指南精华拆成三个层面NSF 与设计元素的建模方式、Formula/LotusScript/Java 的语言选型边界、代理权限与签名这套运行机制。想清楚这三层再读任何官方手册都不慌。这份指南适合一类读者要维护老 Notes 应用的开发或者正打算把 Domino 数据接进新前端体系的工程师。它不是百科是快速确定“什么场景用什么方案”的落地路径。2. 从 NSF 和设计元素搭骨架先认清表单、视图、代理、代码库四块地基2.1 表单、视图、代理、代码库先认清四块地基Domino 的“数据库”是一个 NSF 文件业务代码和配置都装在里面没有单独的 .sql 文件也没有迁移脚本。新建一个应用时你打开 Designer 会看到设计元素列表表单、视图、代理、脚本库、页面、XPage 等。对日常业务系统来说我一般只盯住四个东西表单定义的数据结构、视图提供的查询入口、代理承担的批处理逻辑、脚本库共享的函数和常量。表单不要照抄纸面单据。Domino 表单既是编辑界面又是字段容器一个字段的存储类型在表单上定义文本、数字、时间日期、富文本、姓名、作者/读者字段。我的经验是能用普通文本字段就别用富文本富文本在 Web 场景下坑最多金额和数量用数字字段校验交给输入转换公式日期字段统一存日期型避免前端传来字符串后再转换。视图容易让人误解。它看起来像表格本质是 NSF 内的索引集合。视图由选择公式决定哪些文档进入索引列决定显示哪些字段列排序决定索引的排序键。Domino 的视图索引不是实时查出来的服务器在需要时才刷新你写的公式稍有偏心索引性能和正确性都会出问题。2.2 最小业务模型把“客户-订单”落到 NSF我做 Domino 数据建模的常用做法是“先表单后视图再代理”。假设要做客户和订单管理建两个表单Customer 和 Order。Customer 有 CustomerName、Region、OwnerName 三个字段Order 有 OrderNumber、CustomerID、OrderDate、Amount、Status 五个字段。Order 和 Customer 之间没有外键约束靠 CustomerID 这个普通文本字段关联查订单时在视图里按 CustomerID 分类运行时保证一致性靠代理去校验。对新手来说这个“没有外键”的设定反直觉但 Domino 的设计哲学就是轻约束、重视图。建好表单后写视图最基础的是“订单状态视图”。视图选择公式如下这是最常用的入门公式SELECT Form Order Status ! Cancelled这段公式的意思很直白只要 Form 字段等于 Order、Status 字段不等于 Cancelled 的文档都进这个视图。两点需要解释Form 是系统保留字段保存文档时会自动写入当前表单名所以它既是业务字段又是筛选条件公式里的和!是 Domino 公式语言的逻辑运算和不等判断。注意这里返回的是文档集合不是 SQL 的行集。公式里不要写Today、Now这类随时间变化的函数否则视图索引缓存会失效数据一多视图就慢得没法看。我还会给视图加一个分类列列值取CustomerID这样前端的 Notes 客户端里能按客户折叠查看订单对老用户很友好。但分类列属于索引的一部分不要在这里做字符串拼接比如CustomerID - Status。拼接结果会让索引变大而且容易让同分类内容排不到一起。2.3 Reader/Author 字段文档级权限的黑匣子ACL 管的是整个库的权限级别而文档级权限靠 Reader 字段和 Author 字段控制。这是 Domino 权限体系里最像黑匣子的部分。我在给订单表单加权限限制时会在表单上拖一个 Reader 字段填入$Readers值来源用公式OwnerName : OrdersManagementAdmins这个字段的含义是只有 OwnerName 里列出的用户以及 OrdersManagementAdmins 组才能看到这个文档。如果用户不在任何 Reader 字段里即使 ACL 给他编辑者权限他也看不到该文档。Author 字段则更进一步它控制用户能否修改文档常见于多人协作场景。写 REST API 或数据集成时最容易在这里踩坑API 调用方持有的身份不在 Reader 字段里代码里按视图能查到文档但用这个身份去读单条文档时会返回空结果。很多集成问题排查到最后都是权限问题而不是查询条件写错。我的建议是任何外层接入先确认接入账号在哪些库有 ACL 访问权限再看文档的 Reader/Author 字段是否包含该账号最后才查代码逻辑。3. 语言选型是精华中的精华Formula、LotusScript、Java 与 XPages 的边界3.1 视图选择与列公式Formula 最舒服的领域Domino 的 Formula 语言仍然大量存在。它擅长的是三件事视图选择、列值计算、表单里的默认值和简单校验。新手容易把 Formula 当成万能胶用它写复杂循环和文件操作结果卡在数组处理上半天。我的判断标准是凡是一次性取值、单条逻辑、按条件返回结果用 Formula凡是循环多条文档、访问外部系统、需要异常处理就换 LotusScript 或 Java。列公式的使用频率最高。比如“订单是否逾期”这种展示逻辑我会在视图列里这样写listDate : Date(OrderDate); If(Today - listDate 30; 已逾期; 正常)这段公式先把 OrderDate 字段里的日期值取出来用Date去掉时间部分再和Today做差。如果差额大于 30 天返回“已逾期”否则返回“正常”。注意公式里的分号做参数分隔逗号不是分隔符这是 LotusScript 转过来的人最容易写错的地方。问题在于Today。它会在每次视图索引刷新时变化Domino 看到可变函数就放弃稳定的索引缓存视图打开速度会随着数据量上升明显恶化。真正生产环境我不会把这个公式留在列里而是写一个定时代理每天把“是否逾期”算好写入字段列里直接取字段值。这里的核心思路是视图列里放纯字段值除非是需要实时交互的临时视图否则不要在公式里引入当前时间。3.2 LotusScript 批处理代理注意集合遍历和保存参数LotusScript 是老 Notes 项目中最常见的批处理语言因为它在 NSF 对象模型上非常顺手写一两百行的批量更新代理成本很低。最常见的是定时更新订单状态。下面我给一个能直接改到你自己库里跑的模板Sub Initialize Dim session As New NotesSession Dim db As NotesDatabase Dim view As NotesView Dim doc As NotesDocument Dim nextDoc As NotesDocument Set db session.CurrentDatabase Set view db.GetView(OrdersByStatus) Set doc view.GetFirstDocument() While Not doc Is Nothing 先拿到下一条避免修改当前文档后游标失效 Set nextDoc view.GetNextDocument(doc) If doc.HasItem(Status) Then If doc.Status(0) Open Then doc.Status Closed Call doc.Save(True, False) End If End If Set doc nextDoc Wend End Sub这段代理的逻辑很简单从 OrdersByStatus 视图的第一个文档开始遍历只要状态是 Open 就改成 Closed然后调用doc.Save保存。两个关键参数值得说Save(True, False)的第一个参数表示强制保存即使文档被他人修改也要覆盖第二个参数表示不创建响应文档。老系统里我见过Save(True, True)导致每次更新都生成一份响应副本数据库越跑越大所以批处理脚本里这个参数记得写 False。代码里有个细节很关键我先GetNextDocument(doc)拿到 nextDoc再改当前文档并保存。如果你先改完文档再调GetNextDocument当你的修改条件导致该文档不再符合视图选择公式时游标会找不到下一份文档循环提前结束。这种“配着视图遍历”的写法是我踩过几次坑后的固定套路建议直接照抄。3.3 Java 代理管好线程和对象回收项目里只要出现 JDBC 访问外部库、调用第三方 Java 库、或者要复用一套业务模型我就会写 Java 代理。Java 代理运行在 Domino 的 Java 虚拟机里通过lotus.domino包访问 NSF 对象。它比 LotusScript 更接近现代工程体系但代价是需要自己操心对象回收。下面是同一种批处理逻辑的 Java 版本import lotus.domino.*; public class CloseOpenOrders extends AgentBase { public void NotesMain() { try { Session session getSession(); Database db session.getCurrentDatabase(); View view db.getView(OrdersByStatus); Document doc view.getFirstDocument(); while (doc ! null) { Document next view.getNextDocument(doc); // 先取下一份防止修改视图后游标失效 String status doc.getItemValueString(Status); if (Open.equals(status)) { doc.replaceItemValue(Status, Closed); doc.save(true, false); // 强制保存不创建响应文档 } doc.recycle(); // 及时回收对象避免内存膨胀 doc next; } } catch (Exception e) { e.printStackTrace(); } } }这段代码在逻辑上和 LotusScript 版本完全一致但多了两件重要的事doc.recycle()和next变量的保存。Domino 的 Java 对象和 JVM 堆不是一回事每个 Document 在底层有原生对象句柄不调用recycle()会导致句柄泄漏。老 Domino 服务器上跑长时间代理内存增长到服务器卡死多半是这种问题。还有一个 Java 特有的坑代理里不要自建线程去并发操作 Session。Domino 的Session对象不是线程安全的一个 Session 同时被多个线程使用会得到随机错误最常见的报错是NotesException: Object has been removed or recycled。如果要用并发给每个线程创建独立 Session而且要确保执行完后再回收。这是 Java 代理和普通 JVM 程序差异最大的一点写过一次就记得住。3.4 XPages 与 REST 的选型别让老框架绑架新项目XPages 在 Domino 历史里承担过 Web 开发的重任但它那套基于 JSF 的托管 Bean 和页面生命周期学习曲线比现代前端框架陡得多服务器内存消耗也大。新项目我不会再建 XPages 页面除非是在维护旧系统。现在的常用做法很清晰NSF 继续负责数据存储和业务代理前端用 Vue、React 这类框架通过 REST 接口读写 Domino 数据。Domino 本身有数据服务能力常见接口形式是按视图读条目和按 unid 取文档。我在做集成时会先建一个专门的视图只暴露必要字段然后把查询参数限制好。比直接开放整个 NSF 好得多因为视图是 Domino 索引过的查询结果权限也天然由 Reader/Author 字段控制不会把整库文档暴露出去。这里的关键经验是REST 接口只是壳真正的数据边界和权限边界都在 NSF 里设计好前端拿到的只是 JSON 而已。4. 代理的签名、权限与调度真正决定能不能落地的三件事4.1 代理触发方式和目标选择没人看的“代理属性”其实决定行为写代理时代码本身只占工作量的一半另一半在代理属性里。打开代理属性框至少有三个选择会影响代理行为触发方式、运行位置、目标选择。触发方式决定代理何时执行运行位置决定代码在哪里跑目标选择决定代理处理哪些文档。我整理过一张对照表照着选就好触发方式典型场景需要注意的点手动触发数据修复、临时统计必须有人打开客户端或 Web 页面触发定时触发每日夜间批处理、同步任务间隔受 Domino 调度限制最小粒度取决于版本事件触发表单保存前校验、打开时初始化返回 False 会中断操作必须谨慎代理的目标选择容易被忽略。选择“所有文档”会让代理遍历整个 NSF“新建或修改的文档”只处理当前会话中新增或变化的文档“按公式选择”最灵活可以指定SELECT Status Open。事件触发代理最常见的是QuerySave和QueryOpen它们能在文档保存前做字段校验校验不通过时返回 False 阻止操作。新手经常在代理里写完了代码却忘了设定目标范围结果定时任务跑起来把全库文档都改了一遍这种翻车我已经见过不止一次。4.2 权限体系ACL、ECL 与“以什么身份运行”不能少代理的权限体系比普通应用复杂因为它牵涉到“谁签名”“谁运行”“访问哪个库”三个问题。ACL 是数据库访问控制列表决定代理能访问哪些库以及能对文档做哪些操作签名是代理的代码身份服务器用它判断这段代码来自哪个用户ECL 是执行控制列表在 Notes 客户端里限制代码能调用哪些系统资源。最常见的失败现象是代理在开发机的 Designer 里手动运行正常部署到服务器定时运行却报错“没有权限执行操作”。原因是代理属性里选了“在客户端上运行”定时触发时又没人打开客户端。解决方法是把代理的运行位置改成“在服务器上运行”并且在数据库 ACL 里给代理的签名者至少“代理者”权限。签名者不是当前登录用户而是代理代码的签名证书所有者这点特别容易被混淆。在 Designer 里改完代理代码后选中代理右键选择“签代理”才能更新签名。我经历过一次代码改了、部署了、定时任务还是旧行为最后发现是改完代码后忘记重新签名服务器仍然按照旧签名和旧代码执行。很多“改不动”的问题根本不是逻辑问题而是签名状态没刷新。习惯是在每次发布代理前统一执行一次重新签名包括脚本库。如果脚本库被多个代理引用只重签代理不重签脚本库运行的还是旧版脚本库。4.3 定时调度与服务端日志跑完不等于跑对定时代理是最常见的批处理载体。设置调度时我一般会先按小时跑几天观察日志稳定后再改成每日执行。Domino 定时代理的最小调度间隔通常以分钟计但不要设得太密代理还没跑完又触发新一轮会造成资源竞争。跑完不等于跑对没有日志的代理像没有仪表盘的汽车。我的习惯是代理入口写一行Print输出到服务器控制台关键节点写日志文档或发邮件通知。下面这段代码展示批处理结束后给管理员发通知邮件Sub SendDone(db As NotesDatabase, total As Integer) Dim memo As New NotesDocument(db) memo.Form Memo memo.SendTo Admin memo.Subject 订单状态批处理完成 memo.Body 本轮共处理 CStr(total) 条订单 Call memo.Send(True) End Sub这里的Memo表单是 Domino 内置邮件模板SendTo可以是用户姓名或群组名Send(True)表示直接发送不进发件箱。日志级别不用太细能把每次执行的处理数量、失败数量、开始时间和结束时间记下来就够用了。排查问题时先看控制台方向的Print输出再看通知邮件里有没有异常计数大多数问题五分钟内能定位到代理还是数据有问题。5. 避坑排查让我翻过车的 5 个 Domino 开发细节5.1 视图空白或不刷新追查选择公式里的“时间炸弹”现象文档明明在库里视图打开却是空白或者数据要手动刷新才显示。原因视图选择公式或列公式里用了Today、Now这类可变函数Domino 判断索引无法保持稳定视图自动刷新的机制被破坏。解决把这类逻辑从公式里移出去用定时代理计算后写入普通字段让视图只依赖稳定字段。排查时先看视图的选择公式和列公式里有没有时间函数再检查数据库属性里“不刷新未改变的引用”这项有没有被关掉。5.2 批处理时报“Document has been deleted”循环里自相矛盾现象代理遍历视图处理文档跑到某个节点抛出“Document has been deleted”异常。原因我在循环里删除了当前文档然后又用当前文档向后取下一份游标已经失效。解决办法是标准模板在删除或修改前先取下一份文档处理完当前文档后直接切到下一份全程不回头调当前文档的方法。这个模板在 LotusScript 和 Java 里都通用前面章节的代码块里我特别写了nextDoc变量就是为了避免这个问题。5.3 时间字段总是差 8 小时NotesDateTime 与 Web 端的时区错位现象Domino 里存的时间是 09:00通过 REST 接口返回到前端变成 01:00或者前端提交 10:00库里显示 18:00。原因NotesDateTime 存储的是本地时区时间而大多数 Web 接口按 UTC 或前端时区解析两侧没有做显式时区转换。解决在 REST 接入层统一转换。推荐做法是接口入参要求前端传 UTC 毫秒或 ISO 8601 字符串后端显式创建DateTime并指定时区返回时先取出DateTime再转换成带时区的字符串不要直接丢java.util.Date给 JSON 序列化工具。这个问题不解决后面所有报表统计都会歪。5.4 本地能跑、服务器定时跑就失败签名与运行身份现象代理在 Designer 里手动执行成功配置成服务器定时任务后报权限错误或直接无响应。原因本地执行用的是当前开发机的会话和本地权限服务器上执行时用的是域服务器里代理的签名身份和 ACL 权限两者完全分开。解决把代理运行位置改为服务器确认数据库 ACL 给代理签名者开放了代理者以上权限重新签名整个设计元素和引用的脚本库。如果是 Web 触发的代理还要检查代理属性里的运行身份是“当前 Web 用户”还是“代理作者”前者权限更严格适合前台校验后者适合后台批处理。5.5 富文本字段用 Web 页面保存后变乱码MIME 与普通文本的冲突现象Notes 客户端里正常的富文本字段经过 Web 页面或 REST 接口修改后重新打开 Notes 客户端看到 HTML 源码或内容丢失。原因Domino 在 Web 场景下把富文本字段保存为 MIME 结构而代理里用普通字符串赋值会把字段改写为纯文本项破坏了原生的复合文档格式。解决Web 端尽量避免直接改写富文本字段核心业务数据用普通文本或独立明细文档必须改富文本时要判断字段的底层类型是RICHTEXT还是MIME_PART再决定用GetUnformattedText还是操作MIMEEntity。富文本适合做展示不适合做接口传输这是我的长期结论。6. 进阶技巧把 Domino 数据接出去前先做这三件小事接数据出去不是写一个视图、开一个接口就完事我现在的固定套路是先把这三件小事做完再让前端联调。第一件事是建专门的导出视图。视图名加api_前缀选择公式只管业务条件列只放需要暴露的字段不写任何公式逻辑。这样做的好处是 Domino 会把视图索引优化好REST 接口拉视图条目时不会触发临时计算。注意把视图的“首次使用时不刷新”关掉改成定时刷新否则第一次查询会触发大索引更新的等待。第二件事是查询条件里优先走 DQL 或视图键值。能用视图按键查的就不要用全库搜索DQL 对组合条件友好但返回结果也最好只拿 unid 列表不要一次拉几百个富文本字段。我一般在接口层先查 unid再按 unid 批量取文档详情这样对 NSF 和网络都友好。第三件事是做一个小验证脚本定期检查导出视图的数据是否和源头一致。可以用代理统计视图条目数和库里实际文档数比对也可以抽查最近 100 条记录的更新时间。验证脚本不需要复杂能发现“视图滞后三天”和“权限过滤异常”这两类问题就够了。下面这段用于统计视图条目的代码可以贴在临时代理里Sub Click(Source As Button) Dim s As New NotesSession Dim db As NotesDatabase Dim view As NotesView Set db s.CurrentDatabase Set view db.GetView(api_OrdersExport) MsgBox 导出视图当前条目数 view.EntryCount End SubEntryCount返回的是视图索引中当前缓存的条目数量而不是实时查询结果。如果视图没有刷新这个数可能和 NSF 里真实文档数不一致这本身就说明视图刷新策略需要调整。我遇到的案例里很多“数据少了几条”的报障最后查出来不是代码漏了而是视图索引没有按预期刷新提前跑这个验证脚本能省掉一下午排查。我现在的习惯是凡是给外部系统用的视图都单独建库或单独建视图禁止在共享视图中直接暴露任何视图公式里出现Today一律重写每次发布代理前重新签名脚本库和代理绝不手软。这些规矩看着琐碎但维护了三四个 Domino 应用之后你也会认同它们是省时间的重点。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?