Aurora ???????

2012-08-15 13:50:09by jessen

Aurora 服务端脚本支持

周升洋

Hand

2012-08-13

为什么要引入脚本

以Aurora框架为基础的项目开发中,业务逻辑以及数据校验一般都会写到package里面,最终在数据库中执行.这会增加数据库的负载,增加相应时间.实际应用中,大部分数据校验工作都可以在中间层(应用服务器)完成,甚至一些简单的逻辑也不一定非得写到package。

在此之前,中间层可以通过svc来控制流程,也可以通过已经开发好的标签进行数据加工和一些简单的逻辑校验操作.想要依靠svc完成复杂的操作,则需要开发者开发新的标签。

服务端脚本的引入,可以使中间层能做更多的事,能减小数据库的压力,缩短请求相应时间,充分利用应用服务器的计算能力。

运行环境

服务端脚本运行于后台服务器,和浏览器没有任何关系。

如果要使用脚本功能,则须保证jdk1.6以上,aurora-plugin.jar请尽量保证最新.同时需要aurora-js.jar包.jar包置于<WEB_HOME>/WEB-INF/lib目录,点此下载

内建对象介绍

我们实现了几个js对象,用于代理java中的对象,并为它们设计了api,具体的细节如下

表 1. 代理对象

对象名说明
CompositeMap对应uncertain.composite.CompositeMap
ModelService用于操作bm
Session用于操作HttpSession
Cookie用于操作Cookie
ActionEntry用于调用原有标签式action

CompositeMap和ModelService,可以随便创建.但是不建议创建Session,Cookie,取而代之的是使用他们的唯一引用$session,$cookie,这会在下面介绍。

CompositeMap

CompositeMap代理了框架java中uncertain.composite.CompositeMap,并实现了大多数常用的方法。

表 2. CompositeMap的属性和方法

CompositeMap()构造方法
CompositeMap(name)构造方法,指定节点名
CompositeMap(p,uri,name)构造方法,指定prefix,namespace uri,name
CompositeMap(map)构造方法,复制一个CompositeMap
get(path)XPath操作,取值
put(path,value)XPath操作,设值
getChildren()取得所有子节点,作为Array返回,没有子节点时返回空Array
addChild(child)添加一个CompositeMap,作为子节点
getChild(name)根据名字查找一个子节点
createChild(name)根据给定节点名,新建一个子节点
createChildByTag(xpath)根据给定xpath,新建一个节点
removeChild(child)移除子节点
toXML()转换为xml
toString()简洁概要 字符串
getData()取得对应的java CompositeMap
['xxx']属性操作,可读/写
.xxx属性操作,可读/写(要求属性名不能为特殊变量)
getParent()取得父节点
getRoot()取得根节点
createChildNS(name)创建子节点,并且copy当前节点的namespace,prefix
setText(text)设置CDATA文本
getText()取得CDATA文本

例 1. 用法示例

var map = new CompositeMap('a');
map.key1 = 'value1';
var m = new CompositeMap('c');
map.addChild(m);
m.put('@key', 'value');
println(m['key'])
println(map.get('/c/@key'));
println(map.toXML())

$ctx是一个特殊CompositeMap对象,它指代当前的context。

为了方便访问,我们为$ctx多加了几个特殊的"属性",用来指代常用子节点,这些值不能用来当作key存放普通属性值。

表 3. $ctx特殊属性

$ctx.parametercontext的parameter节点
$ctx.sessioncontext的session节点
$ctx.cookiecontext的cookie节点
$ctx.modelcontext的model节点

ModelService

ModelService 代理了框架 中 aurora.database.service.BusinessModelService 类,实现了几个常用方法

表 4. ModelService的属性和方法

