ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

苍穹插件开发数据加载全攻略:表单、列表、服务插件与性能优化

苍穹插件开发数据加载全攻略:表单、列表、服务插件与性能优化 做了这么多年苍穹二开插件里“加载数据”这件事几乎每天都在写。但奇怪的是我在社区和客户现场经常看到一种现象很多人代码能跑却不知道自己到底在加载什么为什么这样加载换一个场景就抓瞎。今天这篇我就专门把“插件开发里的数据加载”这件事拆开揉碎讲清楚从最基础的取数方式到列表、表单、服务三类插件各自的加载套路再到我踩过的坑一次说透。这篇内容适合谁刚接触苍穹插件开发、被动态表单模型绕晕的初级二开也包括已经写了半年一年插件、想系统性梳理数据加载方案的开发。全文没有广告没有理论堆砌都是可以直接拿过去用的代码片段和排查思路。1. 加载数据之前先把苍穹的数据模型搞清楚1.1 你取到的数据到底是“显示值”还是“存储值”刚开始写插件的人最容易栽的跟头是把页面看到的值和数据库存的值混为一谈。苍穹页面渲染出来的数据是经过格式化的比如日期显示成“2025-06-18”金额显示成“1,234.56”基础资料显示成编码名称的组合但这些都不是数据底层的本相。插件里通过this.View.Model.GetValue(billDate)取到的通常是经过平台处理的DynamicObject或者基础类型而通过GetDataEntity()拿到的才是这张单据完整的实体数据。我见过一个同事想在表单插件里拿基础资料字段的编码直接用了this.View.Model.GetValue(supplier)取出来一个对象打印一看里面有Id、Number、Name他当时就懵了。实际上在苍穹的模型里基础资料字段的值是一个Long型的id要拿到编码和名称得走DynamicObject的属性取法或者通过DataEntity再去解析。这里我的建议是先分清楚你是在哪个环节取数。如果只是取当前表单页面上某个字段的值用this.View.Model.GetValue(字段标识)就够了如果你要拿着这条数据去做服务端逻辑或者要把单据体数据批量拿出来处理直接用this.View.Model.GetDataEntity()把整个动态实体拿到手再逐字段取。另外一个高频坑是字段标识苍穹里字段标识大小写敏感而且设计器里显示的名称和真正的标识是两回事。比如页面上叫“供应商”标识可能是“supplier”也可能是“kdec_supplier”带上前缀的往往是二开扩展字段。你写错一个字母运行时不报错取回来的就是null排查要花半天。所以每次取数之前先到BOS设计器里核对一下字段标识别凭记忆写。1.2 三种插件类加载数据的姿势完全不同苍穹插件主要分表单插件、列表插件、服务插件三大类我接触过的项目中几乎80%的数据加载需求都落在这三块里。同类插件内部加载数据的方式也有差异不要拿表单插件里取数的方式硬套到列表插件上。表单插件包括动态表单和单据的插件核心操作对象是this.View.Model你可以直接读写当前实体数据配合BeforeDoOperation、AfterDoOperation、AfterLoadData这类生命周期事件去处理数据。列表插件不一样它的核心是ListShowParameter和QueryServiceHelper你要主动构造查询条件去查数据再绑定到列表视图上。服务插件则更多的在操作服务、计划任务、审批流里跑没有页面Model可用一切数据都得通过服务上下文去拿。简单说表单插件是“人在页面上操作数据已经躺在那里你只需读和改”列表插件是“按条件从数据库里拉一批数据渲染成列表”服务插件是“后台默默处理数据要通过操作参数或服务上下文显式去取”。这三个场景的代码写法和注意事项差别巨大下面我分别带着代码讲。2. 开发环境与插件工程的基本盘2.1 一套能跑起来的本地开发环境再好的思路环境配不好也是白搭。金蝶云苍穹的插件开发本质上是Java开发我在项目里最常用的组合是IntelliJ IDEA 苍穹的Maven依赖 本地/测试环境的苍穹服务。这里有个很重要的点苍穹插件分“苍穹设计器内嵌编写”和“独立Java工程编写”两种模式。设计器里那种轻量级的规则插件、简单字段联动可以直接在界面上配但只要是稍微复杂一点的加载数据逻辑比如多表联查、自定义过滤、批量取数我的经验是必须走独立Java工程用Maven管理依赖调试也方便。你需要在pom.xml里引入苍穹的开发SDK依赖具体坐标版本跟你使用的苍穹版本强相关。这个问题我在几个客户的现场都遇到过引入了错误的依赖版本插件一部署就报NoClassDefFoundError。正确做法是让实施或架构组提供与你环境匹配的依赖清单或者从平台的lib目录下把对应jar包导出来install到本地仓库。别自己在网上随便找依赖版本版本不匹配的问题非常隐蔽。工程建好之后写插件类、编译、打成jar包然后在苍穹设计器的“插件”配置里填上你的插件类全限定名保存发布。这是一个标准的二开流程。需要提醒的是每次改完Java代码都要重启或热部署对应的服务否则你改的代码不生效这种“改了没反应”的假象我遇到过太多次最后发现是没重启。2.2 插件类要继承谁父类决定你能干什么选错父类是插件运行时不执行或者找不到方法的头号原因。我理了一下最常用的继承关系单据/动态表单插件继承AbstractFormPlugIn或AbstractBillPlugIn。AbstractBillPlugIn是单据插件比AbstractFormPlugIn多了单据专属的能力。列表插件继承AbstractListPlugIn。服务插件根据场景不同继承AbstractOperate操作服务插件、AbstractAuditPlugIn审核相关、AbstractBillPlugInService单据服务等。我见过一个特别典型的问题同事在列表插件里重写了afterLoadData发现怎么都不触发后来一看他继承的是AbstractFormPlugIn列表视图根本没有表单模型。所以动手写之前先确认你的插件挂载在哪个位置BOS里“表单插件”“列表插件”“服务插件”三个入口对应不同的父类体系别混淆。绑定插件时注意插件标识必须是类的全限定名包括包名写成类名简写是找不到的。还有一个顺序问题BOS里可以挂多个插件插件之间的执行顺序按配置顺序来如果A插件要先加载数据、B插件要消费A的数据顺序千万不能反否则B拿到的永远是null。3. 实操核心三类加载数据的标准写法3.1 表单插件用Model取当前数据用Query取扩展数据表单插件加载数据分两种一是加载当前单据自身的数据二是根据当前单据的信息去查别的数据。前者简单后者是常规需求。先看第一类。在单据的AfterLoadData事件里我可以这么取public class MyBillPlugin extends AbstractBillPlugIn { Override public void afterLoadData(EventObject e) { // 取单个字段值 String billNo (String) this.getModel().getValue(billNo); // 取整单数据实体 DynamicObject billData this.getModel().getDataEntity(); // 取单据体数据集合 DynamicObjectCollection entryRows billData.getDynamicObjectCollection(entryEntity); } }这里有几个细节。getValue返回的字段类型要看字段定义比如基础资料字段返回的是Long型id而不是对象单据体取出来的是一个DynamicObjectCollection你可以遍历它去读每一行。再说第二类需要查外部数据。这里我推荐优先用QueryServiceHelper它是苍穹里通用的查询入口可以跨表查、可以加过滤条件返回DynamicObjectCollectionimport kd.bos.servicehelper.QueryServiceHelper; DynamicObjectCollection result QueryServiceHelper.query( my_bill, // 查询的实体标识 billNo,supplier.name,totalAmount, // 要查的字段支持基础资料属性的点路径 billNo ? and totalAmount ?, // 过滤条件参数用?占位 new Object[] { billNo, 1000 } // 条件参数 );我自己的习惯是能用query一次性查出来的绝不循环里单条load。比如我要按单据头一批单据号查所有单据体行直接一次query把头和体都拉出来再在内存里分组比在循环里一个个load效率高一个数量级。这里需要多说一句字段路径。苍穹的查询字段支持用点号取关联基础资料的属性比如supplier.number、supplier.name这是平台自动join的省了你手动多表关联的麻烦。但你得知道这种点路径的写法在动态表单模型里同样适用比如getValue(supplier.name)在部分场景下也能取到。不过我用下来最稳的还是先拿对象再取属性。3.2 列表插件构造查询参数喂给列表视图列表插件加载数据的思路和表单插件完全不同。列表页的核心是ListShowParameter你构造好它再调用this.getView().showData()就能刷新列表。一个最基础的列表加载长这样public class MyListPlugin extends AbstractListPlugIn { // 刷新列表数据可指定过滤条件 public void refreshList() { ListShowParameter showParam new ListShowParameter(); showParam.setFormId(my_bill); // 设置列表的过滤条件只在当前列表视图生效 showParam.getCustomParams().put(keyword, 张三); // 打开或刷新 this.getView().showData(showParam); } }但实际项目里列表插件更多的场景是重写查询条件。常见的是在afterQueryData里对查出来的数据做二次加工或者在beforeQueryData里改写查询参数。我项目中用得最多的是重写列表的过滤Override public void setFilter(FilterParameter filter) { super.setFilter(filter); // 追加客户权限过滤 filter.getFilterItems().add(new FilterItem(customer.id, , currentCustomerId)); }还有个使用要点列表插件的取数如果只是展示不需要把数据实体全部加载出来尽量只取列表需要的字段否则数据量大的时候列表首屏会很慢。我在一个客户现场排查过他们列表每次打开要5秒后来发现是查询条件里带了单据体字段导致平台把整个主从表都load了一次。去掉单据体字段后首屏降到1秒以内。列表加载完数据后你还可以在afterQueryData里拿到查询结果集Override public void afterQueryData(EventObject e) { super.afterQueryData(e); DynamicObjectCollection rows this.getModel().getDataEntities(); // 对返回结果做加工或统计 }这里注意afterQueryData里的数据是当前列表页展示的那一页数据不是全量数据。如果你要统计整个数据集的汇总别在这里写聚合逻辑否则你统计的只是当前页。3.3 服务插件从操作上下文里取数据服务插件没有页面Model最常见的是操作服务插件通过继承AbstractOperate来实现。它的核心方法是execute通过OperateContext上下文取参数。public class MyOperatePlugin extends AbstractOperate { Override public void execute(OperateContext ctx) throws Exception { // 操作服务传入的选中数据一般是Long[]形式的id数组 Long[] selectedIds ctx.getSelectedIds(); // 按具体操作类型可能需要从ctx.getDataEntities()取完整数据 DynamicObject firstRow ctx.getDataEntity(); // 或者取操作参数里的自定义参数 Object customValue ctx.getCustomParam(customKey); } }在服务插件里取数要区分两个来源如果操作是从列表页发起的ctx.getSelectedIds()会带选中行的id如果是从单据详情页发起的ctx.getDataEntities()里会有当前单据的实体数据。我做过一个批量审核的服务插件就是先拿selectedIds再批量load单据数据逐条做校验最后调用审核服务。服务插件里加载数据我强烈建议用批量接口不要一条条循环load。苍穹的BusinessDataServiceHelper提供了load和loadMore两个方法import kd.bos.servicehelper.BusinessDataServiceHelper; // 单条加载 DynamicObject single BusinessDataServiceHelper.load(id, my_bill); // 批量加载 DynamicObjectCollection batch BusinessDataServiceHelper.loadMore(ids, my_bill);如果数据量比较大比如一次几百上千条还有分页load的姿势用loadMore配合OrmConfig做字段裁剪只取你要用到的字段避免把大字段比如文本、附件信息也一并load出来内存和性能都会好很多。我写过一次批量处理没裁剪字段结果服务直接OOM了后来加了OrmConfig只取必要字段内存瞬间降下来。3.4 通过OrmConfig控制加载字段性能差距就在这BusinessDataServiceHelper.load系列方法支持传OrmConfig用来指定查询哪些字段类似于SQL里的select指定列。这个参数很多初学者会忽略但它是性能优化的第一道门。import kd.bos.orm.OrmConfig; OrmConfig config new OrmConfig(); config.addField(billNo); config.addField(supplier); config.addField(entryEntity.entryRow); config.addField(entryEntity.amount); DynamicObjectCollection dataRows BusinessDataServiceHelper.loadMore( ids, my_bill, config );OrmConfig还有一个好处你可以控制是否加载单据体行、是否加载基础资料属性。默认情况下load会连基础资料的关联属性都带出来数据量大时开销很明显。如果你只需要Id和Number就明确只查这两个字段别让平台白做那么多join。我自己的一个经验阈值是超过50条的批量load一律用OrmConfig裁剪字段超过200条的必须做分页超过1000条的建议直接走QueryServiceHelper用分页查询。这套标准我在多个项目里复用基本没出过性能事故。4. 查询引擎与服务选择的细节决定代码上限4.1 什么时候用QueryServiceHelper什么时候用BusinessDataServiceHelper这是我在面试候选人时经常问的一个问题也是项目里争论最多的点。我先把结论说出来查询展示用QueryServiceHelper拿到实体做操作用BusinessDataServiceHelper。QueryServiceHelper适合做列表查询、报表取数、条件统计这类只读场景它支持复杂过滤、排序、分页查出来的DynamicObject是轻量的行数据。BusinessDataServiceHelper.load系列适合按id读取完整业务对象它会返回带单据体的完整实体你可以直接对这个实体做修改再保存或者把它传给操作服务去审核、提交。两者之间有一个很容易踩的坑QueryServiceHelper查出来的数据是“查询快照”它不是完整的业务对象有些字段没查的话就是null而且不能直接拿去调保存操作。如果代码里查完直接改数据再提交保存往往保存的只是部分字段甚至因为缺了关键字段导致保存报错。正确做法是拿到查询出的id后再用BusinessDataServiceHelper.load把完整实体load出来在完整实体上改数据、做操作。4.2 写过滤条件时的表达式语法苍穹查询的过滤条件不是原生SQL它有一套自己的表达式规则。基本写法是“字段标识 比较符 参数占位符”。比较多的是等于、不等于、大于、小于、in集合、like模糊匹配。in集合的写法我截个代码DynamicObjectCollection result QueryServiceHelper.query( my_bill, billNo,customer.name,totalAmount,status, billNo in (?, ?, ?) or status ?, new Object[] { BILL001, BILL002, BILL003, A } );如果你只有一个集合参数也可以把集合当一个占位符传进去苍穹支持把List或数组整体作为一个in参数。但要注意空的集合千万不要传否则生成的SQL可能变成in ()有些数据库直接报错有些数据库查不出数据这算一个低级但常见的坑。还有like的写法参数里拼%号比如new Object[]{% keyword %}而不是在条件里写like %?%。这点和JDBC的PreparedStatement习惯一样参数和SQL模板要分开平台才能正确做防注入和类型转换。4.3 数据权限与字段权限的隐藏坑服务插件里取数如果不做任何处理默认是带操作者上下文的也就是说查询结果会受到数据权限和字段权限控制。这个机制有好有坏。好的一面是安全坏的一面是你可能会莫名其妙地查不到数据。我第一次做项目时就遇到过管理员账号跑计划任务查数据全正常换普通账号在前台触发同一个服务查出来少了几条。排查了半天最后发现是数据权限规则在起作用。解决方案要看具体需求如果查询就应该遵守权限那就保持默认如果后台任务必须绕过权限则要用特定的查询入口或者提升上下文。我个人的习惯是后台任务、计划任务这类场景尽量显式处理权限上下文前台用户操作的场景则明确依赖权限校验。字段权限的坑更隐蔽。有时候数据能查出来但某些字段返回null不是因为数据没有值而是当前操作者对该字段没有查看权限平台在查询层就把字段值屏蔽了。遇到这种情况先别怀疑代码用管理员账号跑一次同样的逻辑如果管理员能看到那就是字段权限问题去权限配置里检查即可。5. 常见问题与排查技巧实录5.1 为什么插件里取出来的数据总是null这个问题的出现频率在我的答疑记录里排第一。我总结了一下十个里有七个是以下三个原因一是字段标识写错。区分大小写、带不带扩展前缀去BOS设计器里核对别靠记忆。二是取数时机不对。表单还没加载完就去取数据比如在beforeBindData里用getValue取页面值这时候数据都还没进来取到null太正常了。要在afterBindData、afterLoadData这类生命周期事件里取。三是数据源本身就没值。页面可能显示了值但那个值是计算列或动态字段不是真实存储字段。你需要在数据库里直接跑一条SQL确认数据是否存在排除业务数据本身为空的情况。排查这类问题我有一招特别好用在代码里打印当前数据实体的JSON串。用JSONUtils.toJson(dataEntity)或者直接输出实体类型一跑就能看到数据里到底有哪些字段、哪些值是null比一步步断点快得多。5.2 加载数据慢先查查询字段和循环性能问题我前面提了两次这里集中说一个排查顺序。首先看是不是循环里逐条load数据这是最常见的性能杀手。把循环内load改成批量loadMore或者一次性query通常能带来几十倍的性能提升。其次看查询条件有没有走索引过滤条件里的字段如果没建索引数据量大时查询会很慢。再者看是不是把不必要的单据体行全部加载了用OrmConfig裁剪掉无意义的大字段。我处理过最夸张的一个案例一个列表页打开要15秒最后发现列表查询条件里带了一个基础资料属性的模糊匹配而那个基础资料表有几十万条数据平台做关联查询直接炸了。后来改造成先按编码前缀查基础资料再按查到的id集合过滤列表整个查询降到1秒内。5.3 修改数据后不生效或者保存失败这个坑通常发生在“查到数据 → 改了字段 → 直接保存”这套流程里。前面讲过QueryServiceHelper查出来的数据是轻量快照不是完整业务对象。你对快照修改再调用保存服务平台会因为数据不完整报错或者更新的字段不生效。正确流程是用BusinessDataServiceHelper.load拿出完整实体修改字段再调用保存服务。而且如果涉及单据体行的新增、删除、修改要特别注意行的状态新增加的行要setStatus(EntityBase.STATUS_ADDNEW)修改的行要setStatus(EntityBase.STATUS_UPDATE)。我见过不少新手在这块踩坑改完数据保存数据库里纹丝不动就是行状态没设置对。5.4 插件没生效时的三板斧插件写好挂上运行没反应先别急着怀疑代码逻辑。我的排查顺序是第一检查插件类有没有编译进jar包用反编译工具看一眼确认类存在。第二检查BOS的插件配置是否写对了全限定名有没有保存发布。第三确认服务是否重启或热部署成功。这三步走完百分之八十的“插件没生效”问题都能解决。如果还没生效就去看苍穹的日志。服务端日志里会记录插件加载异常有时候是类冲突、有时候是依赖缺失。我遇到过最无语的一次是插件类名和打包后的包名不一致BOS配置的是旧包名新包名从未被加载日志里压根没有插件相关输出折腾了两小时。写在最后加载数据这件事看起来是每个插件开发的第一步但真要写顺手还是得靠场景积累。我个人的体会有三条第一时刻搞清楚自己站在哪类插件上表单、列表、服务三种姿势别混着用第二能用批量绝不用循环写代码之前先算一下数据量级第三数据的查询和修改变成两件事查出来的快照别拿去保存要保存就先load完整实体。如果你们项目中也在做苍穹插件开发遇到加载数据相关的疑难杂症建议把本文提到的排查顺序过一遍大部分问题都能在日志和代码里找到答案。这类插件开发的系列内容我后面还会继续梳理比如数据保存、表单校验、列表自定义按钮、服务插件的高级用法都是实打实的项目经验到时候再跟你们慢慢聊。
返回列表