☰
Domino B/S开发Excel导入导出:Java代理与Apache POI实战
2026/9/30 7:51:02 网站建设 项目流程

简介:面向Lotus Domino平台BS应用开发者,内容聚焦使用LotusScript代理与Java代理完成Excel导入导出的常见需求,适合在浏览器/服务器架构中处理数据报表、批量迁移或报表生成场景的工程师。这项工作在企业数据整合、批量上报和数据库迁移等日常运维中非常常见。导出以Java代理为主,说明如何设置输出内容为Excel可识别的MIME类型,借助PrintWriter逐行生成制表符分隔数据,并遍历视图文档,按指定字段读取且处理多值域;导入部分介绍通过Apache POI解析Excel后写回NotesDocument的典型思路,也提及LotusScript下的实现方式。包体为单个doc文档,仅35KB,内容集中精炼,可直接用于开发参考。已有140人浏览学习,适合正在Domino平台做BS开发、希望快速实现Excel数据交互的读者。

1. Lotus Domino BS开发里的Excel导入导出:代理是入口,MIME是核心

在 Lotus Domino 的 BS 应用里,导出 Excel 的核心其实是一段以 application/vnd.ms-excel 开头、用 TAB 分隔的文本流,浏览器看到 MIME 头就自动变成 Excel。难的是把视图文档、域值、多值域和回收时机处理对。这份资源里我整理了 Java 代理导出、Apache POI 导入,以及等价的 LotusScript 代理三套实现。适合正在用 Domino BS 做报表导出、数据迁移的开发者,也适合想从 LotusScript 迁到 Java 代理的团队。提前避雷:这里说的 Java 代理是 Domino 里的 Java 代理程序,不是 JDK 动态代理,别被热词带偏。

2. Java代理导出Excel:MIME类型、TAB分隔与对象回收的完整拆解

2.1 入口与输出流:为什么是NotesMain()而不是main(),为什么用getAgentOutput()而不是System.out

Domino 的 Java 代理不是普通 Java 程序,它的入口固定是继承 AgentBase 后的 NotesMain(),由 Domino 服务器在当前 HTTP 请求线程里调用。你写不了 public static void main(String[] args),写了也不会被调度。这就是很多第一次接触 Domino 代理的人翻车的地方:拿起来先在本地写了个 main 方法测试,结果部署到服务器完全没动静。

看这段入口代码,它完成了代理工作的全部前置条件:

public void NotesMain() { try { session = getSession(); agentContext = session.getAgentContext(); db = agentContext.getCurrentDatabase(); view = db.getView("Documents.vw"); pw = getAgentOutput(); pw.println("Content-Type:application/vnd.ms-excel"); printHead(); printView(); pw.print(sb.toString()); } catch (Exception e) { e.printStackTrace(); } finally { recycle(); } }

逻辑说明:getSession() 从当前代理线程拿到 NotesSession,它代表与 Domino 服务器的连接;agentContext 是当前代理的上下文对象,里面有当前数据库、当前文档、请求参数等信息;db.getView("Documents.vw") 取的并不是视图文件名,而是 Designer 里视图的别名,建议用别名而不是视图名,因为视图改名后别名通常不变。

参数说明:getAgentOutput() 返回 PrintWriter,它是 Domino 为当前 HTTP 响应准备的输出通道。这里最容易踩的坑是用 System.out.println 输出,那只会把内容打到服务器控制台和日志里,浏览器端什么都收不到。Content-Type 必须写在最前面,我用的是原代码的写法,你也可以在它后面再 println 一个空行,避免部分 Web 服务器把第一行内容当作响应头的一部分处理。

2.2 表头与字段取值:多值域不能只取第一个值

printHead() 本身很简单,但它和 FIELDS、FIELDSNAME 两个数组的对应关系是导出正确性的基础。FIELDS 是文档中字段的实际名称,FIELDSNAME 是 Excel 里要显示的中文表头,两个数组必须一一对应,否则导出的列顺序和内容就对不上。

private void printHead() { for (String fieldName : FIELDSNAME) { sb.append(fieldName); sb.append("\t"); } sb.append("\n"); }