ModelService(bm)构造方法,指定bm的名字,形如sys.sys_user
fetchDescriptorjson形式设置fetchDescriptor,可读/写
optionjson形式设置service_options,可读/写
executeDml(paraMap,operation)根据参数map,指定操作(Insert,Update,Delete,Execute),执行DML操作
execute(paraMap)executeDml(paraMap,'Execute'),默认paraMap为parameter
insert(paraMap)executeDml(paraMap,'Insert'),默认paraMap为parameter
update(paraMap)executeDml(paraMap,'Update'),默认paraMap为parameter
delete(paraMap)executeDml(paraMap,'Delete'),默认paraMap为parameter
queryAsMap(paraMap)查询数据,返回一个包含数据的CompositeMap,默认paraMap为parameter
queryIntoMap(paraMap)查询数据,放于指定CompositeMap,默认paraMap为parameter
query()查询数据,等价model-query,参数取自option,参数map为parameter

例 2. 用法示例

var bm = new ModelService('sys.sys_user');
bm.fetchDescriptor = {
    pagesize: 3,
	pagenum: 2,
	fetchAll:true
};
/**
 * fetchDescriptor 如果没设置,或设置为无效值,则会被重置为FetchDescriptor.fetchAll()
 * fetchDescriptor 有3个有用的属性
 *   pagesize 分页大小,数字或字符类型的数字(10,'10')
 *   pagenum  页码,数字或字符类型的数字(10,'10')
 *   fetchAll(fetchall) 默认false,若为true,则pagenum,pagesize无效
 * pagesize,pagenum 用于计算offset,offset不可直接设置
 *
 * var fd = bm.fetchDescriptor 返回的为json对象
 * 此对象包含4个值,除了上面3个,还有offset
 * 修改此对象不会影响bm,如果想要起作用,需要重新赋值:
 * fd.pagenum = 3;
 * bm.fetchDescriptor = fd;
 */

var res = bm.queryAsMap();
var arr = res.getChildren();
for (i = 0;i < arr.length;i++) {
    println(arr[i].user_name);
}

$ctx.parameter.user_id = 21;
bm.option = {
    rootPath: 'rp_user'
};
/**
 * option 可以设置的值有:
 * rootPath
 * autoCount
 * defaultWhereClause
 * queryOrderBy
 * connectionName
 * fieldCase
 * recordName
 * updatePassedFieldOnly
 */

var res = bm.queryAsMap();
copyAttribute(p.createChild('fd'), bm.fetchDescriptor);
p.addChild(res);

bm.query();
println($ctx.get('/model/rp_user/record/@user_name'))

Session

Session 主要代理了 HttpSessionOperate 类的方法,可以在js中通过它来操作HttpSession。

Session 存在唯一引用:$session,不需要创建( new Session() )

表 5. $session的属性和方法

create()如果session尚未创建,则创建一个session
write(target,source)将source(xpath)位置的值 以target作为key写到session,同时写到context的session节点上
writeValue(target,value)功能类似write,但需要具体的值,而不是路径
clear()清空session
copy()将当前session中的所有值,copy到context中session节点上
toXML()代理context中session节点的 toXML() 方法
['xxx']读:代理context中session节点的 ['xxx'] 方法;写:相当于writeValue(新特性)
.xxx读:代理context中session节点的 .xxx 方法;写:相当于writeValue(新特性)

例 3. 用法示例

$session.clear();
$session.write('session_id', '/parameter/@session_id');
//上句等价于$session.writeValue('session_id', $ctx.parameter.session_id);
$session.write('lang', '/parameter/@user_language');
$session.write('is_ipad', '/parameter/@is_ipad');
$session.write('user_id', '/model/rp_user/record/@user_id');
println($session.toXML());
//新特性
$session.aaa = 1;//等价于$session.writeValue('aaa', 1);

Cookie

Cookie 代理了 AuroraCookie 类的方法。

Cookie 存在唯一引用 :$cookie,不需要创建( new Cookie() )

表 6. $cookie的属性和方法

