扩展脚本编写指南
简介
扩展脚本运行引擎为Rhino,基于javascript语法,运行期解释执行。jvm中的类的方法和属性均可应用于脚本中,调用方式类似于java代码编写。 比如下面一段脚本: //客户端设置编码规则脚本 var easNames = JavaImporter(); easNames.importPackage(Packages.com.kingdee.eas.base.codingrule); with(easNames){ var editData = pluginCtx.getDataObject(); var bizOrgUnit= pluginCtx.getKDBizPromptBox(\"prmtSaleOrgUnit\").getValue(); var number = CodingRuleManagerFactory.getRemoteInstance().getNumber(editData, bizOrgUnit.getId()); pluginCtx.getKDTextField(\"txtNumber\").setText(number); } 其中 var easNames = JavaImporter(); easNames.importPackage(Packages.com.kingdee.eas.base.codingrule); 引入包com.kingdee.eas.base.codingrule,CodingRuleManagerFactory 属于该包中的内容,但其处在with(easNames){ … }中,所以不需要指定其全路径,即可调用getRemoteInstance()方法。如果没使用with指定作用域,则必须使用全名称(包名+类名)。
变量的定义声明遵循javascript语法,接口实现使用function声明。比如给一些控件增加监听器:
//F7控件增加值改变监听 pluginCtx.getKDBizPromptBox(\"prmtHROrg\").addDataChangeListener(function (event) { java.lang.System.out.println(\"DATA CHANGED:\" +event.getOldValue() + \"$$$$\" + event.getNewValue()); }); //KDTable增加表格编辑事件监听 pluginCtx.getKDTable(\"kdtEntrys\").addKDTEditListener(function(event,methodName){ if(methodName == \"editStopped\") { com.kingdee.eas.util.client.MsgBox.showInfo(\"$$$$$EditStopped\");
} }); 上下文
扩展脚本中封装了两种上下文,插件上下文(pluginCtx)和方法上下文(methodCtx)。这两种上下文为系统内置,无需额外的声明或获取。
pluginCtx主要存放当前执行环境的相关信息,比如服务端的Context,客户端的界面控件、界面状态、编辑对象等。methodCtx存放当前操作的相关信息,包含方法参数和方法返回值。
插件上下文:
客户端和服务端的插件上下文存储的内容不一样,实现方式上,其分别对应于两个java类,com.kingdee.eas.ep.app.BeanParam和com.kingdee.eas.ep.client.UIParam。这两个类中的方法均可以在脚本中直接使用。
上下文都存在get和put方法。脚本中直接使用get(key)获取值,put(key,value)设置值。
服务端通过pluginCtx.getContext()获取服务端上下文,比如: //服务端更新单据状态 var imp = JavaImporter(); imp.importPackage(Packages.com.kingdee.eas.util.app); with(imp){ var ctx = pluginCtx.getContext(); var billId = methodCtx.getParamValue(0); var sql = \"update T_SD_SALEORDER SET FBASESTATUS = \" + status + \" WHERE FID = '\"+billId+\"'\"; DbUtil.execute(ctx, sql); }
客户端插件上下文封装了部分常用的内容,具体如下: //获取界面上下文 java.util.Map getUIContext() //获取组织上下文 com.kingdee.bos.Context getMainOrgContext() //获取界面状态,比如新增、编辑 java.lang.String getOprtState() //获取当前编辑对象,编辑界面的editData com.kingdee.bos.dao.IObjectValue getDataObject() //获取界面绑定 com.kingdee.bos.appframework.databinding.DataBinder getDataBinder() //获取规则处理 com.kingdee.eas.framework.client.UILifeCycleHander getLifeCycleHander() //获取当前界面实例 com.kingdee.bos.ui.face.IUIObject getUI() 直接通过pluginCtx调用即可,比如pluginCtx.getUI即可获取当前界面实例。
界面的控件可以通过get + 控件类型 + ( + 控件名 +)的方式获得,比如获取单据分录的表格可以这样pluginCtx.getKDTable(\"kdtEntries\")。
方法上下文:
方法上下文存放了方法的相关信息,包括方法参数和方法返回值。方法上下文提供了如下常用方法: //获取方法名 String getName() //获取方法别名 String getAlias() //获取方法参数类型 String[] getParamTypes() //获取方法某个参数的类型 String getParamType(int index) //获取方法参数值 Object[] getParamValues() //获取方法某个参数值 Object getParamValue(int index) //获取方法返回值类型 String getResultType() //获取方法返回值 Object getResultValue() //设置方法返回值 void setResultValue(Object value) 如果想获得方法的某个参数,比如IObjectPK addnew(CoreBaseInfo model)的参数,这可以通过methodCtx.getParamValue(0)来获得model。同时可以通过methodCtx.getResultValue()来获
得方法的返回值。
注意:设置方法的返回值必须通过methodCtx.setResultValue(value)来设置,不可以用类似方法返回值的方式写(return value)。
可以通过定制平台的扩展定义中的上下文来快速编写一些常用脚本。
自定义业务方法
在实体或Façade中新增一个业务方法后,如何调用该业务方法?
自定义业务方法的调用是通过工具类来实现的。工具类主要需要三个参数:元数据PK(IMetaDataPK pk),方法名称(String methodSignature),参数对象数组(Object[] args)。其中方法名称请从该业务方法的扩展点的描述信息中拷贝获得,否则容易出错,如果方法没有返回值,注意名称前有空格,不要删除。
客户端调用业务方法方式:
com.kingdee.bos.framework.BOClientTool.callCmethod(pk,methodSignature,args) 比如: //客户端调用服务端方法 var easNames = JavaImporter(); easNames.importPackage(Packages.com.kingdee.bos.ctrl.extendcontrols); easNames.importPackage(Packages.com.kingdee.bos); easNames.importPackage(Packages.com.kingdee.bos.util); easNames.importPackage(Packages.com.kingdee.bos.metadata); easNames.importPackage(Packages.com.kingdee.eas.util.client); easNames.importPackage(Packages.com.kingdee.bos.framework); with(easNames){ var pk = new MetaDataPK(\"com.kingdee.eas.scm.sd.sale.app.SaleOrder\"); var name = \"java.lang.Boolean updateStatus(com.kingdee.bos.util.BOSUuid billId)\"; var params = [BOSUuid.read(\"2s5eatuISSqb7TuLMfibSMSKQjo=\")]; var result = BOClientTool.callCmethod(pk,name,params); MsgBox.showInfo(\"Result is:\" + result); com.kingdee.eas.util.SysUtil.abort(); } 服务器端调用业务方法方式:
com.kingdee.bos.framework.BOProxy.getProxy(ctx,pk).callCmethod(methodSignature,args)
自定义异常
业务异常定义后,可以通过工具类抛出异常。
服务器端调用方式:
com.kingdee.eas.ep.plugin.ExceptionUtil.throwOnServer(pluginCtx.getContext(),name,subName,args)
其中参数为:
Context ctx-服务器上下文 String name-异常名称
String subName-子异常名称
Object[] args-参数对象数组,可删去
args[i]中的内容将会替代对应的{i}中的内容,如果没有参数,可以省略该参数。
客户端调用方式:
com.kingdee.eas.ep.plugin.ExceptionUtil.throwOnClient(name,subName,args) 参数含义如上。
自定义基础资料或单据
定制平台支持自定义基础资料和单据的实现原理是先通过内置的模板拷贝生成新的动态元数据,然后通过模板这个壳来运行。新生成的元数据可进行业务调整。所有对动态内容的操作,最终都转换为对模板的操作,模板根据具体元数据的信息,进行相应的转换后,再执行操作。
模板通过EAS系统内置产生,目前只内置了基础资料和单据两种模板。其他模板请到【定
制模板】功能中查看。 基础资料的模板信息为: 元数据类型 实体 查询 列表界面 编辑界面 列表界面功能 编辑界面功能
业务单据的模板信息为:
元数据类型 实体 查询 列表界面 编辑界面 列表界面功能 编辑界面功能 值 com.kingdee.eas.ep.app.CoreBillBaseCustom com.kingdee.eas.ep.app.CoreBillBaseCustomQuery com.kingdee.eas.ep.client.CoreBillBaseCustomListUI com.kingdee.eas.ep.client.CoreBillBaseCustomEditUI com.kingdee.eas.ep.app.CoreBillBaseCustomListUIFunction com.kingdee.eas.ep.app.CoreBillBaseCustomEditUIFunction 值 com.kingdee.eas.ep.app.DataBaseCustom com.kingdee.eas.ep.app.DataBaseCustomQuery com.kingdee.eas.ep.client.DataBaseCustomListUI com.kingdee.eas.ep.client.DataBaseCustomEditUI
下面通过对基础资料的相关操作来展示如何在脚本中应用动态内容。
客户端调用新增、修改、查询等动作首先需要一个远程接口,可通过如下方式获得: var bizInterface = com.kingdee.bos.BOSObjectFactory.createRemoteCommonBOSObject(new com.kingdee.bos.metadata.MetaDataPK(entityPK)) 其中,entityPK为自定义基础资料的实体的全名称,比如 ”com.kingdee.eas.custom.app.Diploma”。返回值为com.kingdee.eas.ep.IDataBaseCustom类型,这是由于其从该模板生成所致。这样IDataBaseCustom上的所有操作,动态基础资料都有,也均可以通过该实例执行。比如,新增操作
//创建值对象 function createNewData() { } var objectValue = new com.kingdee.eas.ep.DataBaseCustomInfo(); //实体的bosType,可通过[元数据检查]获得 objectValue.setBOSType(entityBosType); objectValue.setPK(new com.kingdee.bos.metadata.MetaDataPK(entityPK)); return objectValue; bizInterface.addnew(createNewData()); //新增 注意:这里值对象创建必须设置一个实体的bosType和PK,引擎是通过这两项进行ORMMapping的。删除、更新、查看等动作由于值对象都存在id,所以其可以自动解析获得,不需要手工设置。
调试
如果想调试某段脚本,请在扩展定义中勾选“调试模式执行”。这样,在脚本运行时会弹出脚本调试窗口。
注意:如果调试的脚本为服务器端脚本,即实体或façade中基于业务方法编写的脚本,则必须在定制平台中启动“脚本远程调试”,否则将会抛出BOS异常,提示启动远程调试服务器。
调试支持单步执行、断点设置、变量查看、脚本控制台等。
关于Rhino
http://www.mozilla.org/rhino/
Rhino is an open-source implementation of JavaScript written entirely in Java. It is typically embedded into Java applications to provide scripting to end users.
Rhino语法简介
Rhino 是一种使用 Java 语言编写的 JavaScript 的开源实现。与本系列的其他很多语言一样,Rhino 是一种动态类型的、基于对象的脚本语言,它可以简单地访问各种 Java 类库。Rhino 从 JavaScript 中借用了很多语法,让程序员可以快速编写功能强大的程序。最为明显的区别是,Rhino 不再使用语句结束符( ; ),放宽了变量声明规则,并且极大地简化了修改和检索对象属性的语法(没有求助于调用存取方法)。
由于 Rhino 是 JavaScript 的一种基于 Java 的实现,所以对于 Java 开发人员来说,它应该特别易于使用。JavaScript 的(以及 Rhino 的)语法非常类似于 Java 编程语言。这两种语言都采用了与 Java 编程语言相似的循环和条件结构,并且遵循类似的语法模式来表示这些结构。
历史简介
在开始学习 Rhino 基础知识之前,先了解一下有关 JavaScript 的起源和目的可能会非常有用,JavaScript 为 Rhino 提供了很多独特的特性。JavaScript 的历史与 Web 浏览器动态表示和操作 Web 页面内容的能力是相符合的。JavaScript 的第一个版本(最初称为 LiveScript)是由 Netscape Communications 公司于 1995 年发布的,它是 Netscape Navigator 2.0 Web 浏览器的一部分。JavaScript 打算为程序员提供一种简单而直观的方法,编写一些可以在 Web 页面上下文中执行任务的简单脚本。在随后的一年中,Microsoft 引入了 JScript,它自己的用于 Internet Explorer 的 JavaScript 端口。
这两个版本的 JavaScript 都包括一个基于对象的 API,称为 文档对象模型(Document Object Model)或 DOM,用以访问和操作 Web 页面的内容。JavaScript 的第三个实现是一种新的脚本语言,称为 EcmaScript,其目的是对自己和 DOM 进行标准化。不幸的是,Microsoft 和 Netscape 都没有完全实现 EcmaScript 标准,因此到今天都还存在兼容方面的问题。
随着 Java 语言在 20 世纪 90 年代末期取得的成功,Netscape 计划发布 Javagator,它是 Navigator 中一个 100% 的纯 Java 实现。虽然 Javagator 从来没能开花结果,但是 Netscape 对 JavaScript 的移植(称为 Rhino)已经经过时间的考验存活了下来。Rhino 是 JavaScript 1.5 脚本语言的一个 100% 的纯 Java 实现,不包含 DOM API。实际上,Rhino 有时仍然被当作 Netscape 的基于 Java 的 JavaScript。
JavaScript 中的 \"Java\"
在 Rhino 的第一个发行版本中,Netscape 显然想利用 Java 编程语言所取得的成功。在该公司自己的脚本语言中,很明显地从 Java 语言中借用了一些基本的语法。这使得它特别适合 Java 开发人员学习和使用。例如,考虑一下清单 1 中给出的 Java 函数与 Rhino 函数之间的相似性,Rhino 函数测试了给定的数字是否为素数:
清单 1. Rhino 函数:这个数字是素数吗? function isPrime (num) {
if (num <= 1) {
print(\"Please enter a positive integer >= 2.\") return false }
var prime = true
var sqrRoot = Math.round(Math.sqrt(num)) for (var n = 2; prime & n <= sqrRoot; ++n) { prime = (num % n != 0) }
return prime }
除了几处例外,该程序与 Java 程序非常类似:
圆括号用来分隔代码块。
for 和 if 结构的语法与 Java 语言的相同。
Rhino 与 Java 语言采用相同的算术和条件操作符(例如,对 sqrRoot变量的赋值),甚至可以支持类似的访问其他算术函数的方法。
Rhino 允许使用预先定义的布尔常量 true 和 false 。 虽然没有在这里显示,但您应该注意到,Rhino 的 while 和 do...while 循环的结构都与 Java 语言的相同。
具体区别
当然,Rhino 和 Java 语言之间有一些显著的区别。首先,由于 Rhino 是采用动态类型的语言,因此在函数和变量的声明中看不到类型。您可以使用 function 关键字开始函数的声明,使用 var 关键字来声明局部变量(这与全局变量不同),但是不用包括正在声明的变量的类型。Rhino 运行库将在执行过程中推断变量的类型。与 Java 语言不同,Rhino 没有语句结束符(在 Java 语言中是分号),不过它也可以支持语句结束符,这是可选的。 数组
Rhino 中的数组可以表示为放在方括号中的一串使用逗号分隔的数值列表。因此,下面就是 Rhino 中的数组:
numbers = [0, 1, 2, 3, 5, 24]
names = [\"Mike\ arrayOfArrays = [1, [2, 3], [3, 4, 5, [6, 7]], 8]
第三个例子说明数组中的元素不一定非得是简单类型。
联合数组
联合数组(associative array)是另外一种数据类型,它可以表示为一个字符串。联合数组在其他语言中有时称为 词典(dictionary)或 hash 表,它是一系列关键字-值对,关键字和值使用冒号(:)分隔开。Rhino 中的联合数组的作用与 Java 语言中的 java.util.HashMap 非常相似。下面是 Rhino 中的联合数组:
person = {name:\"Mike Squillace\ link = {text:\"IBM Home\
可以使用两种方法来引用该列表中的元素。要设置刚才定义的 person hash 表中的 age 属性,您可以这样使用: person[\"age\"] = 39 ,或者使用 person.age = 39 。要读取该值并将其保存在变量 myAge 中,您可以这样使用: myAge = person[\"age\"] ,或者使用 myAge = person.age .
Rhino 将典型的有索引的数组作为一种特殊的联合数组来对待:它们只是一些关键字是正整数的联合数组。因此,下面这两行代码实际上定义的是完全相同的数组:
a1 = [\"fee\
a2 = {0:\"fee\
通过属性进行循环
Rhino 提供了一种特殊的循环结构: for...in 结构,它可以通过联合数组中的属性进行循环。下列代码输出了刚才定义的 person 联合数组,以及这些属性的值:
for (prop in person) {
print(\"person[\" + prop + \"] = \" + person[prop]) }
当然,我是通过一个类 hash 表结构实现循环的,因此不能保证每个属性及其值都可以输出。
正则表达式
与数组类似,在 Rhino 中也可以使用 正则表达式来表示文本,使用 Perl 和其他脚本语言的用户应该非常熟悉其语法。在 Rhino 中,当表示为文字值时,正则表达式是通过正向斜线(/)来分隔的。
在 Rhino 中,正则表达式被传递给字符串对象的方法,以便更简单地执行文本处理任务。例如,下面的第一行代码定义了一个正则表达式,它可以匹配算术表达式中的正整数和标准算术操作符。第二行代码通过调用 match 函数处理给定的表达式,如下所示:
tokenExpr = /\\d+|[\\+\\-\\*\\/]/g
\"38-4+98/5\".match(tokenExpr)
结果是生成一个 Rhino 字符串数组,数组的元素包含以下内容:
\"38\ \"/\
函数显式声明
最后,Rhino 提供了 函数数据类型。正如前面介绍的那样,Rhino 作为第一类数据类型支持函数 —— 可以从函数中返回,也可以传递到函数中,还可以在变量声明中使用。因此,我可以在解释器提示符中编写下面的代码,并得到结果 9,这是定义平方函数的期望结果:
square = function (x) { return x * x }
按照这个平方函数的定义,我输入了以下内容:
square(3)
这样定义的函数不但可以用来处理文本,还可以用来处理其他数据。例如,我可以定义一个如下所示的联合数组函数:
fnList = {
square:function (x) {return x * x}, cube:function (x) {return x * x * x}, sqrt:function (x) {return Math.sqrt(x)} }
然后我可以对数组中的列表循环调用该函数,输出每个函数的值,就像它是一个数字一样。例如,我可以编写下面的代码:
for (fnName in fnList) {
print(\"The \" + fnName + \" of \" + 3 + \" is \" + fnList[fnName](3)) }
将获得如下所示结果:
The square of 3 is 9
the sqrt of 3 is 1.7320508075688772 The cube of 3 is 27 Rhino 中的对象
在使用联合数组并且将函数表示为文本之后,就可以在 Rhino 中将任何对象表示为联合数组。实际上,对象的文本表示只不过是一个联合数组,它可能包含某些函数作为一些值。下面这个例子将展示在 Rhino 中使用对象是多么简单,Rhino 处理这些对象表现得多么强大。在开始这个例子之前,请再次考虑 person hash 表的例子:
person = {name:\"Mike Squillace\
在 Rhino 中,这是一个联合数组的文本表示,不过更确切的说,它是一个对象的文本表示。这种表示在 Rhino 中也称为 对象初始化。刚才定义的值的类型是由解释器在输入上面的定义之后根据下面的代码进行判断的:
[object Object]
上面的代码说明了变量 person 中存放的值是 Object类型的。
添加方法
然后,我将向您展示,在重新定义 person 对象,以包含检索该对象的第一个名称的函数时,会出现什么样的情况。我将通过编写下面的代码来展示这一点:
person = {
name:\"Mike Squillace\ age:37,
position:\"software engineer\
getFirstName:function () {return this.name.split(\" \")[0]} } 该函数(更确切地说是方法) getFirstName 使用了 this 指针来引用当前的对象,并对 name 属性调用 split 方法。然后, split 方法返回一个数组,其中保存了根据空格字符将给定字符串分割成子字符串的结果。最后取得该数组的第一个值并返回。
我们已经很熟悉调用新的 getFirstName 函数的方式,如下所示:
person.getFirstName()
圆括号告诉 Rhino 解释器我正在调用一个函数,而不是简单地引用一个对象的属性。然而要注意的是,该函数本身也只是另一个 person 对象的属性,如果没有圆括号,它将引用一个未定义的值。
添加更多的方法
Rhino 还可以允许动态地为对象添加属性和方法。例如,如果想添加一个检索通过 person 对象表示的某人名字的方法,那么只需简单输入下列代码即可:
person.getLastName = function () {return this.name.split(\" \")[1]}
现在,当我输入下面的方法时,就可以得到想要的结果,在本例中,这个结果是 \"Squillace\":
person.getLastName()
(注意,您可以使用 delete 操作符来删除任何属性,例如 delete person.getLastName 。)
使用原型
虽然上面这个例子非常有趣,但是您可能不希望一直使用对象初始化来定义个别某些人。幸运的是,Rhino 提供了另外一种创建对象的方法:使用 构造函数(constructor function)。例如,下面的函数可以作为一名 Person 对象的构造函数:
function Person (name, age, job) {
this.name = name || \" this.job = job || \" this.getFirstName = function () {return this.name.split(\" \")[0]} this.getLastName = function () {return this.name.split(\" \")[1]} } 拥有构造函数之后,就可以使用 new 操作符来创建对象了,如下所示: mike = new Person(\"Mike Squillace\ 任何函数都可以用作构造函数,不过通常希望使用定义(使用 this 指针来引用正在定义的对象)对象属性的函数以及为这些属性赋值(或函数)的函数作为构造函数。 无类编码 作为一个 Java 开发人员,您很可能会认为下一个步骤是在 Rhino 中定义 Person 类。实际上,Rhino 并不需要这样定义类 —— 因为它根本就不使用类!在 Rhino 中既没有类,也没有类的实例,只有特定的对象。当调用 new 操作符时,构造函数就为对象创建一个所谓的 原型(prototype);也就是说,它创建了一个 模板(template),从中构建给定类型的对象。 基于类的语言与基于原型的语言的比较 诸如 C++ 和 Java 之类的 基于类的语言使用一个类定义来表示一组具有特定属性集的对象,包括用来表示类的实例和这些实例可以执行的任务类型的数据。一旦编写并编译了类定义,就不能在运行时再对其进行修改,该列的所有实例都包含该类中定义的数据和方法(也可能 只有这些数据或方法)。在这种情况下,所有的类都是 Class 类的实例。 基于原型的语言并不区分类和实例;它们只识别基于原型或模板的特殊对象。原型定义了对 象初始化时使用的属性。基于原型的实例的属性以及原型本身在任何时间都可以修改。因此,不需要一个“类”对象或数据类型。从某种意义上来说,我们可以认为基于原型的语言是基于对象的差异性,而不是基于对象的共同性。 在诸如 Rhino 之类的基于原型的语言中,您可以修改特定对象的属性,或者其原型的属性。例如,如果想为刚才定义的 mike 对象中添加一个特殊属性,可以使用下面的方法: mike.disability = \"blind\" 我还可以通过引用 Person 构造函数的原型属性来修改 Person 的属性。如果以后想为所有从这个构造函数中派生出来的对象都添加一个 birthdate 属性,可以使用下面的方法: Person.prototype.birthdate = null 然后使用: mike.birthDate = new Date(66, 10, 3) // months are zero-based: this is 11/3/66 还要注意基于 Person 原型创建的新对象都有 birthdate 属性,因此下面的代码是有效的: jami = new Person(\"Jami Bomer\ jami.birthdate = new Date(79, 5, 28) 因篇幅问题不能全部显示,请点此查看更多更全内容