简介:面向用友NC65平台开发新手与初中级工程师,这份《NC65开发常见API详解》是一份PDF格式的API使用手册,按实际开发场景整理了二十余类高频操作,包含获取选中表体行/列数、设置界面默认值、表单默认执行方法、报表合计行显示、UI小数位控制、表体清空、字段编辑权限、查询条件打印、提示框弹出、查询面板取值、时间比较、编辑公式、缓冲数据清理、查询对话框默认值、单据类继承关系、行号与合计行列显隐、状态驱动按钮可用性、UI工厂自定义按钮、动作脚本按钮设置、字段显示隐藏、单据开发步骤、界面数据访问、数据库导入导出以及List/Map/Set操作等典型示例。文档按问题分节,每个知识点均提供简明方法说明与可直接参考的Java代码,便于开发者在NC65单据或报表开发中快速定位并复用。压缩包仅含1个PDF文件,约193KB,小巧易保存。已有579人浏览学习,适合需要系统梳理NC65常见API用法的新手对照练习,也能帮助初中级工程师提升编码效率、减少查阅时间。
1. NC65 开发常见 API:为什么新手第一个月都在跟 ClassNotFound 搏斗
NC65 二次开发听着是写 Java,实际上一半时间在跟它的 classloader 和 API 命名空间搏斗。新手刚接触,常常被ClassNotFoundException和下划线开头的内部方法搞到怀疑人生。这篇笔记不聊虚的,直接围绕单据保存、按钮事件、数据查询、审批流这些占了日常 80% 需求的高频场景,把 NC65 开发里最常用的 API 用法拆开讲。每个小节都给出能直接落地的代码写法,并说清参数从哪来、返回值怎么拿、哪些坑是版本差异造成的。适合刚接手 NC65 项目、正在写第一个单据功能,或者还在评估要不要选这个平台做二次开发的朋友。
2. 打好地基:NC65 模块依赖与 API 的三大分类
2.1 API 的“三兄弟”:客户端、服务端与公共模块,引错 jar 必翻车
NC65 本身是个庞大的体系,它的 API 按部署位置可以粗分成三块。第一块是客户端 API,跑在用户桌面或浏览器端,负责界面交互、按钮响应、卡片数据组装;第二块是服务端 API,跑在应用服务器上,负责业务逻辑、数据库事务、审批流驱动;第三块是公共 API,像工具类、常量定义、异常基类,两端都会引用。这三兄弟最典型的区别是依赖范围不同。客户端模块通常依赖公共模块,服务端模块也依赖公共模块,但客户端模块一般不要去依赖服务端模块。
很多新手在 IDE 里为了图方便,把nc.bs.*、nc.itf.*、nc.ui.*的 jar 一股脑全加进编译路径,结果编译期一切正常,部署到NCHome下一启动就报错。原因就是 NC65 运行时是按模块的module.xml来加载类的,没声明依赖的模块,类加载器在运行期根本找不到。下面这个module.xml片段是标准写法,注意你新增的模块 ID 和名称要跟实际目录对应。
<!-- 在 NC65 模块的 module.xml 中声明依赖,这是客户化模块的基石 --> <module> <!-- 这里填你的模块 ID,通常是小组件编码,比如 5001 --> <id>5001</id> <!-- 模块显示名称,自己看得懂就行 --> <name>customer_po</name> <dependency> <!-- 公共模块:提供基础工具类、异常基类 --> <id>nc.uap.pub</id> <type>module</type> </dependency> <dependency> <!-- 服务端框架:提供 NCLocator、事务管理 --> <id>nc.bs.framework</id> <type>module</type> </dependency> <dependency> <!-- 单据模板 UI 基础:提供 AbstractAction 等 --> <id>nc.uap.qbd</id> <type>module</type> </dependency> </module>逻辑说明:<dependency>里写的<id>是 NC65 平台内部模块的唯一标识。比如你要写一个自定义按钮,就必须依赖 UI 基础模块;要调用服务端查询接口,就必须依赖框架模块。如果漏了依赖,IDE 里能用 ctrl+鼠标点进源码,但部署后运行到那一行就会直接抛NoClassDefFoundError,这是新手最常见的第一个“翻车”现场。
参数说明:<type>固定写module,不要写成jar。NC65 的模块体系是以module为粒度做版本控制和类加载的,写jar则意味着你要手工指定一个 jar 文件,这在标准开发里几乎用不到。
2.2 NCHome 目录与环境变量:为什么你改的代码“没生效”
NC65 启动时会读取NCHome环境变量,所有的 jar 包、配置文件、日志输出都以它为根。很多新手在 IDE 里配好了 JDK 和 Tomcat,但忘了设NCHome,结果启动后加载的是 IDE 自带的临时目录,改动代码永远不生效。我一般会在启动脚本里显式加上-DNCHome=/opt/nc65这种参数,这样即使服务器上有多个 NC65 实例,也不会互相干扰。
# 设置 NCHome 环境变量并启动调试模式 export NCHome=/opt/nc65 export JAVA_HOME=/usr/java/jdk1.8 export PATH=$JAVA_HOME/bin:$PATH # NC65 的调试端口默认是 8787,如果被占用了可以自行修改 # suspend=n 表示不阻塞启动流程,等 IDE 的远程调试客户端主动连上来 java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8787 \ -DNC_HOME=$NCHome \ -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager \ -classpath "$NCHome/bin" nc.bs.startup.Startup逻辑说明:这里启动的是 NC65 的服务端进程。-Xdebug和-Xrunjdwp是 Java 标准的远程调试参数,配好后就能在 IDE 里打断点调试服务端代码。nc.bs.startup.Startup是 NC65 服务端的入口类,日常开发调试基本都从它拉起。
参数说明:-DNC_HOME显式指定安装根目录,注意这里的环境变量名是NC_HOME,大小写都兼容,但建议保持一致。-classpath里只写了$NCHome/bin,是因为 NC65 启动器会自动扫描lib、ext-lib、modules下的 jar,不需要手工把所有 jar 都塞进来。
2.3 lib 与 ext-lib:放错位置的 jar 比不写依赖更致命
NCHome下有两个存放第三方 jar 的目录:lib和ext-lib。lib是 NC65 平台自带的第三方库,比如 Spring、C3P0、commons-lang,这些是平台核心,版本一般不要动。ext-lib是留给客户化二次开发用的扩展目录,你引入的第三方 jar(比如 fastjson、poi、httpclient)应该放在这里。
我见过一个真实案例:有人把 fastjson 的 jar 复制到了lib下,结果因为版本太老,导致平台自带的 JSON 序列化全部出错,接口返回的数据全都变成了乱码。排查了一整天,最后把 jar 从lib移回ext-lib,立刻恢复。所以记住一句话:平台自己的lib目录,不到万不得已不要碰。ext-lib才是你的地盘。另外,放进ext-lib后要重启服务端才能生效,没有热加载这种说法。
3. 服务端 API 实战:单据保存、查询与审批的代码模板
3.1 用 NCLocator 找服务:别再用 new 关键字搞业务对象了
NC65 的服务端 API 大量使用了类似 Spring 的容器管理思路。你如果直接new一个BillSave或IFxxxService的实例,事务、数据源、日志上下文全部拿不到,轻则报空指针,重则造成事务不提交,数据写一半。正确的姿势是通过NCLocator从容器里按接口类型查找实现类。
// 获取一个服务端业务接口的实现 import nc.bs.framework.common.NCLocator; import nc.itf.uap.IUAPQueryBS; // NCLocator.lookup() 会根据当前线程上下文(数据源、组织、语言)返回代理对象 IUAPQueryBS queryBS = NCLocator.getInstance().lookup(IUAPQueryBS.class); // 执行一条 SQL,注意用问号占位符,不要拼接字符串,避免 SQL 注入 Object[][] result = queryBS.executeQuery( "select pk_billtype, typename from bd_billtype where pk_billtype = ?", new String[]{"30"} );逻辑说明:NCLocator.lookup()的入参是接口类的Class对象,返回值是接口的代理实现。代理会帮你处理数据源切换、事务上下文传递,这些细节如果全都靠手工new是搞不定的。这里拿IUAPQueryBS举例,它是 NC65 底层元数据查询的公共入口,凡是查数据库表都很方便。
参数说明:executeQuery的第一个参数是 SQL,第二个参数是占位符对应的字符串数组。注意返回值是二维数组Object[][],行是记录数,列是字段值。如果查询结果为空,返回的是空数组而不是null,遍历前最好判一下长度。这里查询条件里的?占位符不要省略,NC65 底层会走 preparedStatement,能防止 SQL 注入。
3.2 单据保存的标准动作:主键、时间戳、AggVO 一个都不能少
保存单据是 NC65 里最高频的操作。新手最容易翻车的地方是主键没有生成、ts时间戳没有赋值,导致保存成功后列表界面缓存不刷新。下面这段代码是单据保存的基操,我一般会把它封装成一个公共方法,所有业务模块都复用。
import nc.vo.pub.lang.UFDateTime; import nc.bs.framework.common.NCLocator; import nc.itf.uap.IUAPBillBS; import nc.vo.pubapp.pattern.pub.Constructor; import nc.vo.pubapp.pattern.model.entity.bill.AbstractBill; // 以采购订单为例,billVO 通常是继承了 AbstractBill 的聚合 VO public void saveBill(AbstractBill billVO) { // 1. 生成主键。第二个参数是单据编码,比如采购订单是 "PO" String pk = Constructor.createPK("PO"); billVO.getParentVO().setPk_bill(pk); // 2. 给时间戳字段赋值。ts 是 NC65 的乐观锁字段,列表缓存依赖它判断数据变化 // 如果 ts 为 null,缓存会认为这条数据不存在,保存完列表刷不出来 billVO.getParentVO().setTs(new UFDateTime()); // 3. 通过服务端接口保存。IUAPBillBS 内部会判断是插入还是更新 IUAPBillBS billBS = NCLocator.getInstance().lookup(IUAPBillBS.class); billBS.save(billVO); }逻辑说明:第 1 步的Constructor.createPK是 NC65 统一的主键生成器,第二个参数是单据编码,对应的表主键字段会拿到这个值。我特意不用PrimaryKeyGenerator.generatePK,因为Constructor这个类在实体框架里更通用,兼容性最好。第 2 步的ts是乐观锁字段,很多列表查询和缓存刷新都依赖它,不赋值会出现“保存成功但列表查不到”的灵异事件。第 3 步的IUAPBillBS是单据操作的服务端接口,save方法内部会处理插入或更新的判断。
参数说明:UFDateTime是 NC65 自定义的时间类型,不要用java.util.Date,因为底层数据库方言适配时会有类型转换问题。setPk_bill是父 VO 的主键 setter,如果你的单据是主子表结构,表体 VO 不需要手动设主键,框架会根据父主键自动生成外键关联。这里要特别注意:Constructor.createPK的编码参数必须跟单据模板里的 billcode 保持一致,否则跨模块引用时可能生成重复主键。
3.3 查询 API:QueryUtil 与 SmartService 怎么选
NC65 里查询数据有两条路。一条是直接查数据库,用QueryUtil或IUAPQueryBS;另一条是走业务模型,用SmartService带权限和缓存地查。新手经常把这两条路混着用,结果看到奇怪的数据权限问题。直查数据库绕过权限体系,适合后台定时任务和统计报表;界面上的单据列表查询必须走 SmartService,否则下属机构的人能看到全集团的数据,这是重大的数据安全漏洞。
// 第一种:直接查库,适合报表和明细数据 import nc.bs.pub.util.QueryUtil; // 注意 dr = 0 是 NC65 的逻辑删除标记,查询条件里必须带上 java.util.List<Object[]> list = QueryUtil.executeQuery( "select pk_bill, bill_no from your_table where dr = 0 and pk_group = ?", new Object[]{"1001"} ); // 第二种:走 SmartService,自动带上组织权限,适合界面列表 import nc.bs.sm.SmartService; SmartService smartService = new SmartService(); // 设置组织权限范围,如果不设置,会用当前线程上下文里的组织,容易误伤 smartService.setPk_group("1001"); // queryByCondition 的第一个参数是单据编码,第二个是 HQL 风格条件字符串 Object[] vos = smartService.queryByCondition("PO", "bill_no like '%XS%' and dr = 0");逻辑说明:第一种方式直接拼 SQL,性能高但绕过了权限体系,适合内部统计或定时任务批量扫描。第二种方式通过SmartService,会结合当前操作员的组织权限过滤数据,适合做界面上的单据列表查询。两者的共同点是条件里都要带上dr = 0,否则会把已删除的脏数据查出来。另外SmartService返回的对象数组里每个元素是一个 VO 示例,可以直接用 getter 取字段,比二维数组更直观。
参数说明:QueryUtil.executeQuery返回List<Object[]>,第二个参数是占位符数组,类型是Object[],这里和IUAPQueryBS的String[]不一样,传入数字时要包装成Integer。SmartService.queryByCondition的第一个参数是单据编码或 VO 名,第二个是 HQL 风格的条件字符串,setPk_group是手动指定组织,不指定则用当前线程上下文里的组织。
提示:在
SmartService里写条件字符串时,字段名要用 VO 的属性名(驼峰命名),不是数据库字段的下划线命名。比如数据库列是bill_no,VO 属性是billNo,条件里写billNo like '%XS%'才对。写错不会报错,但查出来是空集合,最容易误导排查方向。
3.4 审批流 API:驱动工作流别自己去改状态字段
有个很常见的错误做法是,新手为了省事,直接 update 单据表里的审批状态字段,比如把approvestatus从 0 改成 1。这么做虽然数据库里变了,但审批流引擎完全不知道,导致后续的审批记录、消息通知、反审核全部错乱。正确做法是调用工作流 API。审批状态是流程引擎在驱动,不是你的 SQL 在驱动,你把状态值改了,相当于骗过了业务表但骗不过流程引擎,下游节点全都不认。
import nc.bs.workflow.WorkFlowManager; import nc.vo.pub.BusinessException; import nc.vo.pub.lang.UFDateTime; public void approveBill(String billId, String operatorId) { WorkFlowManager wfm = new WorkFlowManager(); try { // 参数说明:operatorId 是当前操作员的 user_id,billId 是要审批的单据主键 // 第三个参数是动作标识:approve 表示通过,unaudit 表示弃审,reject 表示驳回 wfm.approve(operatorId, billId, "approve", "审批通过"); } catch (Exception e) { // 审批失败要抛业务异常,不能让 UI 层以为成功了,否则客户端会显示成功但流程没走 throw new BusinessException("审批失败:" + e.getMessage()); } }逻辑说明:WorkFlowManager是 NC65 审批流驱动的入口。参数里的billId是要审批的单据主键,operatorId是当前操作员的用户 ID,不能写死。审批动作如果直接抛Exception,客户端会看到一大段英文堆栈,不友好;抛BusinessException则能控制提示信息,这是 NC65 的约定。另外,审批动作本身是异步还是同步,取决于流程配置。如果流程里配了“提交后自动审批”,那么调用approve后状态可能不会立刻变成已审核,可以考虑在循环里轮询状态,或者查流程引擎的任务表。
参数说明:第一个参数是当前操作员的user_id,可以通过InvocationInfoProxy.getInstance().getUserId()获取,不要用硬编码的测试账号。动作字段approve表示通过,unaudit表示弃审,reject表示驳回。第四个参数是审批意见,会写入审批记录表,前端审批历史里能看到。
4. 客户端 UI API 实战:按钮事件与单据交互这样写才不“黑匣子”
4.1 客户端按钮事件:自定义按钮为什么要继承 AbstractAction
NC65 的单据模板上有六大标准按钮(新增、修改、保存、删除、审批、弃审),但实际项目里经常要加自定义按钮,比如“生成请购单”“推送外部系统”。这些自定义按钮的点击逻辑在客户端 UI 里,需要继承AbstractAction类。注意,这里的AbstractAction是nc.ui.pubapp.uif2app.actions包下的,不要引成 Swing 或 AWT 的Action接口。
import nc.ui.pubapp.uif2app.actions.AbstractAction; import nc.vo.pub.BusinessException; import nc.ui.pub.bill.BillCardPanel; // 这是一个生成下游采购订单的自定义按钮动作 public class GeneratePOAction extends AbstractAction { @Override public void doAction() throws BusinessException { // 1. 拿到当前卡片编辑面板 BillCardPanel cardPanel = getBillCardPanel(); // 2. 获取表头 VO,这里以 PurchaseOrderVO 为例,实际要替换成你的单据 VO PurchaseOrderVO headVO = (PurchaseOrderVO) cardPanel.getHeadVO(); // 3. 业务逻辑:校验或调用服务端 if (headVO.getBillstatus() != null && headVO.getBillstatus() == 1) { throw new BusinessException("已审核单据不能重复生成!"); } // 4. 这里调用服务端接口生成下游单据,省略具体代码 // 5. 最后刷新卡片数据,让用户看到新增的子表行 cardPanel.refresh(); } }逻辑说明:getBillCardPanel()是AbstractAction提供的方法,能拿到当前操作的单据卡片面板。通过getHeadVO()取表头数据,getBodyVO()取表体数据数组。自定义按钮要生效,必须在模块的 UI 配置里把按钮的action类指向这个类。另外要注意,AbstractAction还有两个可以重写的钩子方法:beforeDoAction()和afterDoAction()。beforeDoAction常用于二次确认,返回false可以中断后续动作;afterDoAction适合做日志记录。
参数说明:BillCardPanel是 NC65 客户端最核心的控件类,它封装了表头、表体、卡片状态(浏览/编辑/新增)等逻辑。getHeadVO()返回的是Object类型,所以这里需要强转成你的具体 VO。billstatus字段类型是Integer,判断时要注意 NPE(空指针异常),最好先判空,这也是经验之谈。
4.2 卡片面板取数与赋值:不要直接操作界面控件
新手常见做法是先getComponent("pk_dept")拿到输入框组件再getValue(),这样写不仅代码冗余,而且遇到权限编辑、不可编辑状态时经常拿不到值。NC65 更推荐直接用 VO 的 getter 取数,用 setter 赋值后调用updateVO回显。这个原则贯穿整个 NC65 UI 开发:界面只是 VO 的投影,数据模型才是本体。
// 在编辑事件中修改表头部门字段并回显 import nc.ui.pub.bill.BillCardPanel; BillCardPanel cardPanel = getBillCardPanel(); PurchaseOrderVO headVO = (PurchaseOrderVO) cardPanel.getHeadVO(); // 修改值之前先备份旧值,方便做脏数据回滚 // getAttributeValue 是根据字段名反射取值,适合写通用代码时用 Object oldDept = headVO.getAttributeValue("pk_dept"); headVO.setPk_dept("1001A1100000000001"); // 回显到界面,触发控件刷新 cardPanel.updateVO(headVO);逻辑说明:getAttributeValue是根据字段名反射取值,适合写通用代码时用;setPk_dept是具体 VO 的 setter,性能更好。updateVO会触发界面控件刷新,把新值显示出来,同时标记该字段为脏数据,用户点保存时才能正确比对。如果不调updateVO,只是调了setPk_dept,内存里的 VO 变了,但界面输入框不会变,用户会以为自己没选中。
参数说明:pk_dept是部门主键字段,实际开发时要去 NC 元数据管理器里确认你用的字段名,不要凭感觉猜。字段名写错时updateVO不会报错,但界面不会刷新,容易让人误以为代码没生效。另外,getAttributeValue传入是数据库字段名还是 VO 属性名,取决于元数据定义,建议先翻一下你实体类里的属性名。
4.3 提示与异常:BusinessException 是给用户看的,不是给你打印堆栈用的
在客户端 UI 里,如果代码直接抛出RuntimeException,NC65 框架会弹出一个英文的、包含完整堆栈的对话框,新手看着怕,用户看着烦。正确做法是手动捕获业务异常,然后抛出BusinessException,它会被框架统一拦截,并弹出中文业务提示。客户端 UI 的异常拦截器只认BusinessException,其他异常都会被当成系统错误处理。
import nc.ui.pubapp.uif2app.actions.AbstractAction; import nc.vo.pub.BusinessException; import nc.ui.pub.bill.BillCardPanel; public class ApproveAction extends AbstractAction { @Override public void doAction() throws BusinessException { try { // 调用服务端审批接口,这里简化了,实际需要从面板取数 getBillCardPanel().getBillModel().approve(); } catch (Exception e) { // 这里把底层异常包装成业务异常,提示语要写人能看懂的话 // 原始异常的堆栈会打印到 NCHome/logs/client.log,方便排查 throw new BusinessException("审批失败,请检查单据是否已提交或当前操作员是否有权限"); } } }逻辑说明:getBillModel().approve()是客户端内置的审批方法,它会同步触发服务端流程。加上这一层 try-catch 后,任何底层异常都会被转换成简洁提示,同时原始异常可以通过e.printStackTrace()打印到 NC 日志里,方便排查。记住一条铁律:UI 层永远不要向上抛非业务异常,因为你不知道框架会怎么处理它,大概率是一个很不友好的模态框。
参数说明:BusinessException的构造函数接受字符串,支持在 UI 层直接弹出;如果要携带异常链,可以用new BusinessException(msg, cause)。这样cause里保留了原始异常信息,后端的日志链路能串起来。
5. 避坑指南:NC65 API 开发中 5 个让老手也翻车的细节
5.1 现象:ClassNotFoundException: org.apache.commons.lang3.StringUtils
原因:模块依赖声明不完整。module.xml里没有声明nc.uap.pub或commons-lang3所在的模块,导致运行时 classloader 找不到类。这种情况在本地 IDE 运行时偶尔正常,因为 IDE 把整个lib目录都加载了,但部署到独立 NC65 环境就暴露。
解决:打开module.xml,在<dependency>节点里补上对应模块的<id>。如果实在不知道是哪个模块提供的,可以在NCHome/lib下搜一下 jar 包名,再把 jar 对应的模块 id 添加进来。我一般用find /opt/nc65 -name "commons-lang3*.jar"先定位,再用unzip -p查看META-INF/MANIFEST.MF里的模块标识。
5.2 现象:单据保存成功后,列表界面查询不到这条数据
原因:保存时没有给ts(时间戳)字段赋值,或者主键生成策略不正确,导致缓存服务比对数据时认为这是一个无效记录。这属于 NC65 的“缓存一致性”坑。列表界面通常走的是SmartService,它内部有一个 5 分钟的缓存,比对数据是否更新的依据就是这个ts字段。如果ts是null,缓存直接丢弃这条记录。
解决:保存前统一调用Constructor.createPK()生成主键,并setTs(new UFDateTime())。这段逻辑在 3.2 节代码里已经注明,强烈建议封装成一个公共方法,所有单据保存都走它。不要嫌麻烦,这个坑我已经在项目里遇到不下五次,每次都是新人踩完老人踩。
5.3 现象:自定义按钮点击后一点反应都没有,控制台也不报错
原因:按钮的action类路径配置错误,或者按钮的interceptor拦截器把事件吞掉了。NC65 客户端按钮不是简单绑定一个 click 事件,它有一套事件分发机制。按钮配置里除了action,还可以配置interceptor。拦截器的beforeAction方法如果返回了false,后面所有动作都不执行,而且不会弹任何提示,看起来就像按钮坏了。
解决:检查按钮配置里的action属性是否完整包名+类名;检查是否有全局拦截器拦截了beforeAction并返回了false。可以通过在doAction第一行加System.out.println("action start")来判断类是否被加载。如果输出看到了,但后续没反应,就把拦截器先摘掉再试。
5.4 现象:SQL 查询报“列名无效”或“ORA-00904”
原因:你查的字段在数据库表里不存在,或者 NC65 的元数据缓存没有刷新,导致系统生成的 SQL 与实际表结构不一致。这种情况尤其在新增自定义字段后出现。NC65 的元数据(Metadata)是存在数据库里的,服务器启动时会加载到内存,但如果你直接改了数据库表结构(比如alter table加了一列),服务器内存里的元数据还是旧的。
解决:先在数据库客户端里执行select * from your_table where 1=0,确认字段在哪个表里。然后在NCHome下删除temp目录,重启服务,让元数据缓存重新加载。如果还是不识别,就去“元数据管理”节点把对应实体重新部署一遍。注意,temp目录删了之后首次启动会慢一些,因为要重新构建各种缓存索引,这是正常现象。
5.5 现象:调用外部 REST 接口时提示“会话已过期”或“未登录”
原因:外部系统调用 NC65 接口时,没有在请求头里携带有效的会话凭证。NC65 的接口鉴权默认是基于 Session 的,外部系统需要先调用登录接口拿JSESSIONID或 Token。很多新手只调了业务接口,没走登录流程,当然会被拒。
解决:在调用方代码里,使用 HttpURLConnection 或 HttpClient 时,必须手动把登录接口返回的 Cookie 存放在请求头Cookie: JSESSIONID=xxx里。如果是服务端到服务端的调用,可以用InvocationInfoProxy临时模拟一个系统管理员上下文,但生产环境这么做会有审计风险,建议还是走正式鉴权。另外,NC65 有单点登录(SSO)体系,如果你们已经接了统一身份认证,可以直接申请一个应用凭证走 SSO 接口,比手动维护 Session 可靠得多。
6. 调试与进阶:如何用远程调试和日志定位快速吃透 NC65 API
6.1 远程调试:用 IDE 打断点,告别 System.out 猜谜
把 2.2 节的启动参数配好后,在 IDE 里新建一个 Remote 调试配置,主机填服务器 IP,端口填8787。这样打断点后能看到NCLocator.lookup返回的代理对象内部属性,排查“为什么会走到这个实现类”这种问题非常高效。以前我为了查一个客户化模块的类加载顺序,硬是在代码里加了几十行System.out.println,后来发现用调试器看ClassLoader的层级清晰得多。远程调试要注意:服务器上的代码版本必须跟本地一致,否则断点位置会偏移,误入歧途。
6.2 日志定位:NC 日志文件怎么快速定位到自己的异常
NC65 的日志默认输出到$NCHome/logs目录,常见的有nc.log(服务端日志)、client.log(客户端日志)。如果界面弹了异常框,但没打堆栈,去client.log里按时间点搜ERROR就行。我一般会写一个统一的日志封装,在 catch 块里写Logger.error(e.getMessage(), e),保证堆栈完整。比System.out强的地方在于,日志文件里带了精确到毫秒的时间戳和线程 ID,能还原当时的调用上下文。
# 快速查看最近 30 分钟的报错日志,关键字搜 ERROR grep "ERROR" /opt/nc65/logs/nc.log | tail -200 # 如果想要的是关于某个单号的完整链路,直接搜业务主键 grep "your_billcode" /opt/nc65/logs/nc.log | head -20逻辑说明:grep是 Linux 下的文本搜索命令,tail -200表示取最后 200 行。这里先用ERROR过滤出全部错误,再根据业务单号缩小范围。NC65 的日志默认按天滚动,查历史问题要记得切到对应的日期文件,比如nc.log.2025-10-15。
6.3 用 Arthas 查看类加载情况:本地能跑,服务器翻车的救命稻草
当你怀疑“代码改了但没生效”时,JConsole 可以看到各个 ClassLoader 加载了哪些 jar。更高级一点,用 Arthas 的sc -d命令直接查看某个类是从哪个 jar 加载的。这个方法在排查多版本 jar 冲突时是救命稻草。曾经遇到过一个项目,commons-beanutils在lib下有两个版本,运行时加载了旧版,导致反射赋值全部失败,用 Arthas 查了类加载器来源才定位到问题。
# 使用 Arthas 远程诊断 NC65 进程 # 先找到 NC65 的 Java 进程 PID,一般用 jps 或者 tomcat 的进程号 java -jar arthas-boot.jar 12345 # 查看指定类是从哪个 jar 加载的,确认运行期用的是不是你的最新代码 sc -d nc.bs.framework.common.NCLocator # 反编译查看类的实际字节码,对比源码,确认服务器上跑的到底是不是你刚编译的版本 jad nc.bs.framework.common.NCLocator逻辑说明:12345是 NC65 服务端进程的 PID。sc -d会输出类的包名、加载器、代码来源。jad反编译可以对比字节码和源码,确定服务器上跑的到底是不是你刚编译的版本。这一招对排查“本地能跑,服务器上翻车”的问题特别管用。注意,jad命令在 Arthas 里是内嵌的,不需要额外安装插件,但反编译出来的代码是简化版,不是绝对还原。
之前我带过几个新人,他们总喜欢把问题怪到“NC 框架太坑”上,但最后查出来 80% 都是主键没生成、ts没赋值、模块依赖漏声明这三个原因。NC65 的 API 其实不复杂,复杂的是它强约束的开发约定。把这些约定刻进脑子,开发效率能翻一倍。希望帮到你。
本文还有配套的精品资源,点击获取