put(name,value,maxage)向cookie中存放值,maxage默认值-1(关闭浏览器失效)
[xxx']put的简化版,读/写
.xxx同上

例 4. 用法示例

$cookie.put('IS_NTLM', 'N');

ActionEntry

标签式操作是框架原有的,它们和框架紧密结合在一起,难以完全由js替代,故提供一种方法来调用这些action。

要构造一个ActionEntry需要2个参数,namespace,tag-name,如果只指定了一个参数tag-name,则namespace默认uncertain.proc。

在执行ActionEntry时需要同时传入参数.(参数名区分大小写,参数值为字符串)

例 5. 用法示例

var ns_a = 'aurora.application.action';
var ae = new ActionEntry(ns_a, 'resource-access-check');
ae.run({
    resultPath: '/access-check/@status_code'
});

内建函数介绍

内建函数主要都是工具函数,它们都是全局函数。

表 7. 内建函数

print(obj,newline),println(obj)打印信息到控制台,可以起到调试的作用
raise_app_error(err_code)

用户可以显式终止当前代码的执行,并报告错误代码,错误代码会被框架翻译成可读的错误消息,最终显示给用户。

一些数据校验操作,之前写在package里面在数据库中执行,现在可以在中间层就做掉。

$bm(bm_name,option)构造一个ModelService对象,如果传入option参数,则会同时设置option。
$instance(clsName)根据完整java类名,从IObjectRegistry中取出它的实例.
$cache(cacheName)得到一个已定义的cache实例。(cache配置清参考cache.config文件)
$config()取到当前的ServiceConfigData的根节点。
$logger(topic)取到与当前文件相关联的ILogger实例,可以输出信息到日志文件(topic,默认server-script)。

如何开始

一个完整的小例子。

<?xml version="1.0" encoding="UTF-8"?>
<a:service xmlns:s="aurora.plugin.script" xmlns:a="http://www.aurora-framework.org/application">
    <a:init-procedure>
        <s:server-script><![CDATA[
            var p = $ctx.parameter;
            if (p.a) println('a=' + p.a);
            else println('a is not passed in.');
            p.b = 'sss'
        ]]></s:server-script>
    </a:init-procedure>
    <a:service-output output="/parameter"/>
</a:service>

为了可以使用命名空间aurora.plugin.script中的server-script标签,必须首先确保uncertain.xml中包含相关的内容:

<package-path classPath="aurora_plugin_package/aurora.plugin.script"/>

由这个小例子可以看出script是如何集成到svc(或screen)中的。通过server-script标签,可以直接书写大段的js。

请注意:server-script标签只能写在init-procedure标签下面。server-script标签可以出现多次,但是我们不建议这么做。

自定义库

为了增加灵活性,我们允许开发人员可以编写一些通用的函数,放到一个或多个js文件中,然后在多个地方调用。

对于很长的js片段,我们也建议单独编写,然后简单的需要的地方引用。

标准js库路径为:<WEB_HOME>/WEB-INF/server-script/

导入的js文件必须位于WEB-INF目录下,建议都放在server-script目录中,目录层次可根据需要自行划分。

假设<WEB_HOME>/WEB-INF/server-script/a.js的内容如下

function sayHello(){
    println('hello');
}

那么我们可以这么用

<s:server-script import='a.js'><![CDATA[
    sayHello();
]]></s:server-script>

通过server-script标签的import属性,开发者可以导入外部的js库(其本质是提前执行一下)。

可以同时导入多个,用';'分割。import = 'a.js;b.js'。

外部js文件之间如果存在依赖关系,import的时候需要同时引入,且保证先后顺序。

一个完整的例子--login

这是我们用js改造的登录过程,login.svc

<?xml version="1.0" encoding="UTF-8"?>
<a:service xmlns:s="aurora.plugin.script" xmlns:a="http://www.aurora-framework.org/application">
    <a:init-procedure>
        <s:server-script import='tool.js'><![CDATA[
            var p = $ctx.parameter
            // 首先判断是否已经具备必要的登录信息
            if (!p.user_name) raise_app_error('Please Input UserName');
            if (!p.user_password) raise_app_error('SYS_PASSWORD_NULL');
            // 根据parameter中的user_name 查询用户
            bm = $bm('sys.login.sys_login', {
                rootPath: 'rp_user'
            });
            bm.query();
            user = $ctx.get('/model/rp_user/record');
            // 如果没查到任何用户,则创建一个虚拟的来代替,以便下面的逻辑一致
            if (!user) user = new CompositeMap();
            // 将用户输入的密码进行md5加密,以便与数据库中的比对
            p.md5_user_password = md5(p.user_password);
            // 将用户选择的语言环境标识写入session
            $session.write('lang', '/parameter/@user_language');
            // 进行各种校验...
            if (!user.user_id) raise_app_error('SYS_USER_NULL');
            if ('Y' == user.frozen_flag) raise_app_error('SYS_FROZEN_FAILURE');
            if (!user.nls_language) raise_app_error('SYS_LANGUAGE_NULL');
            if (p.md5_user_password != user.encrypted_user_password) raise_app_error('SYS_PASSWORD_FAILURE');
            if (!user.start_date) raise_app_error('SYS_USER_FAILURE');
            // 校验通过,允许登录,开始更新用户登录信息
            p.user_id = $session.user_id = user.user_id;
            $bm('sys.login.sys_user_login').update();
            p.encrypted_session_id = 'temp';
            p.client_ip_address = $ctx.get('/request/@address');
            // 向数据库中插入session记录,将返回的session_id加密,再更新回去
            $bm('sys.login.sys_session').insert();
            p.encrypted_session_id = des_encrypt(p.session_id);
            bm = $bm('sys.login.sys_session', {
                updatePassedFieldOnly: true
            });
            bm.update();
            // 将必要的信息写入session
            $session.clear();
            $session.write('session_id', '/parameter/@session_id');
            $session.write('lang', '/parameter/@user_language');
            $session.write('is_ipad', '/parameter/@is_ipad');
            $session.write('user_id', '/model/rp_user/record/@user_id');
            $cookie.put('JSID', p.encrypted_session_id);
            $cookie.put('IS_NTLM', 'N');
        ]]></s:server-script>
    </a:init-procedure>
    <a:service-output output="/parameter"/>
</a:service>

使用java原生对象

这是一个高级的主题。

我们的引擎允许直接在javascript中使用java的类,使用java类来创建对象,并且可以使用类中的方法。

作为开发者,我们建议,javascript最好是去调用java对象中写好的方法,而应该尽量避免直接编写大量的复杂的代码。也就是说,javascript充当了一个代理的角色。

这可以很好的避免性能问题,也可以曾加代码的重复利用率。

怎么样在javascript中准确的使用java的各种特性是一个复杂的话题,我们在此不做深入讨论。下面只介绍简单的模式:得到一个对象,调用他的方法

调用JAVA 代码

在server-script中使用java类,首先需要将这些类import进来,否则就只能使用一个很长的全名限定(类似java中的完整类名) 。

比较常见的应用场景是:调用第三方提供的java API。

下面的小例子会创建一个类的实例,然后传参数调用他的方法。

例 6. 使用java类

importPackage(Packages.com.hand.api);//效果等价于java中的import com.hand.api.*;不会自动导入子包
var apiInstance = new SomeAPI();//server-script中定义变量一律使用var,这点不同于java
var returnedValue = apiInstance.execute("param1",2,true);//调用方法,传参
println(returnedValue);//查看返回的值

returnedValue = SomeAPI2.getSomeValue(1,2,3);//静态调用也是可以的

//使用importPackage批量导入类的时候,可能会因为和其他包里面的类重名 而出现一些问题
//这时候也可以单独import一个类,如下
importClass(Packages.com.hand.api.SomeAPI);//仅导入SomeAPI,其他类不可用
//也可以不用import, 使用完整类名限定,如下
apiInstance = new Packages.com.hand.api.SomeAPI();


关于前缀Packages.,当包名以java开头时,可以省略这个前缀,否则必须写!

需要注意的地方是,参数的类型,以及返回值的类型。基本的数据类型,引擎会自动转换,以匹配目标方法的参数类型。多态是在运行时期确定的,不是编译时期!

对于java中有而javascript中没有的概念(比如:byte,char,interface),我们有相应的办法去实现,但是比较不常见

例 7. 特殊的东西

//1.定义一个java类型数组
var byteArray =  java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE,4);
//以上代码创建了一个java类型的byte数组,长度是4
//2.实现一个接口
var runnable = new java.lang.Runnable({
	run : function(){
		println("run");
	}
});


