ThingsBoard支持用户自定义函数(UDF)用于规则引擎和数据转换器中的数据处理UDF的原始编程语言是JavaScript,因为它很流行并且受欢迎程度较高我们计划永远支持JavaScript。
尽管如此我们还是决定提供JavaScript的替代方案你可以在下面文档了解到我们的动机。
目的
ThingsBoard目前使用Java11编写有两种方法可以执行JS函数:
A) 使用Node.js编写的JS Executor微服务这是在集群/微服务模式下运行的默认方式。
B) 使用由Nashorn JS引擎提供支持的本地JS Executor该引擎在JVM内部运行在单体模式下部署时的默认方式。
Nashorn JS引擎已经弃用并从Java16中删除这就是为什么我们需要一个替代Nashorn的原因许多用户建议使用GraalVM的内置polyglot功能但由于多种原因并不适合我们,最重要的原因是安全性和控制UDF执行各个方面的能力此外大多数UDF都是相对简单的转换或过滤数据的函数我们希望找到一种更有效的方式来执行。
我们搜索现有的脚本/表达语言(EL)实现结果向我们推荐了MVEL,ThingsBoard表达语言(TBEL)算是是MVEL的分支具有一些重要的安全约束、内存管理以及ThingsBoard特有的常用使用帮助函数。
TBEL对比Nashorn
TBEL与Nashorn相比较更轻量速度快例如:执行1000个简单”return msg.temperature > 20”的脚本Nashorn需要16秒MVEL需要12毫秒速度提升1000多倍我们的TBEL分添加了安全改进代码以确保不会滥用CPU和内存因此无需在少箱环境中运行Nashorn。
当然TBEL没有JS强大但是在大部分用例中不需要如果需要JS的灵活性可以像以前一样使用远程的JS执行器。
TBEL对比JS Executors
JS Executors是一个基于Node.js的独立微服务功能强大支持最新的JS语言标准但是远程执行JS函数会具有明显的性能开销。
规则引擎和JS执行器通过队列进行通信此过程会消耗资源并引入相对较小的延迟(几毫秒),如果你将多个规则节点链接到一个规则链中可能会有较大延迟。
TBEL执行消耗的资源最少并且没有额外的进程间通信延迟。
TBEL
TBEL用Java语法编写的表达式然而与Java不同的是TBEL是动态类型的(具有可选的类型)这意味着源代码中没有类型限定。
TBEL表达式可以像单个标识符一样简单也可以像带有方法调用和内联集合的表达式一样复杂。
表达式
1
msg.temperature
在这个表达式中我们只有一个标识符(msg.temperature)在TBEL中称为属性表达式的,因为表达式的唯一目的是从变量中提取属性或上下文对象。
TBEL可以用布尔表达式假设在规则引擎中使用TBEL来定义简单的脚本过滤器节点:
1
return msg.temperature > 10;
与Java一样TBEL支持全局运算符优先级规则可以使用括号来控制执行顺序。
1
return (msg.temperature > 10 && msg.temperature < 20) || (msg.humidity > 10 && msg.humidity < 60);
多重语句
可以编写包含任意数量语句的脚本使用分号表示语句的终止,这在所有情况下都是必需的除非只有一条语句或脚本中的最后一条语句。
1
2
3
var a = 2;
var b = 2;
return a + b
请注意’a + b’后缺少分号新行不能替代MVEL中分号的使用。
强制转换
MVEL的类型强制系统适用于通过尝试将“右”值强制转换为“左”值的数据类型然后反之亦然进行两个类型不一致的比较情况。
例如:
1
"123" == 123;
此表达式在TBEL中为真因为类型强制系统会将未类型化的数字123强制为String后进行比较。
Map
TBEL创建Map并实现内存使用控制这就是为什么TBEL只允许内联创建映射。
最常见的Map操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Create new map
var map = { "temperature": 42, "nested" : {"rssi": 130}};
// Change value of the key
map.temperature = 0;
// Add new key
map.humidity = 73;
// Check existance of the key
if(map.temperature != null){
}
// Null-Safe expressions using ?
if(map.?nonExistingKey.smth > 10){
}
// Iterate through the map
foreach(element : map.entrySet()){
// Get the key
element.key
// Get the value
element.value;
}
// remove value from the map
map.remove("temperature");
// get map size
map.size();
List
TBEL创建List并实现内存使用控制这就是为什么TBEL只允许内联创建映射。
最常见的List操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Create new list
var list = ["A", "B", "C"];
// List elelement access
list[0];
// Add element
list.add("F");
// Add element using index
list.add(3, "D");
// Remove element by index
list.remove(4);
// Remove element by value
list.remove("D");
// Set element using index
list[2] = "C";
list.set(2, "C");
// Size of the list
list.size();
// Get sub list - JS style
list.slice(1, 3);
// Foreach
for (item: list) {
var smth = item;
}
// For loop
for (int i =0; i < list.size; i++) {
var smth = list[i];
}
Array
TBEL创建Array并实现内存使用控制只允许原始类型的数组,String数组会自动转换为List。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Create new array
int[] array = new int[3];
array[0] = 1;
array[1] = 2;
array[2] = 3;
str = "My String";
str[0]; // returns 'M';
function sum(list){
int result = 0;
for(var i = 0; i < list.length; i++){
result += list[i];
}
return result;
};
sum(array); // returns 6
array[3] = 4; // Will cause ArrayIndexOutOfBoundsException
文本
文字用于表示脚本中的固定文本值。
字符串
字符串可以用单引号或双引号表示。
1
2
"This is a string literal"
'This is also string literal'
转义符:
- \ - 转义单斜杠
- \n - 换行
- \r - 回车
- \u#### - Unicode字符(例如: \uAE00)
- ### - 八进制字符(例如: \73)
整数
整数可以用十进制、八进制或十六进制表示。
十进制整数可以表示为任何不以零开头的数字。
1
125 // decimal
八进制整数表示方法是在数字前加上一个零然后是0到7范围内的数字。
1
0353 // octal
十六进制整数表示为在前加上0x前缀然后是0-9..AF范围内的数字。
1
0xAFF0 // hex
浮点数由整数和点表示。
1
2
3
10.503 // a double
94.92d // a double
14.5f // a float
可以使用后缀B
和I
(必须大写)表示*
BigInteger*
和*
BigDecimal*
的文字。
1
2
104.39484B // BigDecimal
8.4I // BigInteger
布尔保留 true
和false
关键字。
空字符串保留null
和nil
关键字。
Java类
TBEL实现允许包中的一些Java 类例如:java.util
和java.lang
。
1
var foo = java.lang.Math.sqrt(4);
出于安全考虑这些类的使用受到限制可以调用静态和非静态方法但不能将类的实例分配给变量:
1
2
3
var list = ["A", "B", "C"];
java.util.Collections.reverse(list); // allowed
list = new java.util.ArrayList(); // Not allowed
为了简化JS迁移添加了带有静态JSON的方法类:JSON.stringify
和JSON.parse
其工作方式类似JS。
例如:出于同样的目的我们添加了 Date
可以在没有包名称的情况下使用的类。
控制流
If语句
BEL支持完整的C/Java风格的if-then-else语句块例如:
1
2
3
4
5
6
7
if (temperature > 0) {
return "Greater than zero!";
} else if (temperature == -1) {
return "Minus one!";
} else {
return "Something else!";
}
三元语句
与Java中一样的三元语句:
1
temperature > 0 ? "Yes" : "No";
支持嵌套的三元语句。
Foreach
TBEL中最强大的功能之一是foreach运算符在语法和功能上都类似于Java 1.5中的foreach运算符接收两个用冒号分隔的参数第一个是当前元素的局部变量第二个是要迭代的集合或数组。
例如:
1
2
3
4
sum = 0;
for (n : numbers) {
sum+=n;
}
由于TBEL将字符串视为可迭代对象可以使用foreach迭代字符串(逐个字符):
1
2
3
4
str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
for (c : str) {
//do something
}
For
1
2
3
4
var sum = 0;
for (int i =0; i < 100; i++) {
sum += i;
}
do While
do while
和do until
在TBEL中实现遵循与Java相同的until
与while
约定。
1
2
3
4
do {
x = something();
}
while (x != null);
… 语法相同 …
1
2
3
4
do {
x = something();
}
until (x == null);
While
TBEL实现了标准的while和ntil循环。
1
2
3
while (isTrue()) {
doSomething();
}
… or …
1
2
3
until (isFalse()) {
doSomething();
}
Helper函数
btoa
编码Base64的ASCII字符串(即字符串中的每个字符都被视为二进制数据的一个字节)
语法:
String btoa(String input)
参数:
- 输入:
string
- 二进制字符串
返回值:
Base64表示形式的ASCII字符串。
实例:
1
2
var encodedData = btoa("Hello, world"); // encode a string
var decodedData = atob(encodedData); // decode the string
atob
解码Base64字符串
语法:
String atob(String input)
参数:
- 输入:
string
- base64编码字符串
返回值:
解码的ASCII字符串。
实例:
1
2
var encodedData = btoa("Hello, world"); // encode a string
var decodedData = atob(encodedData); // decode the string
bytesToString
字节数组转字符串
语法:
*String bytesToString(List
参数:
- bytesList:
List of Bytes
- 字节数组 - charsetName:
String
- 字符编码默认是UTF_8
返回值:
根据指定的字节列表构造的字符串。
实例:
1
2
var bytes = [(byte)0x48,(byte)0x45,(byte)0x4C,(byte)0x4C,(byte)0x4F];
return bytesToString(bytes); // Returns "HELLO"
decodeToString
bytesToString的别名。
decodeToJson
字节列表解码为JSON。
语法:
*String decodeToJson(List
参数:
- bytesList:
List of Bytes
- 字节数组
返回值:
JSON对象或原始内容。
实例:
1
2
3
4
var base64Str = "eyJoZWxsbyI6ICJ3b3JsZCJ9"; // Base 64 representation of the '{"hello": "world"}'
var bytesStr = atob(base64Str);
var bytes = stringToBytes(bytesStr);
return decodeToJson(bytes); // Returns '{"hello": "world"}'
stringToBytes
字符串转换为字节数据。
语法:
*List
参数:
- 输入:
Binary string
- 字符串中的每个字符都是一个字节的二进制数据 - charsetName:
String
- 字符编码默认是UTF_8
返回值:
字节数组。
实例:
1
2
3
var base64Str = "eyJoZWxsbyI6ICJ3b3JsZCJ9"; // Base 64 representation of the '{"hello": "world"}'
var bytesStr = atob(base64Str);
return stringToBytes(bytesStr); // Returns [123, 34, 104, 101, 108, 108, 111, 34, 58, 32, 34, 119, 111, 114, 108, 100, 34, 125]
parseInt
字符串转换为整数。
语法:
Integer parseInt(String str[, String radix])
参数:
- str:
string
- 整数表示的字符串 - radix:
String
- 解析字符串时使用的可选基数
返回值:
一个整数值。
实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
return parseInt("0") // returns 0
return parseInt("473") // returns 473
return parseInt("+42") // returns 42
return parseInt("-0", 10) // returns 0
return parseInt("-0xFF") // returns -255
return parseInt("-FF", 16) // returns -255
return parseInt("1100110", 2) // returns 102
return parseInt("2147483647", 10) // returns 2147483647
return parseInt("-2147483648", 10) // returns -2147483648
return parseInt("2147483648", 10) throws a NumberFormatException
return parseInt("99", 8) throws a NumberFormatException
return parseInt("Kona", 10) throws a NumberFormatException
return parseInt("Kona", 27) // returns 411787
parseFloat
字符串转换为浮点数。
语法:
Integer parseFloat(String str)
参数:
- str:
string
- 解析的字符串
返回值:
一个浮点值。
实例:
1
return parseFloat("4.2"); // returns 4.2
parseDouble
字符串转换为双精度。
语法:
Integer parseDouble(String str)
参数:
- str:
string
- 解析的字符串
返回值:
一个双精度值。
实例:
1
return parseDouble("4.2"); // returns 4.2
parseHexToInt
十六进制字符串转换为整数。
语法:
int parseHexToInt(String hex[, boolean bigEndian])
参数:
- hex:
string
- 具有big-endian字节顺序的十六进制字符串 - bigEndian:
boolean
- 如果为true则为big-endian(BE)字节顺序否则为little-endian(LE)
返回值:
一个整数值。
实例:
1
2
3
4
return parseHexToInt("BBAA"); // returns 48042
return parseHexToInt("BBAA", true); // returns 48042
return parseHexToInt("AABB", false); // returns 48042
return parseHexToInt("BBAA", false); // returns 43707
parseLittleEndianHexToInt
语法:
int parseLittleEndianHexToInt(String hex)
parseBigEndianHexToInt
语法:
int parseBigEndianHexToInt(String hex)
toFixed
将double值四舍五入为相邻值。
语法:
double toFixed(double value, int precision)
参数:
- value:
double
- 双精度值 - precision:
int
- 精度
返回值:
一个double值。
实例:
1
2
return toFixed(0.345, 1); // returns 0.3
return toFixed(0.345, 2); // returns 0.35
hexToBytes
十六进制字符串转换为整数列表。
语法:
*List
参数:
- hex:
string
- 具有big-endian字节顺序的十六进制字符串
返回值:
一个整数列表。
实例:
1
return hexToBytes("BBAA"); // returns [187, 170]
bytesToHex
将字节列表(其中每个整数表示单个字节)转换为十六进制字符串。
语法:
*String bytesToHex(List
参数:
- bytes:
List of integer
- 整数值列表其中每个整数代表单个字节
返回值:
十六进制字符串。
实例:
1
return bytesToHex([187, 170]); // returns "BBAA"
bytesToBase64
将字节数组转换为Base64字符串。
语法:
String bytesToBase64(byte[] bytes)
参数:
- bytes:
List of integer
- 整数值列表其中每个整数代表单个字节
返回值:
Base64字符串。
实例:
1
return bytesToBase64([42, 73]); // returns "Kkk="
base64ToHex
Base64字符串解码为十六进制字符串。
语法:
String base64ToHex(String input)
参数:
- 输入:
String
- Base64字符串
返回值:
十六进制字符串。
实例:
1
return base64ToHex("Kkk="); // returns "2A49"
base64ToBytes
将Base64字符串解码为字节数组。
语法:
byte[] base64ToBytes(String input)
参数:
- 输入:
String
- Base64字符串
返回值:
字节数组。
实例:
1
return base64ToBytes("Kkk="); // returns [42, 73]
parseBytesToInt
在给定偏移量、长度和可选的字节顺序的情况下从字节数组中解析int。
语法:
*int parseBytesToInt([byte[] or List
参数:
- data:
byte[]
orList of Byte
- 字节数组 - offset:
int
- 偏移量 - length:
int
- 长度 - bigEndian:
boolean
- 如果为假则为LittleEndian否则为BigEndian
返回值:
整数值。
实例:
1
2
return parseBytesToInt(new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD}, 0, 3, true); // returns 11189196 in Decimal or 0xAABBCC
return parseBytesToInt(new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD}, 0, 3, false); // returns 13417386 in Decimal or 0xCCBBAA