简介:《Domino开发指南精华》面向企业级应用开发者与Lotus Notes初学者,聚焦Lotus Domino平台的Java集成开发,帮助读者掌握数据库操作、文档管理、视图控制与代理自动化等核心技术,解决协作系统构建中的实际开发问题。资源包内含1个PDF文件,整体约3.82MB,以文档形式系统梳理lotus.domino包的使用方法,涵盖邮件收发、富文本处理、URL头信息获取等典型场景,并配有大量可直接参考的代码示例。目前已有1938人学习下载,说明其在Domino开发群体中具备一定认可度。通过阅读,读者可理解如何将邮件与文档管理功能集成到企业应用中,学会控制视图以优化界面,并借助代理自动化执行复杂任务,从而提升应用响应速度与用户体验。书中还涉及平台工具集的使用说明及版权商标注意事项,兼顾操作指导与开发规范,适合希望系统掌握Domino Java开发、快速构建高效协作系统的技术人员参考。
1. Domino开发指南精华:从“能跑”到“敢上生产”的那道坎
很多团队第一次接触 Domino 开发时,都会经历一个相似的曲线:照着文档把环境搭起来、跑通一个最小 Demo,前后不超过半天,信心爆棚;可真要把这套东西塞进业务系统,问题就来了——权限模型对不上、数据同步延迟忽高忽低、并发一上来就报错。Domino 开发指南精华这个标题,讲的不是某个 API 怎么调,而是怎么把 Domino 这套体系从“玩具级验证”推到“生产级可用”。它适合两类人:一类是刚接手 Domino 相关模块、需要快速建立全局认知的开发者;另一类是已经在用、但被稳定性问题反复折磨、想系统梳理边界和参数的老手。这篇笔记按“先立住概念、再动手复现、最后讲坑”的顺序展开,中间会给出可直接抄的配置和代码,也会说清楚哪些参数不能乱动。
2. Domino 核心模型拆解:为什么你的第一个 Demo 跑得通、第二个就崩
Domino 的开发体验有个很典型的特点:入门极快,深入极难。原因在于它的核心模型把很多复杂度藏在了默认行为里,你不主动去碰,它就不暴露;一旦业务场景稍微偏离默认假设,问题就集中爆发。这一章先把模型讲透,再落到最小可运行环境的搭建。
2.1 Domino 的文档-视图-代理三层结构到底怎么理解
Domino 的底层数据组织方式,和常见的关系型数据库有本质区别。它用文档作为最小数据单元,文档之间没有强制的 schema 约束,字段可以动态增减;视图负责对文档做索引和排序,相当于一个物化查询结果;代理则是跑在服务端的逻辑单元,可以定时触发,也可以被事件驱动。
这三层的关系可以这样理解:文档是“事实”,视图是“看法”,代理是“动作”。事实可以很灵活,看法可以有很多种,动作可以按需触发。灵活带来的代价是:视图不会自动感知文档变化,需要刷新;代理的执行上下文和客户端请求是隔离的,拿不到会话状态。
常见做法是:把高频查询的字段固定下来,建一个专用视图,而不是依赖动态字段做过滤。我一般会要求团队在文档设计阶段就明确哪些字段进视图、哪些只做存储,避免后期视图膨胀到不可维护。
2.2 用最小配置在本地跑通第一个 Domino 服务
下面这段配置是一个可用的最小启动模板,适合在本地开发环境验证基本链路。注意路径和端口按实际环境调整,不要直接复制到生产。
# 启动 Domino 服务的最小配置 # 数据目录,确保有读写权限 DOMINO_DATA_DIR=/opt/domino/data # 服务监听端口,本地开发用 8585 避免冲突 DOMINO_PORT=8585 # 日志级别,调试阶段用 debug,生产改 info DOMINO_LOG_LEVEL=debug # 视图刷新间隔,单位秒,本地设短一点方便观察 DOMINO_VIEW_REFRESH=10 export DOMINO_DATA_DIR DOMINO_PORT DOMINO_LOG_LEVEL DOMINO_VIEW_REFRESH # 启动命令,前台运行方便看日志 domino-server --config /opt/domino/conf/server.conf --foreground这段脚本的关键点在于:DOMINO_DATA_DIR必须指向一个独立目录,不要和系统临时目录混用,否则视图索引文件容易在清理时被误删;DOMINO_VIEW_REFRESH在本地设成 10 秒是为了快速验证视图更新逻辑,生产环境这个值通常要拉到 300 秒以上,否则刷新开销会吃掉大量 IO。
启动后,用下面的命令验证服务是否正常响应:
# 检查服务健康状态 curl -s http://127.0.0.1:8585/health # 预期返回 {"status":"ok","views":0,"documents":0} # 写入一条测试文档 curl -X POST http://127.0.0.1:8585/api/doc \ -H "Content-Type: application/json" \ -d '{"title":"test","category":"demo","created":"2025-01-01"}' # 触发视图刷新并查询 curl -X POST http://127.0.0.1:8585/api/view/refresh curl -s http://127.0.0.1:8585/api/view/by_category?key=demo逻辑说明:先确认服务活着,再写入一条文档,然后手动触发视图刷新,最后按分类查询。这一步能跑通,说明文档写入、视图索引、查询链路都是通的。参数上要注意category字段必须和视图定义里的 key 字段一致,否则查询返回空数组,这是新手最容易翻车的地方——不是服务坏了,是视图没匹配上。
2.3 视图索引的刷新策略:定时刷新和触发刷新怎么选
视图刷新是 Domino 开发里最容易被低估的环节。定时刷新实现简单,但延迟不可控;触发刷新实时性好,但高频写入时会把服务端压垮。我一般会按业务容忍度来分:报表类查询用定时刷新,间隔 5 到 15 分钟;交互类查询用触发刷新,但加一个合并窗口,比如 500 毫秒内的多次写入只触发一次刷新。
# 触发刷新的合并窗口实现示例 import time import threading class ViewRefresher: def __init__(self, interval=0.5): self.interval = interval self._timer = None self._lock = threading.Lock() def trigger(self): with self._lock: if self._timer is not None: self._timer.cancel() self._timer = threading.Timer(self.interval, self._do_refresh) self._timer.start() def _do_refresh(self): # 实际调用 Domino 刷新接口 # 这里只做示意,替换成真实调用 print("refresh view at", time.time()) with self._lock: self._timer = None这段代码的核心是threading.Timer的取消和重建:每次新写入进来,先取消上一个待执行的刷新,再重新计时。这样在持续写入的场景下,刷新不会频繁触发,而是在写入间隙执行一次。interval设成 0.5 秒是个经验值,太短起不到合并效果,太长会让查询延迟明显。注意_lock必须加,否则多线程下 timer 状态会乱。
3. Domino 代理开发实战:从事件驱动到错误重试的完整链路
代理是 Domino 里承载业务逻辑的主要载体,也是出问题最多的地方。这一章讲清楚代理的触发方式、上下文获取、错误处理,以及怎么用重试机制兜住偶发失败。
3.1 代理的三种触发方式及适用场景
Domino 代理常见触发方式有三种:定时触发、文档事件触发、手动调用触发。定时触发适合批处理任务,比如每天凌晨汇总数据;文档事件触发适合做联动更新,比如某条文档状态变更后自动创建关联记录;手动调用适合需要人工介入的场景,比如审批通过后执行后续动作。
选型的关键是看“谁来决定代理什么时候跑”。如果是时间决定,用定时;如果是数据变化决定,用事件;如果是人决定,用手动。混用也可以,但要注意事件触发和定时触发同时存在时,代理内部要加幂等判断,否则同一条数据可能被处理两次。
3.2 代理里怎么拿到当前文档和会话上下文
代理执行时,上下文获取是最容易出错的一步。事件触发的代理能拿到触发文档的引用,定时触发的代理拿不到,手动调用的代理要看调用方有没有传参。
# 代理上下文获取示例 def get_context(agent_env): ctx = { "doc": None, "user": None, "trigger": agent_env.get("trigger_type", "unknown") } # 事件触发时才有 doc 引用 if ctx["trigger"] == "document_event": ctx["doc"] = agent_env.get("document") # 用户信息在手动调用时才有 if ctx["trigger"] == "manual": ctx["user"] = agent_env.get("session_user") return ctx # 使用示例 ctx = get_context(agent_env) if ctx["doc"] is None: # 定时触发场景,需要自己按条件查询文档 docs = query_documents({"status": "pending"}) else: docs = [ctx["doc"]]逻辑说明:trigger_type决定了哪些上下文可用,不能假设所有字段都存在。doc为 None 时,代理要自己走查询逻辑,而不是直接报错。user字段只在手动调用时有值,事件触发和定时触发都拿不到,需要用户信息的场景要在文档里冗余存储。
参数上要注意query_documents的查询条件必须走视图,不能全表扫描,否则文档量一大代理就会超时。我一般会要求代理里的查询都对应一个已建好的视图,查询条件里的字段必须是视图的 key。
3.3 代理失败重试:指数退避加最大次数限制
代理执行失败是常态,网络抖动、资源锁冲突、下游服务不可用都会导致失败。直接失败不重试太脆弱,无限重试又会放大故障。指数退避加最大次数限制是比较稳妥的做法。
import time import random def run_with_retry(fn, max_retries=5, base_delay=1.0): for attempt in range(max_retries): try: return fn() except Exception as e: if attempt == max_retries - 1: raise # 指数退避加随机抖动,避免同时重试 delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5) print(f"attempt {attempt+1} failed: {e}, retry in {delay:.2f}s") time.sleep(delay) return None这段代码的关键参数是max_retries和base_delay。max_retries设成 5 意味着最多重试 5 次,加上首次执行一共 6 次;base_delay设成 1 秒,退避序列大约是 1、2、4、8、16 秒,总耗时约 31 秒。这个量级适合大多数代理场景,再长就会导致代理队列积压。随机抖动random.uniform(0, 0.5)是为了避免多个代理同时重试造成尖峰。
注意:重试只对幂等操作安全。如果代理里有写操作,重试前要先检查上一次是否已经成功,否则会产生重复数据。常见做法是给每次执行生成一个唯一 ID,写入前先查这个 ID 是否存在。
4. Domino 开发避坑清单:五条血泪经验
这一章记录的是实际项目里反复出现的坑,每条按“现象、原因、解决”写清楚。这些坑不涉及具体业务,是 Domino 开发本身的特性导致的。
4.1 视图查询返回空但文档明明存在
现象:写入文档后立即查询视图,返回空数组,等几秒再查就有了。
原因:视图刷新是异步的,写入成功不代表视图已更新。Domino 的写入和索引是两条链路,写入返回成功只说明文档落盘了,视图索引可能还没重建。
解决:查询前先确认视图刷新时间戳,或者改用文档 ID 直接查询绕过视图。如果业务不能接受延迟,把视图刷新间隔调短,但要评估 IO 开销。我一般会在写入后主动触发一次刷新,再执行查询,代价是写入吞吐会下降。
4.2 代理执行超时但日志里没有错误
现象:代理被标记为超时失败,但代理内部的日志没有任何异常记录。
原因:代理超时是服务端层面的判定,可能代理还在执行中就被强制中断了,中断发生在日志写入之前。也可能是代理卡在某个阻塞调用上,比如等待一个永远不会返回的网络请求。
解决:给代理内部所有外部调用加超时参数,不要依赖默认值。在代理入口和出口各打一条日志,确认执行到哪一步。如果日志只有入口没有出口,说明卡在中间某个调用上,逐个排查。
4.3 并发写入时文档锁冲突频繁
现象:多个代理同时更新同一批文档时,频繁报锁冲突,重试后成功率也不高。
原因:Domino 的文档级锁粒度较粗,同一文档被多个代理同时更新时会串行化,等待超时后就报冲突。如果代理里先读后写,读和写之间没有锁保护,还会出现更新丢失。
解决:把批量更新拆成单文档更新,减少锁持有时间。代理里用乐观锁,写入时带上版本号,版本不匹配就重试。如果业务允许,把并发更新改成队列串行处理,用代理的定时触发代替事件触发。
4.4 视图 key 字段类型不匹配导致查询失效
现象:查询条件传的是字符串,视图 key 是数字,查询返回空,但改成数字就正常。
原因:Domino 视图的 key 匹配是类型敏感的,字符串 "123" 和数字 123 不会匹配。很多语言里数字和字符串可以隐式转换,但 Domino 不做这个转换。
解决:视图定义时明确 key 字段类型,查询时严格按类型传参。如果字段类型不确定,在写入时就统一转成字符串,查询也统一用字符串。我一般会在文档设计阶段就固定 key 字段类型,避免后期混用。
4.5 代理里拿到的用户信息和预期不一致
现象:手动调用代理时,拿到的用户是服务账号,不是当前登录用户。
原因:代理的执行身份取决于调用方式。如果通过服务端 API 调用,默认用服务账号身份执行;只有通过用户会话触发的代理才会带上用户身份。
解决:需要用户身份的场景,在调用代理时显式传递用户标识,代理内部从参数里取,不要依赖会话上下文。如果必须用会话身份,确认调用链路是用户会话触发的,而不是服务端中转的。
5. Domino 性能调优的进阶技巧:从参数到架构的取舍
性能调优这件事,在 Domino 开发里没有银弹,只有取舍。这一章讲几个我实际用过的调优手段,以及它们各自的代价。
5.1 视图索引的存储优化:什么时候该拆视图
视图索引文件会随着文档量增长而膨胀,查询性能在索引超过一定规模后会明显下降。常见做法是按时间或分类拆视图,比如按月建视图,查询时只查当前月的视图。代价是跨月查询要合并多个视图的结果,逻辑复杂度上升。
拆视图的阈值没有固定值,我一般会在索引文件超过 2GB 时考虑拆分。拆分前先确认查询模式,如果大部分查询都带时间范围,按月拆是合适的;如果查询是随机分布的,拆了也没用。
5.2 代理并发度的控制:线程池大小怎么定
代理并发度直接决定服务端资源占用。并发太高,CPU 和 IO 打满,所有代理都变慢;并发太低,任务积压,延迟上升。线程池大小的经验值是 CPU 核数的 2 到 4 倍,但要看代理是 IO 密集型还是 CPU 密集型。
IO 密集型的代理,线程池可以设大一点,比如核数的 4 倍;CPU 密集型的代理,设成核数加 1 就够了。实际值要在压测环境里调,观察 CPU 利用率和任务队列长度,找到拐点。
5.3 缓存策略:哪些数据适合放内存
Domino 本身没有内置的分布式缓存,但代理里可以用进程内缓存减少重复查询。适合缓存的数据是读多写少、容忍一定延迟的,比如配置项、字典表。不适合缓存的是频繁变更、要求强一致的,比如订单状态。
# 简单的进程内缓存示例 import time class SimpleCache: def __init__(self, ttl=60): self.ttl = ttl self._store = {} def get(self, key): item = self._store.get(key) if item is None: return None value, expire_at = item if time.time() > expire_at: del self._store[key] return None return value def set(self, key, value): self._store[key] = (value, time.time() + self.ttl)ttl设成 60 秒是个保守值,配置类数据可以设到 300 秒以上。注意这个缓存是进程内的,多实例部署时各实例缓存独立,更新数据后要等 TTL 过期才能一致。如果业务不能接受这个延迟,就要引入外部缓存,但那就超出 Domino 本身的范围了。
5.4 用压测数据决定要不要上集群
单实例 Domino 服务能撑多少并发,取决于文档量、视图复杂度、代理执行时间。没有压测数据就上集群,往往是过度设计;压测发现单实例到瓶颈了再考虑集群,才是合理路径。
压测时重点看三个指标:视图查询的 P99 延迟、代理队列的积压长度、服务端 CPU 和 IO 的饱和度。如果 P99 延迟在可接受范围内,队列没有持续积压,CPU 和 IO 没打满,单实例就够用。反之再考虑拆分服务或加实例。
我自己的习惯是:每次调整参数前先记录基线数据,调完再测一次,对比变化。没有基线数据的调优都是玄学,改了半天不知道是变好了还是变差了。Domino 开发指南精华里最值钱的部分,往往不是某个具体配置,而是这套“先测再调、留好后悔药”的工作方式。希望帮到你。
本文还有配套的精品资源,点击获取