IObjectRegistry

框架在运行时需要的全局对象都会注册在IObjectRegistry(的一个实例)中,开发人员可以在需要的时候把它们取出来,利用它们来做一些复杂的事情。

我们允许开发人员在script中取出这些对象,并且可以按照和java一样的方式来使用他们。

下面的小例子会调用框架的多语言机制,将消息代码转换为具体的消息。

例 8. IObjectRegistry用法示例

var messageProvider = $instance('aurora.i18n.IMessageProvider');//取得IMessageProvider实例
var msg_code = 'SYS_PASSWORD_FAILURE';
var msg = messageProvider.getMessage('ZHS',msg_code);//取出对应中文消息
$ctx.parameter.msg = String(msg)
//可以在返回的parameter中看到转换后的消息

$instance(clsName)函数会根据传入的完整类名取到一个已经注册过的实例,然后就可以调用它的各种方法。

IObjectRegistry中保存有很多对象,利用它们可以在script中实现一些之前必须在java中才能实现的事。

Cache

利用cache机制可以避免重复执行一些耗时的操作,从而提升响应速度。

Aurora框架本身有一套cache机制,但这套机制只被框架本身利用,普通开发者只能通过配置去选择启用与否。开发者想要根据自己的需要随便存取数据则比较困难。

现在,我们允许开发者在script中直接进行cache操作。这可以增加cache的灵活性和利用率。

