简介:《Domino开发指南精华》面向企业级应用开发者与Lotus Notes/Domino初学者,聚焦Java集成开发这一核心方向,帮助读者解决数据库操作、文档管理、视图控制与代理自动化等实际开发问题。资源包内含1个PDF文件,整体约3.82MB,以文档形式系统梳理lotus.domino包的使用方法,涵盖邮件收发、富文本处理、URL头信息获取等典型场景,并配有大量可直接参考的代码示例。目前已有1938人学习下载,说明其在Domino开发群体中具备一定参考价值。书中不仅讲解基础API调用,还深入讨论视图优化、代理自动化执行复杂任务以及企业协作系统中的集成思路,同时对平台版权与商标使用规范作出说明,便于开发者在合规前提下推进项目。对于希望快速掌握Domino Java开发技巧、构建高效协作系统的读者而言,这份资料兼具操作手册与进阶指南的双重作用。
1. 从一份内部流出的 Domino 开发笔记说起:Java 侧到底能做什么
接手一个跑了七八年的 Domino 系统时,最让人头疼的不是业务逻辑有多绕,而是文档散、示例旧、API 命名还停留在上个时代的风格。前阵子拿到一份叫「Domino开发指南精华」的资料包,翻完之后发现它解决的正是一个很具体的痛点:用 Java 去操作 Notes/Domino 的文档、视图、数据库和会话,而不是只停留在 LotusScript 那一套。这份资料适合两类人——一类是被派去维护存量 Domino 应用、必须用 Java 写扩展的工程师;另一类是想把 Domino 里的数据通过 Java 侧接口抽出来做二次处理的人。它不讲 Domino 服务器怎么装,也不讲 NSF 的底层存储格式,聚焦的是lotus.domino.*这套 Java API 怎么落地。下面按我实际拆包的顺序,把能复现的部分一条条过一遍。
2. Java 访问 Domino 的会话模型:从 Session 到 Database 的链路
2.1 为什么是 Session 而不是直接连库
Domino 的 Java API 不是 JDBC 那种「拿连接就查」的模型。所有操作都挂在一个Session对象下面,Session代表当前线程与 Domino 运行环境之间的会话上下文。这个设计决定了三件事:第一,你不能在任意地方 new 一个 Database 就开干,必须先有 Session;第二,Session 的获取方式取决于你跑在服务器端还是客户端;第三,Session 是有作用域的,跨线程复用会出问题。
常见做法是分两种场景。跑在 Domino 服务器上的 Java 代理(Agent),用NotesFactory.createSession()直接拿;跑在外部 JVM 里通过 DIIOP 连的,需要传主机、用户名和密码。资料里给的是服务器端代理的写法,这也是最稳的一条路,因为不涉及网络层和端口配置。
import lotus.domino.*; public class SessionDemo { public static void main(String[] args) { Session session = null; Database db = null; try { // 服务器端代理场景:无参创建,绑定当前 Domino 运行时 session = NotesFactory.createSession(); // 用服务器名 + 数据库路径拿到 Database 句柄 // 路径用反斜杠分隔,且不带 .nsf 后缀 db = session.getDatabase("CN=AppServer/O=Demo", "apps\\order.nsf"); if (db == null || !db.isOpen()) { System.out.println("数据库打不开,检查路径和权限"); return; } System.out.println("当前用户: " + session.getUserName()); System.out.println("文档数: " + db.getDocumentCount()); } catch (NotesException e) { // NotesException 是这套 API 的统一异常,别只 catch Exception System.out.println("错误码: " + e.id + " 信息: " + e.text); } finally { // 释放顺序和创建顺序相反 if (db != null) db.recycle(); if (session != null) session.recycle(); } } }逻辑上这段代码做了四件事:建会话、开库、读元信息、回收资源。参数上要注意getDatabase的第一个参数是服务器规范名,不是 IP,也不是主机名,写错了会直接返回 null 而不抛异常,这是新手最容易卡住的地方。第二个参数是相对 Data 目录的路径,用双反斜杠转义。
2.2 recycle 不是可选项
recycle()这套机制是 Domino Java API 里最反直觉的设计之一。它背后是 C++ 层的对象引用计数,Java 的 GC 管不到那部分内存。如果你只依赖 GC,跑批量任务时内存会一路涨到 OOM。资料里反复强调的一点是:凡是实现了lotus.domino.Base接口的对象,用完都要 recycle,包括 Document、View、ViewEntry、Item、RichTextItem 等等。
我一般会按「谁创建谁回收」的原则写,配合 try-finally。批量循环里尤其要注意,每次迭代产生的 Document 对象如果不回收,几千条下来就撑不住了。有个折中做法是用session.recycle()一次性回收该会话下所有对象,但它会把你还在用的对象也干掉,所以只适合在任务彻底结束时调用。
2.3 视图遍历的两种姿势
拿到 Database 之后,读数据主要靠 View。db.getView("视图名")拿到视图句柄,然后有两种遍历方式:getAllDocuments()返回 DocumentCollection,适合小数据量;getAllEntries()返回 ViewEntryCollection,适合大数据量,因为它读的是视图索引而不是逐个打开文档。
View view = db.getView("ByStatus"); // 用视图条目遍历,比 getAllDocuments 省内存 ViewEntryCollection vec = view.getAllEntries(); ViewEntry entry = vec.getFirstEntry(); int count = 0; while (entry != null) { // 只取需要的列,避免 openDocument 带来的开销 Vector<?> cols = entry.getColumnValues(); String orderNo = String.valueOf(cols.get(0)); String status = String.valueOf(cols.get(1)); System.out.println(orderNo + " -> " + status); count++; ViewEntry next = vec.getNextEntry(entry); entry.recycle(); // 当前条目用完立刻回收 entry = next; } System.out.println("共处理 " + count + " 条"); vec.recycle(); view.recycle();这里的关键参数是getColumnValues()返回的 Vector,下标对应视图列的排列顺序,从 0 开始。用列值而不是打开文档,性能差距在万级数据上非常明显。注意getNextEntry要在 recycle 当前 entry 之前调用,否则会拿到空指针——这个顺序坑我踩过一次,排查了半天。
3. 文档读写与富文本处理:Item 类型判断和 RichText 的边界
3.1 新建文档与字段赋值
写文档的流程是db.createDocument()拿到 Document,然后replaceItemValue设字段,最后save()。这里有个容易忽略的点:replaceItemValue对同名字段是覆盖,appendItemValue是追加。对于普通字段用 replace 就对了,但多值字段要传 Vector。
Document doc = db.createDocument(); doc.replaceItemValue("Form", "Order"); doc.replaceItemValue("OrderNo", "SO-2024-0001"); doc.replaceItemValue("Amount", 1280.50); // 多值字段必须用 Vector,直接传数组不会报错但存进去是单值 Vector<String> tags = new Vector<>(); tags.add("urgent"); tags.add("export"); doc.replaceItemValue("Tags", tags); // 日期字段用 DateTime,不要用 java.util.Date 直接塞 DateTime dt = session.createDateTime("2024-06-01 10:00:00"); doc.replaceItemValue("CreateTime", dt); boolean ok = doc.save(); if (!ok) { System.out.println("保存失败,可能是表单校验或冲突"); } doc.recycle(); dt.recycle();参数上,Form字段决定这条文档用哪个表单渲染,写错了在 Notes 客户端里会显示成「无表单」。金额用 double 存没问题,但涉及精度计算建议在 Java 侧用 BigDecimal 算完再转。日期字段必须走session.createDateTime,直接塞 Java Date 在部分版本上会存成文本。
3.2 读字段时先判类型
读文档字段不能想当然地按类型取。doc.getItemValueString("Amount")对数字字段也能返回字符串,但格式依赖服务器区域设置,可能带千分位。稳妥做法是先拿 Item 对象,判断类型再取值。
Item item = doc.getFirstItem("Amount"); if (item != null) { switch (item.getType()) { case Item.NUMBER: double amount = item.getValueDouble(); break; case Item.TEXT: String text = item.getValueString(); break; case Item.DATETIMES: DateTime d = (DateTime) item.getValueDateTime(); break; default: // 富文本、附件等类型单独处理 break; } item.recycle(); }getType()返回的常量定义在 Item 类里,NUMBER、TEXT、DATETIMES、RICH_TEXT、ATTACHMENT 等。资料里特别提醒:多值字段的 getType 返回的是单值类型,比如多值文本返回 TEXT,但getValueString()只会给你第一个值,要拿全得用getValues()。
3.3 富文本和附件的处理边界
RichTextItem 是这套 API 里最不好惹的部分。往富文本里追加内容用appendText、appendParagraph这些方法,但不能修改已有的富文本内容,只能追加。想改已有内容,只能删掉整个 Item 重建。
RichTextItem rt = doc.createRichTextItem("Body"); rt.appendText("订单已创建,等待审核。"); rt.addNewLine(); rt.appendText("金额:" + amount); // 附件用 embedObject,路径是服务器本地路径 rt.embedObject(EmbeddedObject.EMBED_ATTACHMENT, "", "D:\\temp\\report.pdf", null);附件嵌入后,EmbeddedObject也要 recycle。另外embedObject的路径是服务器端文件系统路径,不是客户端路径,跑在外部 JVM 时这个路径指的是运行 Java 的那台机器。这个区别在跨机部署时经常搞混。
4. 避坑与排查:Java Domino 开发里最容易翻车的五件事
4.1 现象:getDatabase 返回 null,不抛异常
原因:服务器名写成了 IP 或主机名,或者数据库路径带了.nsf后缀,或者当前用户对该库没有访问权限。Domino 在这几种情况下都返回 null 而不是抛 NotesException。
解决:服务器名用session.getServerName()打印出来对照,路径去掉后缀,权限问题用db.getACL().getEntry(session.getUserName()).getLevel()查一下。我一般会在 getDatabase 之后立刻加一句if (db == null) throw new RuntimeException("库打不开"),把静默失败变成显式报错。
4.2 现象:批量处理跑到一半 OOM
原因:循环里创建的 Document、ViewEntry、Item 没有 recycle,C++ 层内存持续增长,Java 堆还没满,本地内存先爆了。
解决:每个循环体内创建的对象在当次迭代结束前 recycle。用Runtime.getRuntime().freeMemory()观察没用,要看的是进程的本地内存。稳妥做法是每处理 500 条调一次session.recycle(),但前提是当前没有跨迭代复用的对象。
4.3 现象:日期字段读出来差 8 小时
原因:Domino 存的是 UTC 时间,getValueDateTime()拿到的 DateTime 对象默认按服务器时区解释,但getValueString()可能按不同规则格式化。
解决:统一用 DateTime 对象取值,需要转 Java Date 时用dt.toJavaDate(),并且显式设置时区。别用字符串取日期字段,格式不可控。
4.4 现象:视图遍历拿到的列值和预期对不上
原因:视图列的排列顺序和你在 Designer 里看到的可能不一致,尤其是隐藏列和分类列。getColumnValues()返回的 Vector 包含所有列,包括隐藏的。
解决:在 Designer 里数清楚列的顺序,或者改用entry.getDocument()打开文档按字段名取。后者慢但不会错位。资料里建议对性能敏感的视图,在列定义里加注释标明序号。
4.5 现象:save 返回 false 但不报错
原因:文档保存冲突、表单校验失败、或者必填字段为空。Domino 的 save 返回 boolean,不抛异常,具体原因要查日志。
解决:保存前用doc.validate()检查,冲突用doc.save(true, false)强制保存并检查doc.getLastError()。生产环境里我一般会在 save 失败时把文档的 UniversalID 和关键字段打到日志,方便回溯。
5. 进阶:用 Java 代理做定时批处理与性能验证
把上面这些拼起来,最常见的落地场景是写一个定时 Java 代理,每天跑一次,扫描视图里状态为「待处理」的文档,做业务计算后回写状态。这个场景能同时验证会话管理、视图遍历、文档读写和资源回收四条链路。
public class BatchAgent { public static void main(String[] args) { Session session = null; Database db = null; int processed = 0; try { session = NotesFactory.createSession(); db = session.getDatabase(session.getServerName(), "apps\\order.nsf"); if (db == null) return; View view = db.getView("PendingOrders"); Document doc = view.getFirstDocument(); while (doc != null) { Document next = view.getNextDocument(doc); // 先取下一个再处理 try { double amount = doc.getItemValueDouble("Amount"); // 业务规则:超过 1000 的标记为需复核 doc.replaceItemValue("NeedReview", amount > 1000 ? "1" : "0"); doc.replaceItemValue("ProcessDate", session.createDateTime("Today")); if (!doc.save()) { System.out.println("保存失败: " + doc.getUniversalID()); } else { processed++; } } catch (NotesException e) { System.out.println("单条处理失败: " + e.text); } finally { doc.recycle(); } doc = next; } view.recycle(); System.out.println("本次处理 " + processed + " 条"); } catch (NotesException e) { System.out.println("代理异常: " + e.id + " " + e.text); } finally { if (db != null) db.recycle(); if (session != null) session.recycle(); } } }这段代码里有几个值得单独说的点。第一,getNextDocument必须在当前 doc 处理完之前调用,因为 recycle 之后就拿不到下一个了,所以用next变量暂存。第二,单条失败不影响整体,用内层 try-catch 兜住,这在批量任务里很重要,否则一条脏数据能让整个代理挂掉。第三,session.createDateTime("Today")是 Domino 的日期宏,比手动拼字符串可靠。
验证方法上,我一般分三步走:先在测试库跑一遍,用db.getDocumentCount()对比处理前后的状态字段数量;再把代理挂到服务器上手动触发一次,看服务器日志里的输出;最后才是设成定时任务。性能上,万级文档的视图遍历加回写,在普通配置的服务器上大概几分钟量级,如果超过十分钟就要检查是不是在循环里打开了文档或者漏了 recycle。
从那以后我每次写 Domino 的 Java 代理,都会在 finally 里强制走一遍 recycle,并且在循环里加一个计数器,每 500 条打一行日志——这样一旦卡住或者内存异常,至少知道跑到哪了。希望帮到你。
本文还有配套的精品资源,点击获取