简介:面向用友 NC/NCC 系统的客开人员,这份 docx 文档系统讲解业务插件的注册与开发方法,用于解决单据在新增、修改、删除、刷新、查询、审批、取消审批、签字等操作前后需要执行自定义业务逻辑的常见客开需求。相比直接修改动作脚本或单据接口类,在标准功能基础上挂接插件更安全,能有效降低对系统原有逻辑的影响,同一个业务插件还可同时处理多种操作,灵活高效。资源为单个 docx 文件,压缩包仅 92KB,便于随时查阅。文档从业务插件注册节点入手,给出实现 IBusinessListener 接口的完整 Java 代码示例(如 CubeDefUpdateListener),并演示如何通过 BDCommonEvent、BusinessEvent 获取对象数据,以及如何按 insert、delete、update 等事件类型进行分支判断与处理;对于插件注册位置、事件数据获取、多操作复用等关键点都有涉及,可快速套用到实际客开任务中。已有 310 人学习下载,适合正在从事 NC 客开或计划系统学习插件机制的开发者参考。 做用友NC系统二次开发这几年,我最大的感触是,一说起业务插件注册开发,很多人的理解其实停留在“写个类丢进去”这个层面。有人以为写好代码就万事大吉,有人以为插件不过是个菜单配置,更有不少人卡在同一个环节——代码写完了,注册信息也写了,重启之后界面完全没反应,于是开始怀疑人生。这篇文章不绕弯子,直接把NC系统里业务插件注册开发这件事,从原理到实操一条线讲透。内容里不会堆理论,更多的是我踩过坑之后沉淀下来的判断方法和操作细节,希望能帮你少走一段弯路。适合刚接手NC二次开发、被插件注册问题反复折磨、或者正在NC平台上规划扩展功能的人看。
1. 先搞明白注册到底在注册什么
1.1 NC里的插件是平台留好的挂点
要理解注册,首先得明白NC的插件到底是什么。NC内部有大量标准功能模块,比如采购订单、销售订单、库存管理、财务凭证、审批流、报表,这些功能都跑在底层UAP平台上。平台在运行时有非常固定的流程节点:打开单据、编辑字段、保存数据、审批通过、打印输出、列表加载,等等。平台不会在每个环节都对外开放,但确实在一批关键节点上预留了“挂点”,也就是开发文档里常说的扩展点。所谓业务插件,就是针对某个挂点补齐一个处理类,然后通过描述文件告诉平台“在这个节点请调用我这个类”。这个“告诉平台”的动作,就是注册的真正含义。
很多新手把插件和改标准源代码搞混。改源代码意味着你把平台自带的行为改掉了,后续平台升级打补丁,你的修改极大可能被覆盖,而且出了问题很难界定是平台问题还是扩展问题。插件注册则是把自定义代码隔离在标准代码之外,平台只在约定的时机自动调度。这种方式不只是维护方便,更重要的是它遵循了平台的扩展机制,后续升级时风险小得多。NC的插件机制还有一个好处:同一个挂点上可以挂多个插件,通过顺序配置控制执行先后,互不干扰,这是标准源码改造做不到的。
1.2 注册必须说清的三件事:模块、插件点、顺序
一张注册信息其实要表达三个关键点。第一是目标模块,告诉平台这组插件归属哪个业务模块,避免全局扫描。比如某个校验逻辑只对销售订单生效,那它就挂在销售模块下,而不是整个NC系统每个模块都去加载。模块归属错了,最典型的表现就是注册信息能读到,但目标节点上就是不触发。第二是插件点,告诉平台在哪个生命周期调用,比如“打开单据后”“保存前”“按钮点击后”。插件点选择错误,是插件不生效的最常见原因,没有之一。第三是执行顺序,同一延伸点上挂了多个插件时,顺序号小的先执行,后执行的依赖前面结果的必须把顺序排对,否则数据状态和预期完全对不上。
为了让你快速建立选型概念,我把三类常见的插件挂在哪个插件点、适用于什么场景做了一个对比。
| 插件类型 | 典型插件点 | 常见用途 | 生效范围 |
|---|---|---|---|
| 客户端按钮插件 | 单据按钮点击事件 | 自定义校验、调用后台服务、扩展动作 | 单据卡片界面 |
| 单据生命周期插件 | 保存前、保存后、审批后 | 数据加工、日志记录、关联生成其他单据 | 单据保存与审批流程 |
| 列表插件 | 列表加载、行选中 | 列表字段联动、按钮显隐 | 查询列表界面 |
2. 环境准备与工程结构,少走弯路的配置
2.1 开发环境的搭建思路
开发NC业务插件,核心不是代码本身,而是环境是否贴近真实运行环境。常见组合是JDK1.8、官方提供的Eclipse开发环境、NCHome中间件,再加一个可访问的数据库。很多人环境搭不起来,是因为图省事只在自己的IDE里写代码,本地没有一个可启动的NC中间件,代码写完只能靠肉眼检查,无法跑通真实的类加载和插件扫描。这里我必须强调:插件注册最终看的是运行时的扫描结果,而运行时环境的类加载、jar依赖、组件版本全都影响注册能否成功。所以不要节省这个步骤,准备一个独立的NCHome,配置成开发模式,既能实时看日志,又能随时重启验证,后面能省下大把排查时间。
2.2 工程目录和依赖组织
NC插件工程的目录结构并不复杂,但很讲究。通常src目录下放Java源码,resources目录下放META-INF/plugin.xml描述文件,编译后的class文件会对应部署到NCHome的模块目录里。依赖管理是很多人踩坑的地方——别把NCHome下的大量jar复制到自己的工程lib里,而是直接在开发工具的classpath里引用NCHome路径下的jar。我接手过一个项目,前同事把上百个jar复制到工程的lib目录,导致每次平台升级都要比对版本,后来统一改成只引用NCHome路径,版本冲突问题少了很多。这看起来是小事,但实际上决定了你后面注册能否顺利,也是插件更新维护时能不能快速定位差异的关键。
我常用的工程结构大概是这样的:
stock-plugin/ ├── src/ │ ├── nc/demo/plugin/ │ │ ├── CheckStockButtonPlugin.java │ │ └── StockSaveListener.java │ └── resources/ │ └── META-INF/ │ └── plugin.xml └── classes/ # 编译输出,部署时合并到NCHome对应模块2.3 plugin.xml:插件注册的“身份证”
plugin.xml是插件注册信息的载体,决定了平台在启动时能不能发现、加载并调用你的插件。一个最小化的描述文件大概长这样。需要说明的是,不同NC版本的插件点名称有差异,如果你用的版本对不上,以当前版本的扩展点清单为准,但整体结构和注册思路是一致的。
<?xml version="1.0" encoding="UTF-8"?> <plugin> <id>nc.demo.stockplugin</id> <version>1.0</version> <category>client</category> <module>demo</module> <description>库存校验按钮插件</description> <extension point="nc.uif2.bill.button"> <button id="btn_check_stock" name="库存校验" class="nc.demo.plugin.CheckStockButtonPlugin"/> </extension> <extension point="nc.uif2.bill.listener"> <listener id="lst_stock_save" class="nc.demo.plugin.StockSaveListener"/> </extension> </plugin>注意,这个文件不是随便放的。它要被平台模块扫描到,编译后要放在对应模块的META-INF目录下,和模块原有的plugin描述文件放在一起。我曾经遇到一个项目组把plugin.xml塞进自己写的工具jar里,结果平台根本没扫那个jar,注册始终不生效。所以遇到“注册不生效”的问题时,先别怀疑代码,先确认一个基本事实:这个XML到底有没有落到平台会扫描的目录里。
3. 一个能跑通的最小注册实战
3.1 写一个最朴素的按钮插件
我习惯于用最小案例验证机制,第一步先在单据卡片上增加一个自定义按钮,点击后弹个提示,能跑通了再往里塞真实业务逻辑。这个按钮插件继承平台提供的按钮插件基类,在点击动作里取当前卡片数据的主键,调用后台能力,再返回操作结果。核心是验证一件事:注册信息与运行时代码是否能正确对接。下面的示例代码是一个骨架,如果你用的平台版本接口不一样,替换成对应的基类方法即可。
package nc.demo.plugin; import nc.ui.pubapp.uif2app.actions.pub.AbstractButtonPlugin; import nc.ui.pubapp.uif2app.view.BillCardPanel; public class CheckStockButtonPlugin extends AbstractButtonPlugin { @Override public void doAction(nc.ui.pubapp.uif2app.actions.ActionContext ctx) { BillCardPanel card = getBillCardPanel(); if (card == null) { return; } String[] pks = card.getSelectedPrimaryKeyValues(); // 调用后台服务,校验库存,返回提示 int stock = getStockService().queryStock(pks[0]); ctx.showMsg("当前库存:" + stock); } }这段代码不复杂,但里面埋了几个容易出错的地方。getBillCardPanel()返回空时要判空,很多插件运行时报空指针就是没做这个判断;取主键的方式依赖当前界面状态,如果界面上没有选中记录,pks可能是空数组,不处理就会数组越界;调后台服务尽量走平台的事务和上下文,别在新线程里直接创建连接,否则可能出现连接管理混乱和事务不可控的问题。新手写按钮插件最常见的翻车点,就是忽略了这些界面状态的边界情况。
3.2 把插件注册到目标节点
代码写完后,要确认按钮注册到哪个节点上。NC的单据界面通常有节点编码,比如采购订单、销售订单、库存单据各有各的编码。插件描述文件通过扩展点挂按钮,但这个按钮要出现在哪个单据的工具栏上,通常由按钮的target属性和平台资源共同决定。不同版本写法不一样,有些在plugin.xml的button节点上直接配置target,有些需要维护菜单资源和按钮资源,再绑定到单据模板上。这也是很多人一开始就搞混的地方:光写了plugin.xml,没有把按钮资源和目标单据节点关联起来,按钮自然出不来。
我建议的做法是:先在系统里找到目标单据的节点编码,确认该节点属于哪个模块,然后把插件XML放在对应模块目录下;部署后重启中间件,再打开单据页面,看工具栏是否多了按钮。完整的验证流程是:部署 -> 重启服务 -> 清客户端缓存 -> 打开目标节点 -> 观察按钮。少了“清缓存”这一步,经常会发生代码更新了但界面还是老样子。清缓存不只清浏览器缓存,还包括NC客户端本地缓存和中间件临时文件目录,这一点在开发阶段极易被忽略。
3.3 从按钮到完整的业务扩展
按钮插件跑通之后,再扩展其他插件点就顺理成章了。比如保存前校验库存:写一个单据生命周期插件,重写保存前方法,在数据尚未提交时做校验,发现问题直接抛异常中断保存,这个“中断”能有效防止脏数据落库。再比如审批后动作:审批通过之后自动把数据推送到下游系统,或者生成关联单据,这种场景更适合用服务端插件实现。注册方式跟按钮插件类似,在描述文件里多配一段extension即可,但有一点要特别提醒:一个插件类最好只负责一类事情,不要在一个类里堆几十个方法,然后挂到多个插件点上。
我见过一个项目把所有扩展逻辑都写在同一个插件类里,最后类超过两千行,代码本身能跑,但维护非常痛苦。任何一个环节抛异常都要从头翻到尾,定位问题的时间可能比写代码的时间还长。插件类的职责边界清晰,名字和ID保持语义化,注释写明挂载点和业务用途,这些习惯在项目后期会带来巨大的回报。
4. 注册之后不生效?九成问题在这些地方
4.1 五个快速定位动作
插件注册开发里最让人头疼的就是“代码没问题,但就是不生效”。我总结过五个定位动作,按顺序排查能解决绝大多数问题。第一是检查plugin.xml是否落到了平台扫描的模块META-INF目录里;第二是检查插件类是否已经编译进对应的classes或jar包中,而不是只存在于IDE的编译输出里;第三是检查扩展点名称和插件基类与当前平台版本是否匹配;第四是清掉NC客户端缓存和中间件缓存后再看效果;第五是打开系统日志,看启动过程有没有加载你注册的插件。
这五步看着简单,但每一条背后都有真实案例。尤其是第四条。很多开发在本地改了代码,重新登录客户端,发现界面没变,就误以为注册失败,实际上缓存里的旧类还在运行。遇到这种情况,把NCHome下的临时目录和客户端缓存目录清理掉,再重启一次,往往问题就消失了。这不算技术难点,但确实是最耗人耐心的环节。
4.2 系统监视器里的pk锁,到底在说什么
排查过程中可能会用到NC系统监视器。不少人在监视器里看到“pk锁”和“加锁次数1”就开始紧张,其实不用慌。之前就有同事盯着屏幕问我:这个pk锁加锁次数1是不是系统死锁了?我说你把概念搞混了。pk锁是数据库层面对某一主键行的锁,加锁次数表示这一行记录被当前事务获取锁的次数。在并发环境下,多个用户同时打开编辑同一个单据时,系统会对同一条主键记录加锁,加锁次数为1是最常见的正常状态,代表当前会话加过一次锁。
真正需要警惕的不是加锁次数本身,而是锁的持有时间和等待关系。如果某个会话长时间持有锁不释放,其他会话在等这个锁,同时加锁次数不停上涨,那就需要回头查业务代码了。常见的情况是插件里开启了事务但忘记提交,或者是长事务中嵌套了远程调用,把整个事务的持续时间拖得很长。排查思路是:在系统监视器里看会话对应的SQL和事务开始时间,再回到插件代码里找可疑的事务控制位置。这里单独提醒一句:不要为了排查问题去手工杀数据库会话,杀了会话只是解决表面症状,根因往往在代码逻辑里。先看事务边界,再看是否有循环调用,最后才看锁等待链。
4.3 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 注册后界面没有新增按钮 | 插件XML没被扫描,或扩展点不匹配当前版本 | 确认XML部署路径,逐个比对插件点名称 |
| 按钮显示但点击无反应 | 插件类没编译到运行环境,或方法签名覆盖错误 | 重新编译部署,核对父类方法签名 |
| 保存后插件执行了两次 | 同一插件被重复注册,或同时挂到了多个扩展点 | 检查模块中是否有重复的plugin.xml,只保留一个挂载点 |
| 插件抛异常但界面无提示 | 异常被平台捕获后未正确处理 | 在插件方法内捕获并包装为业务异常,打开详细日志 |
| 锁等待严重 | 插件中开启事务后未及时提交,或长事务中调用远程API | 缩小事务边界,远程调用放到事务外,必要时用异步任务 |
4.4 日志是定位插件问题的第一工具
说实话,很多插件问题不用看什么高大上的工具,看日志就够了。NC运行日志在NCHome目录下的日志文件夹里,操作日志、系统日志、错误日志是分开记录的。开发阶段我习惯把日志级别调到DEBUG,专门盯着自己插件类名前缀的输出。这样做的好处是能立刻确认平台有没有加载这个类、插件代码走到了哪一步、执行顺序是否符合预期。如果日志里根本搜不到你注册的类名,那说明问题出在注册阶段,而不是业务逻辑阶段,排查方向立刻就不一样了。
还有一个我自己长期在用的技巧:在插件代码的关键位置加上带明确标记的日志。入口、取数、调用后台、异常出口,每个地方一行,统一用类名做前缀。这样代码上线之后出问题,能靠日志把问题快速圈定在某一段,而不是到处翻代码。这个方法虽然朴素,但实测比反复远程调试高效得多。尤其是在客户现场没有IDE调试条件的时候,日志就是唯一的眼睛,有没有埋好点直接决定排查效率。
5. 插件注册开发的一点个人体会
如果让我给刚开始做NC插件注册开发的人一句建议,那就是:先从最小的按钮插件开始,跑通一个“注册-部署-生效”的完整闭环,再考虑复杂业务。这个最小闭环会逼着你把环境、目录、依赖、缓存这些基础问题全部趟一遍,之后再做任何插件,无非就是复制套路。很多项目组一上来就做一堆复杂插件,结果连最基础的注册环节都没弄明白,后面越做越乱,返工成本极高。
还有一点是插件规范问题。NC平台允许在同一个扩展点上挂多个插件,但不代表可以乱挂。我见过一个节点挂了十几个插件,执行顺序混乱,排查问题时谁都不敢动。建议的做法是:同类扩展尽量合并到同一个插件模块里,顺序号之间预留空隙,方便后续插入新逻辑;插件类名和ID保持语义化,注释写明挂载点和业务用途;代码上线前在测试环境完整跑一遍保存、审批、弃审三个核心动作,确认没有影响到标准流程。
最后分享一个我自己的开发习惯:NC的插件机制虽然灵活,但不要把所有扩展都塞进插件里。如果平台本身提供了标准配置项,优先用标准配置;只有标准功能确实满足不了需求时,才考虑插件扩展。插件是锦上添花,不是万能药。保持克制,后续的升级维护会轻松非常多。
本文还有配套的精品资源,点击获取