除了前面介绍的基本调用以外,在编制后端脚本的时候经常需要调用除了DWF以外的外部服务,或者其他的类库。造成这种局面的原因有可能(但不限于)是由于:
- 大量第三方api是以restful api封装,直接从前端调用存在跨域的问题,必须从后端调用
- 需要引用第三方类库并进行调用,例如:图像处理等。
本章将以restful api调用为例,介绍如何通过后端脚本调用DWF内置的Java类库和第三方restful api服务。
1 基本概念
1.1 后端脚本调用Java类库方法
DWF的后端脚本虽然使用javascript作为自身的主要开发语言,但实际上,后端脚本的运行是允许开发者调用java的类库,因此,理论上可以实现大部分Java可以实现的后端功能。为了实现Java调用,需要遵循一些调用模式,具体如下:
类的引用和对象实例化
在java中引用类的方式,使用:import "类的全路径"的方式实现,在脚本中使用Java.type实现:
var FileClass = Java.type('java.io.File');
引用完成后,可以使用new直接实例化对象,这一点和java使用方法一致:
var FileClass = Java.type('java.io.File'); var file = new FileClass("myFile.md");
对象属性访问和函数调用
在脚本中访问对象的属性和访问和java中访问的方式是一样的,例如:
var JavaPI = Java.type('java.lang.Math').PI;
调用函数也是一样,如下所示:
var file = new (Java.type('java.io.File'))("test.md"); var fileName = file.getName();
访问集合对象
除了对单个对象的访问以外,还可以访问集合对象,例如:数组、键值表等,如下面的代码所示:
var HashMap = Java.type('java.util.HashMap'); var map = new HashMap(); map.put(1, "a"); map.get(1);
关于在后端脚本调用java类的方法可以参考这个文档:https://www.graalvm.org/reference-manual/js/JavaInteroperability/
1.2 后端脚本调用restful api
了解脚本调用Java的方法以后,接下来看看在后端调用restful api的方法。DWF后端采用java开发,没有一个浏览器的环境内置的restful api调用,所以,正确的方式是利用springboot提供的restful api调用方式实现对第三方服务的调用。
这里简单介绍几个相关的类和方法,以便后面的讨论:
请求头设置
在调用restful api的时候,经常要求设置请求头header,例如:cookies,可以利用新建HttpHeader类的对象来实现,即:Java.type('org.springframework.http.HttpHeaders'),例程如下:
//设置请求头 var HttpHeaders = Java.type('org.springframework.http.HttpHeaders'); var MediaType = Java.type('org.springframework.http.MediaType'); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
其中:setContentType接受一个常量,用于设置请求头这个常量的定义可以直接通过引用Java.type('org.springframework.http.MediaType')来获得。这里我们使用了APPLICATION_FORM_URLENCODED。
请求参数设置
除了请求头header以外,调用restful api的时候更重要的是设置请求参数,这通过新建HttpEntity类的对象实现,即:Java.type('org.springframework.http.HttpEntity'),例程如下:
//设置请求体 var LinkedMultiValueMap = Java.type('org.springframework.util.LinkedMultiValueMap'); var request_body= new LinkedMultiValueMap(); request_body.add("image", imgValue); var HttpEntity = Java.type('org.springframework.http.HttpEntity'); request = new HttpEntity(request_body, headers);
在这个程序里:
先新建了一个键值对对象,即:Java.type('org.springframework.util.LinkedMultiValueMap'),调用add方法表示请求的JSON对象的键值对。
然后新建了一个完整的请求对象,即:Java.type('org.springframework.http.HttpEntity'),初始化的时候给出请求体和请求头。
this.restTemplate发出请求获得结果
最后,通过RestTemplate类实现常见的REST请求的模版,例如 GET 请求、POST 请求、PUT 请求、DELETE 请求。
DWF的后端脚本已经为开发者提供了一个RestTemplate类的实例,并且为开发者初始化了一系列参数,例如:防止乱码的UTF-8的字符集等。
通过:this.restTemplate即可开始调用,当然,也可以自行新建一个RestTemplate来实现调用。
下面是一个post调用实现的代码:
// 发出HTTP请求 var url = "https://aip.baidubce.com/rest/2.0/image-classify/v1/car?access_token=1111"; post_response = this.restTemplate.postForEntity(url, request, Java.type('java.lang.String').class); var body = post_response.getBody(); this.logger.info(body); return JSON.parse(body);
在上述调用中,使用了portForEntity函数实现post请求
- 第一个参数表示请求的地址,是字符串类型变量
- 第二个参数表示请求体,是一个键值对变量,一般使用的是Java中的LinkedMultiValueMap类型,即:Java.type('org.springframework.http.HttpEntity')
- 第三个参数表示返回结果类型,这里我们使用了Java中的字符串类型,Java.type('java.lang.String').class
注:有关springboot中包含的restful api调用和具体的使用方法,可以去springboot官网查看,也可以直接在搜索引擎中搜索,网上有大量例程。
2 举例:调用图像识别服务自动获取车型信息
接下来,以百度人工智能服务为例,介绍如何实现DWF与公开的车辆识别人工智能服务集成
2.1 百度人工智能服务简介
车辆识别服务是百度开放的系列人工智能服务之一,只需要到百度人工智能官方网站上注册开发者信息即可获得500次/天的免费使用权限:
https://developer.baidu.com/newwiki/dev-wiki/zhu-ce-bai-du-kai-fa-zhe.html
点击下面的链接:https://ai.baidu.com/tech/vehicle/car
可以得到车型服务的介绍信息,如下图所示:
图-车型识别服务示例
调用车型服务只需要传递一个以字符串表示的图片内容信息,后台会以JSON格式返回车型描述,5个最有可能的车型,车辆在图片上的位置以及车辆的颜色信息,下面是返回结果的示例:
其中location_result表示识别结果所在的区域,color_result表示颜色,result表示最有可能的返回结果。
{ "log_id": "7283145783899943812", "location_result": { "width": 1088.4970703125, "top": 259.20935058594, "height": 524.55328369141, "left": 355.90838623047 }, "color_result": "绿色", "result": [ { "score": 0.50806790590286, "name": "丰田陆地巡洋舰(LandCruiser)", "year": "2016" }, { "score": 0.47251385450363, "name": "丰田兰德酷路泽", "year": "2016-2017" }, { "score": 0.0039114798419178, "name": "雷克萨斯LX", "year": "2017" }, { "score": 0.0011331264395267, "name": "丰田霸道", "year": "2018" }, { "score": 0.00070912146475166, "name": "丰田卡罗拉(Corolla)(科罗拉)", "year": "2017" } ] }
2.2 批量识别集成
理解了百度人工智能服务的作用之后,可以考虑在设备第一步先做一个批量识别功能,如下图所示,点击设备列表
图-批量识别效果示例
调用百度人工智能的方法是将图片转换为base64编码的字符串,然后向百度人工智能服务入口发出一个http post请求调用其restful api。
在DWF中:
- 图片如果是以localfile类型绑定在实体类或者关联类对象之上,可以直接利用this.omf.getString(oid, class, attribute)方法获得。
- 发出restful api请求既可以在前端完成也可以在后端完成,但是由于百度人工智能服务不允许前端跨域调用,因此只能在后端实现,所以可以使用this.restTemplate实现
- 百度的Api规范要求设置请求头为“application/x-www-form-urlencoded”,即:MediaType.APPLICATION_FORM_URLENCODED
- 调用之前需要获取一个令牌,方法见:https://ai.baidu.com/ai-doc/REFERENCE/Ck3dwjhhu
- 调用返回结果为一个字符串,获得之后可以转为JSON对象
因此,所有条件均已具备,后端操作的脚本如下:
var objs = this.selectedObjs; if (objs){ objs.forEach(car => { var r = carAi(car.oid, 'Asset', 'assetImg'); car.assetDesc = r.color_result + r.result[0].name; //识别出车型信息 this.omf.edit(car, 'Asset'); }) } function carAi(oid, targetClass, targetAttr){ var HttpHeaders = Java.type('org.springframework.http.HttpHeaders'); var HttpEntity = Java.type('org.springframework.http.HttpEntity'); var MediaType = Java.type('org.springframework.http.MediaType'); var StringHttpMessageConverter = Java.type('org.springframework.http.converter.StringHttpMessageConverter'); var LinkedMultiValueMap = Java.type('org.springframework.util.LinkedMultiValueMap'); var JString = Java.type('java.lang.String'); //获得车辆图片的base64格式 var imgStr = this.omf.getString(oid, targetClass, targetAttr); if (imgStr) { //必须设置请求头 headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); //设置请求体 var request_body= new LinkedMultiValueMap(); request_body.add("image", imgStr); request = new HttpEntity(request_body,headers); //注意access_token可能会过期 var baiduai = "https://aip.baidubce.com/rest/2.0/image-classify/v1/car?access_token=24.d1b78235254129c39f9c3d071e644459.2592000.1593750888.28335-19713079"; post_response = this.restTemplate.postForEntity(baiduai, request, Java.type('java.lang.String')); var body = post_response.getBody(); this.logger.info(body); //处理返回结果 return JSON.parse(body); } else { return null; } }
其中函数carAi输入一个Asset类对象,返回识别以后的结果,脚本一开始找到所有被选中的对象,逐个调用百度AI接口,然后更新到数据库中。
在DWF中,可以新建一个操作,将动作设置为Implement,然后选择后端脚本,输入上述脚本即可实现调用。
图-设置后端操作
2.3 全局函数设定
上述代码里出现的carAi,drawRestult每次都重复出现在脚本中,能不能不用每次都在脚本里重复出现呢?在DWF中提供了全局脚本设置可实现这一目标:
图-新增全局脚本
如上图所示,新增全局脚本之后,后端脚本可以直接调用函数实现代码的复用。
2.4 识别图像输出
除了识别结果之外,百度的车辆识别API还能输出被识别出来的区域所在四角坐标,那么这种效果也可以集成到DWF之中,如下图所示:
图-将识别后的图片附加到设备上
识别图像的输出
实现上述效果需要用到java内置的作图专用的Graphs类:Java.type('java.awt.Graphics'),其提供了在内存中绘制图片drawImage,并且在图片上绘制矩形框drawRect,文字等一系列函数。
如果需要将图片输出,可以使用Java.type('javax.imageio.ImageIO'),其ImageIO.write方法可以将内存中的图片输出到外部。
文件可以通过Java.type('java.io.File')类的createTempFile创建临时文件,f.getAbsolutePath()获得临时文件所在的位置,f.deleteOnExit()保证临时文件在退出的时候会自动删除。
最后,使用this.omf.setLocalFile可以将识别后的图片附加到对象上。
代码如下:
function drawResult(targetObj, targetClass, targetAttr, result){ // 根据识别的结果在临时文件里绘制 var Color = Java.type('java.awt.Color'); var Font = Java.type('java.awt.Font'); var Graphics = Java.type('java.awt.Graphics'); var BasicStroke = Java.type('java.awt.BasicStroke'); var BufferedImage = Java.type('java.awt.image.BufferedImage'); var File = Java.type('java.io.File'); var ImageIO = Java.type('javax.imageio.ImageIO'); //绘制图像 var path = this.omf.getFilePath(obj.oid, 'Asset', 'assetImg'); var resultPath = null; var img = null; var f = null; try { f = new File(path); img = ImageIO.read(f); } catch(e) { this.logger.error(e.toString()); } temp = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB); x = Math.round(r.location_result.left); y = Math.round(r.location_result.top); h = Math.round(r.location_result.width); w = Math.round(r.location_result.height); g = temp.getGraphics(); g.drawImage(img, 0, 0, null); g.setFont(new Font("Arial", Font.PLAIN, 80)); g.setColor(new Color(255, 0, 0, 255)); g.setStroke(new BasicStroke(3.0)); g.drawRect(x, y, h, w); g.dispose(); f = File.createTempFile(obj.oid, 'png'); try { ImageIO.write(temp, "png", f); resultPath = f.getAbsolutePath(); f.deleteOnExit() } catch (e) { this.logger.error(e.toString()); } return resultPath; }
在上述接口的加持下,后端识别图片的功能可以如此实现:
var obj = this.obj; if (obj) { var r = carAi(obj.oid, 'Asset', 'assetImg'); if (r){ var tmpPath = drawResult(obj, 'Asset', 'assetImg', r); if (tmpPath){ // 根据情况更新对象,例如: obj.assetDesc = r.color_result + r.result[0].name; //识别出车型信息 this.omf.edit(obj, 'Asset'); this.omf.setLocalFile(obj.oid, 'Asset', 'assetImgIdentified', tmpPath); this.res = obj.id; } } }
前端配合刷新
在表单中放入一个oprAi的操作,中文名可以设为“车辆识别-后端”,点击测试一下可以看到识别成功,但是图片没有刷新,这是因为附件插件没动态响应能力,因此需要在前端主动刷新。具体操作的方式是再增加一个前端操作,在操作中调用脚本激活后端操作,等后端操作执行完毕后强制刷新。
代码如下:
this.getOperation('Asset', 'oprAi').then(res => { var operation = res.data.data; // 获得操作对象 this.executeOperation(operation).then((opr) => { this.freshData() // 前端主动刷新 }); })
再次新建一个按钮,将此前端操作作为按钮点击的事件,点击后识别完毕可以实现主动刷新。
3 小结
本章着重介绍了如何通过后端脚本调用Java类库和第三方restful api服务,如何实现全局脚本调用,并且以百度人工智能服务的集成为例介绍了如何在车辆识别服务中嵌入图像识别功能。