下面是一个例子,向cache中放一个值,稍后在取出来

例 9. $cache()用法示例1

var cache = $cache('DataCache');
var key = 'cache.test.key';
var value = cache.getValue(key);
if(value == null){
    println('no data found in cache, set it.');
    cache.setValue('cache.test.key','abcdefg');
}else {
    println('found it in cache.' + value)
}

多次调用此段script,会发现,第一次时没有找到数据,第二次以后都能够找到。

$cache(cacheName)函数会查找一个已经按名字注册过的cache实例,所有的cache实例配置都在<WEB_HOME>/WEB-INF/aurora.feature/cache.config中。

在测试这段代码之前,需要确保配置有效,如果是临时更改则需要重启web服务器,如果是jvm外的cache服务器,也要首先保证它正常运行。

上面的例子仅仅是对基本数据类型的存取,实际应用中,需要cache的数据往往都是一个可序列化的java对象。比如,数据库查询结果,通常会放在一个CompositeMap对象中,java的CompositeMap是可以序列化的,而js的CompositeMap不可序列化,这之间要做一个转换。

例 10. $cache()用法示例2

var cache = $cache('DataCache');
var key = 'cache.test.users';
var value = cache.getValue(key);
if(value == null){
    println('no data found in cache ,query');
    var bm = $bm('sys.sys_user');
    var users = bm.queryAsMap();
    cache.setValue(key,users.getData());//取得对应java CompositeMap(可序列化)
}else{
    var users = new CompositeMap(value);//重新构造一个js CompositeMap
    println(users.getChildren().length);//打印记录数
}

上面的例子展示了如何使用cache存取复杂java对象,这和基本数据类型没有区别(对用户来讲),但要注意:对象必须为可序列化的java对象。

动态修改screen

和svc文件一样,screen文件的init-procedure标签中也可以其嵌入server-script标签。

在一次页面请求中,screen文件会被解析成CompositeMap对象(假设为config),这个config对象包含了screen本身的所有原始的节点信息,接下来init-procedure被调用,这个过程可能会加载一些数据,服务端脚本也会被执行.init-procedure执行完之后,框架会根据config和当前的context来把screen变成html..。

由此可见,服务端脚本有机会在html生成之前修改config对象,从而影响最终生成的html。

下面是一个简单的例子,动态修改login2.screen中登录按钮的显示文本.并插入一个div显示当前时间

例 11. $config()用法示例