逻辑说明:循环把表头字符串追加到 StringBuilder,每列之间用 TAB 分隔,最后追加一个换行。这里用 StringBuilder 而不是每行直接 PrintWriter 输出,是为了减少 I/O 次数,数据量大时差异很明显。

printDocInfo() 是导出时最需要小心的部分。原代码里只取 Vector 的第一个值,这在单选域和普通文本域没问题,但碰上多值域就会丢数据。我一般会改成这样:

private void printDocInfo(Document doc) throws NotesException { for (String field : FIELDS) { Vector fldValue = doc.getItemValue(field); if (fldValue.size() == 0) { sb.append("\t"); continue; } StringBuilder cell = new StringBuilder(); for (int i = 0; i < fldValue.size(); i++) { if (i > 0) { cell.append(";"); } cell.append(fldValue.get(i).toString()); } sb.append(cell.toString()); sb.append("\t"); } sb.append("\n"); }

逻辑说明:getItemValue(field) 返回 Vector,即使字段只有一个值,它也是长度为 1 的 Vector。size() 为 0 时补一个空白 TAB,保证 Excel 列不错位。多值域遍历后用分号拼接,这样 Excel 单元格里能看到完整内容,而不是只保留第一个值。

参数说明:这里的 cell.append(";") 中的分号是我自己定的分隔符,你可以改成逗号或竖线,只要导入时能对应处理。日期字段用 toString() 拿到的通常是 Domino 默认格式文本,如果你需要精确到时分秒的标准格式,建议在导出前对日期字段单独做格式化。

2.3 视图遍历与回收时机:先取下一个文档,再回收当前文档

printView() 的循环写法是 Domino Java 代理的经典模式,它处理了对象回收的安全边界:

private void printView() throws NotesException { Document doc = view.getFirstDocument(); Document temp = null; while (doc != null) { printDocInfo(doc); temp = view.getNextDocument(doc); doc.recycle(); doc = temp; } }

逻辑说明:view.getFirstDocument() 拿到第一条文档,循环里先输出当前文档数据,然后用 view.getNextDocument(doc) 拿下一跳文档并赋给 temp,接着立即回收当前 doc,再把 temp 赋给 doc。这个顺序不能反,如果先 recycle 再 getNextDocument,这个文档对象的内存句柄已经释放,再访问会抛 NotesException。

参数说明:recycle() 是必须的,Domino 的 Notes 对象在 Java 里是对底层 C 接口的封装,不回收会导致句柄泄漏。长时间运行的大批量导出,句柄耗尽后代理表现为突然变慢、报"Notes/Domino object handle limit exceeded"或者干脆崩掉。recycle() 方法里要按依赖顺序回收:先 pw.close(),再 view、db、agentContext,最后 session。这个顺序不能打乱,session 必须最后一个回收,因为它是最外层的连接持有者。

这一段来自原工程里一个很完整的 OutputToExcel 类,我在 Designer 里建 Java 代理时,把代理类型选成 Java 代理(不是 LotusScript 也不是 Web Service),Target 选当前数据库,触发方式按需选“通过 URL 调用”或者“代理选择”。这样浏览器访问一个连接的 URL 就能触发导出。

3. Apache POI导入Excel:附件解析、单元格类型判断与文档落库

3.1 为什么必须用Apache POI来导入,而不是自己解析文件

导出可以靠 TAB 分隔文本流糊弄过去,导入不行。Excel 的 xls 是 OLE2 复合文档格式,xlsx 是基于 ZIP 的 OpenXML 格式,手动解析这两者工作量非常大,而且边缘情况极多。Apache POI 是 Java 处理 Office 文件的通用选择,HSSFWorkbook 吃 xls,XSSFWorkbook 吃 xlsx,API 层面统一用 Workbook、Sheet、Row、Cell 操作。

我一般不建议在 Domino 里用 LotusScript 直接解析 xlsx,LotusScript 处理二进制和 ZIP 解压非常吃力。BS 场景下常见的做法是:前端表单里加一个富文本域,用户在页面选择 Excel 文件上传,保存后文档附件里就有这个文件。代理在 WebQuerySave 事件或者一个按钮触发的独立代理里读当前文档的附件,用 POI 把数据解析出来,再逐行创建 NotesDocument。