var config = $config(); //取到config对象
var btn = CompositeUtil.findChild(config, 'button', 'id', 'btn_1'); //查找登录按钮
btn.put('text', 'new text'); //将登录按钮文本改为new text
var m = new CompositeMap('div').getData();//创建一个java CompositeMap
m.setText(new Date().toString());//设置文本
btn.getParent().getParent().addChild(m);//插入节点

$config()函数会将当前文件的ServiceConfigData返回,同时将CompositeUtil类导出(CompositeUtil类提供一些方法来操作CompositeMap)。

查找到想要操作的节点后,就可以根据需要修改,甚至可以删除,新增。

关于异常

通常,使用server-script动态修改(创建)screen,都需要使用bm进行一些数据操作,如果bm执行过程中发生异常,可能会根据异常来生成相应的错误展示信息。这时候需要使用try-catch来讲可能发生异常的代码包起来,捕获到异常以后,使用以后,执行相应的代码。

var config = $config(); // 取到config对象
try {
    $bm("test.test").execute();// 会发生错误的代码
} catch (e) {
    var javaException = e.javaException;
    // 如果javaException无效,则表示此异常不是由java中抛出的异常,它应该是script语法错误之类的错误
    if (javaException) {
        // 拿到rootCause
        while (javaException && javaException.getCause
                && javaException.getCause()) {
            javaException = javaException.getCause();
        }
        var msg = javaException.getMessage();
        var view = CompositeUtil.findChid(config, "view");
        view.getChildsNotNull().clear();// 移除所有其他节点
        view.createChild("pre").setText(msg);// 添加新节点展示错误信息
    }
}

如果希望通过框架的异常处理机制来取得错误消息(通常是pkg中抛出的自定义异常),则可参照如下代码:

//这个函数可以处理所有类型的异常
function parseErrorMessage(e) {
    if (e.javaException)
        e = e.javaException;
    else
        return e.message;
    while (e && e.getCause && e.getCause())
        e = e.getCause();
    var serviceContext = Packages.aurora.service.ServiceContext
            .createServiceContext($ctx.getData());
    var ed = $instance('aurora.service.exception.IExceptionDescriptor')
    var map = new CompositeMap(ed.process(serviceContext, e));
    return map.message;
}

BM中使用脚本

bm中的脚本是以feature的形式提供的。它可以在bm执行之前,动态的修改bm中的各种属性。目前bm中除了feature,exception-descriptor-config节点不能动态修改以外,其他内容基本上都可以修改。

例 12. bm-script例子

<?xml version="1.0" encoding="UTF-8"?>
<bm:model xmlns:e="aurora.service.exception" xmlns:s="aurora.plugin.script" xmlns:bm="http://www.aurora-framework.org/schema/bm" xmlns:f="aurora.database.features">
    <bm:fields/>
    <bm:features>
        <f:standard-who/>
        <s:bm-script><![CDATA[
            $this.setBaseTable("sys_user");
            var f = Packages.aurora.bm.Field.createField('user_name');
            f.setDatabaseType('VARCHAR2');
            f.setDataType('java.lang.String');
            $this.addField(f);
            f = Packages.aurora.bm.Field.createField('user_id');
            f.setDatabaseType('NUMBER');
            f.setDataType('java.lang.Long');
            $this.addField(f);
            f = Packages.aurora.bm.Field.createField('encrypted_foundation_password');
            f.setDatabaseType('VARCHAR2');
            f.setDataType('java.lang.String');
            $this.addField(f);
            f = Packages.aurora.bm.Field.createField('encrypted_user_password');
            f.setDatabaseType('VARCHAR2');
            f.setDataType('java.lang.String');
            $this.addField(f);
            f = Packages.aurora.bm.Field.createField('start_date');
            f.setDatabaseType('DATE');
            f.setDataType('java.util.Date');
            $this.addField(f);
            var model = $this.getObjectContext();
            println(model.toXML());
        ]]></s:bm-script>
    </bm:features>
</bm:model>

Demo

    Comments

    0 Responses to the article

    暂时没有评论。

    发表评论