3.2 从当前文档取附件并解析工作簿

public class ExcelImport extends AgentBase { public void NotesMain() { Session session = null; AgentContext ctx = null; Database db = null; Document doc = null; try { session = getSession(); ctx = session.getAgentContext(); db = ctx.getCurrentDatabase(); doc = ctx.getDocumentContext(); RichTextItem rt = (RichTextItem) doc.getFirstItem("ExcelFile"); if (rt == null) { return; } for (int i = 0; i < rt.getEmbeddedObjectCount(); i++) { EmbeddedObject eo = rt.getEmbeddedObject(i); if (eo.getType() == EmbeddedObject.EMBED_ATTACHMENT) { importWorkbook(db, eo.getName(), eo.getInputStream()); } } } catch (Exception e) { e.printStackTrace(); } finally { // 按依赖顺序回收对象 if (doc != null) { doc.recycle(); } if (db != null) { db.recycle(); } if (ctx != null) { ctx.recycle(); } if (session != null) { session.recycle(); } } } }

逻辑说明:ctx.getDocumentContext() 拿到的是触发代理时上下文里的文档。如果是 WebQuerySave 触发,就是用户当前提交的那张表单文档;如果是按钮点击后调用代理,则还是当前打开编辑的文档。getFirstItem("ExcelFile") 取富文本域,循环里用 getEmbeddedObject(i) 逐个检查附件。

参数说明:eo.getInputStream() 是 POI 读取的入口,用完之后不要主动关闭,因为它内部绑定的是 Domino 的附件流,随文档回收即可。埋一个坑:如果你的导入代理是定时批量任务,拿不到 documentContext,此时要从数据库某个视图里查询待处理文档,再通过 doc.getAttachment("ExcelFile") 取附件,路径逻辑不一样。

importWorkbook 方法做真正的解析和落库:

private void importWorkbook(Database db, String fileName, InputStream in) { Workbook wb = null; if (fileName.endsWith(".xlsx") || fileName.endsWith(".xlsm")) { wb = new XSSFWorkbook(in); } else if (fileName.endsWith(".xls")) { wb = new HSSFWorkbook(in); } else { return; } Sheet sheet = wb.getSheetAt(0); for (int r = 1; r <= sheet.getLastRowNum(); r++) { Row row = sheet.getRow(r); if (row == null) { continue; } Document newDoc = db.createDocument(); newDoc.replaceItemValue("Form", "ImportForm"); String bh = getCellString(row.getCell(0)); Double total = getCellNumeric(row.getCell(5)); newDoc.replaceItemValue("fldBH", bh); if (total != null) { newDoc.replaceItemValue("Total", total); } java.util.Date date = getCellDate(row.getCell(3)); if (date != null) { newDoc.replaceItemValue("AppDate", session.createDateTime(date)); } newDoc.save(true, false); } wb.close(); }

逻辑说明:先按扩展名选择 Workbook 实现,只支持 xls 和 xlsx 两种,文件名命中的情况之外一律不处理。sheet.getLastRowNum() 返回的是最后一行索引,如果 Excel 有 100 行数据,这个方法返回 99,所以 for 循环条件写的是小于等于。循环从 r=1 开始,默认第一行是表头,这个约定要在开发文档里写清楚,业务人员给表格时最容易把表头行和标题行混在一起。

参数说明:newDoc.save(true, false) 第一个参数 true 表示允许生成新文档,第二个 false 表示不做冲突处理。正式系统里我建议改成 doc.save(true, true) 并配合冲突检测,否则多人同时导入同一批数据时可能出现覆盖问题。session.createDateTime(date) 是把 Java 的 java.util.Date 转换成 Domino 的 DateTime 类型再存入日期域,直接用 replaceItemValue 传 java.util.Date 会变成字符串。

3.3 单元格类型判断:字符串、数字、日期和科学计数法的处理

POI 读取单元格时不会自动判断类型,你必须先看 cell.getCellType() 再决定取值方式。早期 POI 这里的类型是 int 常量,POI 4.x 之后改成了 CellType 枚举,老代码里的 Cell.CELL_TYPE_STRING 在新版本里编译直接报错。我按新版写,但包了兼容层:

private String getCellString(Cell cell) { if (cell == null) { return ""; } if (cell.getCellType() == CellType.STRING) { return cell.getStringCellValue(); } if (cell.getCellType() == CellType.NUMERIC) { if (DateUtil.isCellDateFormatted(cell)) { return cell.getDateCellValue().toString(); } double v = cell.getNumericCellValue(); if (v == Math.floor(v) && !Double.isInfinite(v)) { return String.valueOf((long) v); } return String.valueOf(v); } return cell.toString(); } private Double getCellNumeric(Cell cell) { if (cell == null) { return null; } if (cell.getCellType() == CellType.NUMERIC) { return cell.getNumericCellValue(); } if (cell.getCellType() == CellType.STRING) { String s = cell.getStringCellValue().trim(); if (s.length() > 0) { return Double.parseDouble(s); } } return null; }

逻辑说明:getCellString 处理了最常见的三种情况。STRING 直接取字符串;NUMERIC 里先用 DateUtil.isCellDateFormatted 判断这个数字单元格是不是日期格式,如果是,应该走 getDateCellValue 而不是取数字,否则你会拿到一组秒数;纯数字再做整数判断,避免 100 变成 100.0。科学计数法的问题也在这里:如果 Excel 单元格显示 1000,但存储的可能是 1E3,直接 toString 会得到 1E3 而不是 1000,所以我用 long 转换把整值还原。

参数说明:Double.parseDouble 在字符串不是合法数字时会抛 NumberFormatException,真实业务里这一步要有 try-catch 并记录行号,不能因为某一行脏数据导致整个导入代理中断。我一般会在循环里维护一个失败列表,最后打印前 50 条错误信息到导入日志,这样业务用户知道哪几行需要人工处理。

大批量导入的性能问题也必须考虑。一万行数据逐行 createDocument + save,代理会跑得非常慢。常见做法是每 500 行做一次批量攒批,Document 用完后及时 recycle,同时用 db.setNoReplication() 在导入期间关闭复制,导入完成后再打开。这招在每天定时跑批导数据时候很好用,能省掉大量复制族日志的时间。

4. LotusScript代理实现同样的导入导出:示例代码与Java代理的选型对比

4.1 LotusScript导出Excel:Print输出与Chr(9)制表符

有些老系统表单逻辑全写在 LotusScript 里,加一个 Java 代理要重新编译部署,这时用 LotusScript 写导出是更快的路子。原理和 Java 代理一样,只是输出方式从 PrintWriter 换成了 LotusScript 的 Print 语句。

Sub Initialize Dim session As New NotesSession Dim ctx As NotesAgentContext Dim db As NotesDatabase Dim view As NotesView Dim doc As NotesDocument Dim strOut As String Set ctx = session.AgentContext Set db = ctx.CurrentDatabase Set view = db.GetView("Documents.vw") Print "Content-Type:application/vnd.ms-excel" Print "单号-1" & Chr(9) & "单号-2" & Chr(9) & "标题" & Chr(9) & "创建时间" & Chr(9) & "作者" & Chr(9) & "金额" Set doc = view.GetFirstDocument While Not doc Is Nothing strOut = doc.GetItemValue("fldYM")(0) & Chr(9) strOut = strOut & doc.GetItemValue("fldBH")(0) & Chr(9) strOut = strOut & doc.GetItemValue("Subject")(0) & Chr(9) strOut = strOut & doc.GetItemValue("AppDate")(0) & Chr(9) strOut = strOut & doc.GetItemValue("Creator")(0) & Chr(9) strOut = strOut & doc.GetItemValue("Total")(0) Print strOut Set doc = view.GetNextDocument(doc) Wend End Sub

逻辑说明:Print 语句在代理里默认输出到当前 HTTP 响应,第一行同样要先写 Content-Type。LotusScript 的 GetItemValue(field)(0) 直接取数组第一个元素,单个文档没问题。注意这里没有做多值域处理,如果业务里存在多值域,要在循环里遍历数组再拼接。

参数说明:Print 自带换行,你不需要在字符串末尾手动加 Chr(13) & Chr(10)。表头那行用 Chr(9) 分隔,浏览器识别 TAB 后自动分列。这里有个细节:LotusScript 代理的 Print 输出编码不受你控制,是服务器 Notes.ini 里的默认字符集,中文在 Linux 服务器上可能出现乱码,我一般会把导出逻辑放到 Java 代理里,LotusScript 只做简单场景。

4.2 LotusScript导入Excel:NotesStream逐行读取与文档创建

LotusScript 没有 POI 这种现成库,直接解析 xlsx 不现实。我实测下来,LotusScript 导入要走一条更原始的路线:先把附件保存到服务器临时目录,用 NotesStream 逐行读取,按分隔符拆分,再创建 NotesDocument。下面这个示例按制表符分隔的文本文件处理:

Sub Initialize Dim session As New NotesSession Dim ctx As NotesAgentContext Dim db As NotesDatabase Dim docCur As NotesDocument Dim docNew As NotesDocument Dim attachment As NotesEmbeddedObject Dim stream As NotesStream Dim strLine As String Dim arrFields() As String Dim i As Integer Set ctx = session.AgentContext Set db = ctx.CurrentDatabase Set docCur = ctx.DocumentContext Set attachment = docCur.GetAttachment("import.tsv") If attachment Is Nothing Then Exit Sub End If ' 释放到Domino数据目录,再通过NotesStream打开 Call attachment.ExtractFile("tmp_import.tsv") Set stream = session.CreateStream If Not stream.Open("tmp_import.tsv") Then Exit Sub End If ' 跳过第一行表头 strLine = stream.ReadLine() strLine = stream.ReadLine() Do While strLine <> "" arrFields = Split(strLine, Chr(9)) Set docNew = db.CreateDocument docNew.Form = "ImportForm" docNew.fldBH = arrFields(0) docNew.Subject = arrFields(2) docNew.Total = Cdb(Val(arrFields(5))) Call docNew.Save(True, False) strLine = stream.ReadLine() Loop Call stream.Close() End Sub

逻辑说明:GetAttachment("import.tsv") 从当前文档取名为 import.tsv 的附件,取不到就直接退出。ExtractFile 把附件释放到 Domino 数据目录下,用的是相对路径,这一步不做的话 NotesStream 找不到物理文件。ReadLine 第一行是表头,第二次 ReadLine 才进入数据行。

参数说明:Split 出来的数组下标从 0 开始,所以 arrFields(0) 对应第一列,arrFields(5) 对应第六列,也就是金额那列。Cdb(Val(arrFields(5))) 先把字符串转成数字再转成 Currency 类型,避免直接把字符串赋给数值域导致类型不匹配。这套方案的边界很明显:只能处理简单文本格式,带格式的 xlsx 还是得用 Java 代理。

4.3 两种代理的选型对比:什么时候用Java,什么时候用LotusScript

对比项Java代理LotusScript代理
启动速度第一次调用要加载 JVM,较慢启动快,适合短小频繁的代理
输出编码控制可通过 PrintWriter 和响应头控制依赖服务器默认字符集
Excel 解析能力POI 完整支持 xls/xlsx只适合解析文本格式
多值域与类型处理灵活,可控性高数组操作繁琐,类型转换易错
代码维护结构化,适合团队协作在 Designer 里编辑,调试困难
典型场景大型报表导出、POI 导入、定时批处理简单导出、表单处理、快速脚本

从血泪经验来看,新写功能优先 Java 代理,尤其涉及 Excel 导入导出。LotusScript 适合维护老逻辑,在不动工程结构的前提下快速补一个导出。别为了追求技术统一强制把成熟的 LotusScript 代理改成 Java,改完大概率一周内会被业务发现导出结果变了。

还有一点要澄清:Domino 的 Java 代理和 Java 动态代理完全是两码事。前者是 Domino 里用 Java 语言编写的代理程序,跑在 Domino 服务器上;后者是 JDK 的 Proxy 类运行时生成代理对象的机制,跟 Domino 没有关系。搜索时这两个词经常混淆,你如果是为了做 Domino 导入导出找资料,认准“Domino Java 代理”这个方向就行。

5. 导入导出排查手记:中文乱码、代理超时与多值域的五个坑

5.1 中文乱码:同一个代理在Windows服务器上正常,Linux服务器上WPS打开乱码

现象:开发环境是 Windows + 本地 Domino,导出正常。部署到 Linux 服务器后,浏览器下载的 Excel 用 WPS 打开表头全是乱码,用 Microsoft Excel 打开反而正常。

原因:Java 代理里 getAgentOutput() 返回的 PrintWriter 编码继承服务器启动时的 JVM 默认字符集。Linux 服务器多为 UTF-8,WPS 默认按 GBK 解析没有 BOM 的文本文件,而 Microsoft Excel 会做编码嗅探。

解决:输出时显式指定响应 charset。我习惯在 Content-Type 里直接带编码:

pw.println("Content-Type:application/vnd.ms-excel;charset=GBK");

如果业务环境是 UTF-8 标准要求,那就改成输出 UTF-8 BOM,让 WPS 强制识别为 UTF-8:

pw.print("\uFEFF");

注意 BOM 只能加在最前面,加到表头中间会让第一列出现不可见字符。从那以后我每次做 Domino 导出,都会先问一句对方的办公软件是 WPS 还是 Office,这个问题的答案直接决定我要不要加 BOM。

5.2 大数据量导出超时:浏览器转圈十分钟,最后报代理请求超时

现象:视图里两万条记录,导出代理跑起来后浏览器一直转圈,服务器控制台没有异常栈,过一会儿前端收到超时或者“服务器无响应”。

原因:printView() 里是一个长循环,所有数据攒在 StringBuilder 里,全部处理完才一次性输出。Domino HTTP 任务对单个请求有超时阈值,超过后宁可杀掉请求也不等代理慢慢跑完。

解决:分批输出并强制 flush,在循环里按批向 PrintWriter 写数据:

int batchSize = 500; Document doc = view.getFirstDocument(); int count = 0; while (doc != null) { printDocInfo(doc); count++; if (count % batchSize == 0) { pw.print(sb.toString()); pw.flush(); sb.setLength(0); } Document temp = view.getNextDocument(doc); doc.recycle(); doc = temp; }

逻辑说明:count 每满 500 条,把 StringBuilder 里已经拼好的内容推到 PrintWriter,并立即清空缓冲区。这样浏览器能持续收到数据,同时也降低服务器内存压力。另外可以把视图里已删除但未归档的文档排除,通过 db.getView 时加选择条件或者在视图定义里过滤。

5.3 多值域导出后只剩第一个值:用户以为是漏数据,其实是代码只取了下标0

现象:某个字段在 Domino 表单里是多值域,比如“参与人员”存了三个名字,导出到 Excel 后单元格里只有第一个名字。

原因:原代码 printDocInfo 里写的是 fldvalue.get(0).toString(),多值域的 Vector 有多个元素,但只取了第一个。

解决:遍历 Vector 全部值,用分号拼接后写入单元格。修复代码在第 2 章已经给过。这类问题难发现是因为日志不报错,没有异常也没有警告,只有业务人员核对数据时才看到不对。我现在的习惯是遇到多值域字段,在导出前先看一遍文档的这个域值数量,再决定输出策略。

5.4 导入xlsx报“NotOLE2FileException”或“头损坏”:扩展名和实际格式不一致

现象:用户上传了一个文件名后缀是 .xls 的文件,POI 解析时抛 org.apache.poi.poifs.filesystem.NotOLE2FileException,或者暴力报“Invalid header signature”。

原因:很多业务系统导出的“Excel”其实是用 CSV 或其他文本格式保存但改了扩展名,文件内容根本不是 OLE2 复合文档。按扩展名选 HSSFWorkbook 时,它去读 POIFS 文件系统头,读不到自然抛异常。

解决:不要只相信扩展名,先探测文件头。POI 自带 FileMagic 工具,按前几个字节识别真正格式:

InputStream bufferedIn = new BufferedInputStream(in); byte[] header = new byte[8]; int read = bufferedIn.read(header); if (read < 8) { return; } if (FileMagic.valueOf(header) == FileMagic.OLE2) { wb = new HSSFWorkbook(bufferedIn); } else if (FileMagic.valueOf(header) == FileMagic.OOXML) { wb = new XSSFWorkbook(bufferedIn); } else { return; }

参数说明:importWorkbook 方法现在只按文件名后缀分流,遇到伪装文件就会翻车。改成先读前 8 字节再做判断,能同时兜住扩展名写错和伪装格式两类问题。如果 FileMagic 都不认识,说明这个文件不是 Excel,直接记录日志返回,不要在 POI 的异常里挣扎。

5.5 LotusScript读附件报“文件无法打开”:attachment.FilePath不是服务器物理路径

现象:LotusScript 代理里写 stream.Open(attachment.FilePath),运行后返回 False,或者抛“文件不存在”。

原因:NotesEmbeddedObject 的 FilePath 属性返回的是文档内的逻辑路径,不是服务器文件系统里的绝对路径。NotesStream.Open 要的是服务器本地文件路径,两者对不上,怎么 Open 都是失败的。

解决:先用 ExtractFile 把附件释放到 Domino 数据目录下的临时文件名,再让 NotesStream 去 Open 那个临时文件。第 4 章示例代码里已经写明了顺序:

Call attachment.ExtractFile("tmp_import.tsv") Set stream = session.CreateStream If Not stream.Open("tmp_import.tsv") Then Exit Sub End If

注意 ExtractFile 写出的临时文件在服务器数据目录下,跑完导入后记得清理,否则每次导入都留一个文件,时间长了会堆积垃圾。我习惯在代理末尾里加一条 Kill "tmp_import.tsv",用后即删。

6. 动态列与真xlsx输出:把TAB分隔导出升级成POI文件

6.1 把列定义做成配置,改字段不再重新编译代理

导出代理里 FIELDS 和 FIELDSNAME 是硬编码数组,表单增加一个字段就要改代码重新部署。我后期做的导出代理会把列定义挪到数据库里,存放一个配置文档,每行记录“显示名|字段名|数据类型”,代理启动时读取并转换成 List ,这样业务加字段时,只需要让管理员在配置文档里改一行,不用碰代码。ColumnDef 很简单:

class ColumnDef { String header; String fieldName; String dataType; // string, number, date }

加载配置后,printDocInfo 里根据 dataType 选择取值和格式化分支:date 类型走 SimpleDateFormat,number 类型做精度处理。这套逻辑的优点在于把经常变的部分从代码里分离出来,缺点是配置错误时问题定位要查数据而不是看代码。

6.2 真正输出xlsx:列宽、多Sheet和下载链接

TAB 分隔的导出虽然快,但它不是真正的 Excel 文件,无法设置列宽、无法分 Sheet、无法做数据格式。如果前端要求这些能力,就得改用 POI 生成 xlsx。做法是代理里创建 XSSFWorkbook,写完后通过 FileOutputStream 保存到服务器临时目录:

String tmpPath = System.getProperty("java.io.tmpdir") + File.separator + "export_" + System.currentTimeMillis() + ".xlsx"; try (FileOutputStream fos = new FileOutputStream(tmpPath)) { workbook.write(fos); }

逻辑说明:Domino Java 代理直接输出二进制流有限制,getAgentOutput() 的 PrintWriter 按字符流处理,写二进制文件很容易被编码转换破坏。所以先落盘到临时文件,再给浏览器返回一个下载地址,或者把文件作为附件挂到当前文档里让用户另存。这个方案牺牲了一点响应速度,换来了 xlsx 的完整能力。

设置列宽的姿势值得记一下:sheet.setColumnWidth(index, width) 的单位是 1/256 像素,比较反人类,宽度 20 要写成 20 * 256,也就是 5120。每次写这个都容易算错,建议封装成一个方法:

private void setColumnWidths(Sheet sheet, int[] widths) { for (int i = 0; i < widths.length; i++) { sheet.setColumnWidth(i, widths[i] * 256); } }

从那以后,我每次给 Domino 做 Excel 导入导出,都先确认三件事:输出编码能不能被对方办公软件识别,数据量会不会把代理拖到超时,Notes 对象有没有按顺序回收。这三条都过,基本就不会在线上返工。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询