1. 背景
笔者开发的大数据大数据平台XSailboat 中的DataStudio模块包含基于DAG图的实时计算可视化开发功能,实现了Flink计算任务的可视化开发(想要了解更多,可以查看《Flink的DAG可视化开发实践》)
我们都知道,原生的Flink任务开发适合在IDE中使用代码去开发调试,想要去做可视化是很难的。为此引入了“模式框架+Aviator表达式”的可视化思路,为的就是让Flink任务适合界面开发,又不过多失去原生Flink的表达能力。
Aviator是一个非常优秀的表达式语言,适合应用在需要轻度编程的场景下。在我们的大数据平台中的计算管道开发、数据服务开发、还有其它一些需要前端轻度表达数据处理逻辑的地方,都有使用它。
- 为什么需要负向序号索引?
Aviator原生的功能是不带有负向序号索引的功能的,在没有负向索引的情况下,想要取数组的最后一个元素,就得这么写:
let arr = seq.array(int , 1, 2, 3 , 4) ;
## 打印最后一个元素
p(arr[len(a) - 1]) ;
负向序号索引的目标就是为了让它变得更简洁,对于配置界面来说,尽可能简洁是非常有意义的。所以我们对序号索引操作符([])进行了重载,使其支持负向序号索引,达到了下面的目标:
let arr = seq.array(int , 1, 2, 3 , 4) ;
## 打印最后一个元素
p(arr[-1]) ;
- 为什么需要使得Aviator表达式对我们自己的JSON库支持方括号([])场景下的赋值?
Aviator表达式,在新版本(5.3)下是支持以下特性的。
let arr = seq.array(int , 1, 2, 3 , 4) ;
let mp = seq.map()
## 直接赋值
arr[0] = -1 ;
mp['key'] = -1 ;
## 遍历元素
for e in arr{
p(e) ;
}
## 遍历键值
for k , v in mp{
p(k+'='+v) ;
}
JSON是一种非常常见的数据格式。在实践中发现,CSV+JSON是一种非常好的数据组织格式,它能解决个性化和字段对齐的矛盾。举一个例子:
我们需要开发某种业务领域的专家策略规则,规则数量很多,在应用过程中还会不断扩展。每种规则都有一些公共属性,但又都有自己个性化的一些配置项。这种数据该如何存储到数据库?
在对这种具有“模型”性质的数据,我们后台通常会使用Spring JPA去定义模型Bean,去做增、删、改、查操作。那么思路就是定义一个规则配置(ruleConf)字段,这个字段是一个类RuleConf,然后不同的规则继承RuleConf实现自己的规则配置类,如RuleConf_001、RuleConf_002等。这个ruleConf字段在持久化到数据库的时候,就变成JSON字符串存到这一列中。这样就实现了在一个整齐的表格中存储信息不整齐的数据。
表格就可以看成一种CSV,在我们的Kafka主题中,主题都是CSV格式,而其中的某一列可能是基本类型,也可能是JSON字符串。
而在整个平台中,对JSON字符串的解析和操作,我们有自己的JSON库。在微服务集群场景下,对JSON的操作会成为开发的日常,所以我们不断丰富和扩展JSON库的能力,使得JSON操作尽可能简洁、快速、高效。
在没有扩展之前,我们只能这么实现同样功能的代码:
let ja = j.array(1, 2, 3 , 4) ;
let jo = j.object()
## 直接赋值
j.array.put(ja , -1 , 0) ;
j.object.put(jo , 'key' , -1) ;
## 获取
p(j.array.opt(ja . 0)) ;
p(j.object.opt(jo , 'key')) ;
## 遍历元素
j.array.each(lambda(e)->p(e); end) ;
## 遍历键值
j.object.each(lambda(k , v)->p(k+'='+v) ;end)
在扩展之后,我们就能这样写这些代码:
let ja = j.array(1, 2, 3 , 4) ;
let jo = j.object()
## 直接赋值
ja[0] = -1 ;
jo['key'] = -1 ;
## 获取
p(ja[0]) ;
p(jo['key'])) ;
## 遍历元素
for e in ja{
p(e) ;
}
## 遍历键值
for k , v in jo{
p(k+'='+v) ;
}
2. 代码实现
- 重载索引操作符([])
package com.cimstech.sailboat.aviator;
import java.lang.reflect.Array;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import com.cimstech.xfront.common.json.JSONArray;
import com.cimstech.xfront.common.log.Assert;
import com.cimstech.xfront.common.math.Bool;
import com.cimstech.xfront.common.reflect.XClassUtil;
import com.googlecode.aviator.lexer.token.OperatorType;
import com.googlecode.aviator.runtime.type.AviatorObject;
import com.googlecode.aviator.runtime.type.AviatorRuntimeJavaElementType;
import com.googlecode.aviator.runtime.type.AviatorRuntimeJavaElementType.ContainerType;
import com.googlecode.aviator.runtime.type.AviatorType;
public class IndexOverload extends WedgeFunction
{
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
public String getName()
{
return OperatorType.INDEX.getToken();
}
@Override
public AviatorObject call(Map<String, Object> aEnv, AviatorObject aArg1, AviatorObject aArg2)
{
if (aArg1.getAviatorType() == AviatorType.JavaType)
{
Object obj = aArg1.getValue(aEnv);
if (obj != null)
{
if (obj instanceof JSONArray)
{
Integer index = XClassUtil.toInteger(aArg2.getValue(aEnv));
Assert.notNull(index, "序号不能为null。%s", aArg2.toString());
// 负向索引
if (index < 0)
{
index += ((JSONArray) obj).length();
}
return new AviatorJaRuntimeJavaType((JSONArray) obj, index);
}
// 对List和Array的[]重载,是为了支持负向索引。例如a[-1] 等价a[size-1]
else if (obj instanceof List)
{
Integer index = XClassUtil.toInteger(aArg2.getValue(aEnv));
Assert.notNull(index, "序号不能为null。%s", aArg2.toString());
// 负向索引
if (index < 0)
{
index += ((List) obj).size();
}
final Integer index0 = index;
// 用这种形式返回,就能支持a[0] = 'hello' 这种设置方法
return new AviatorRuntimeJavaElementType(ContainerType.List,
obj,
index0,
new Callable<Object>()
{
@Override
public Object call() throws Exception
{
return ((List<?>) obj).get(index0);
}
});
}
else if (obj.getClass().isArray())
{
Integer index = XClassUtil.toInteger(aArg2.getValue(aEnv));
Assert.notNull(index, "序号不能为null。%s", aArg2.toString());
// 负向索引
if (index < 0)
{
index += Array.getLength(obj);
}
final Integer index0 = index;
// 用这种形式返回,就能支持a[0] = 'hello' 这种设置方法
return new AviatorRuntimeJavaElementType(ContainerType.Array,
obj,
index0,
new Callable<Object>()
{
@Override
public Object call() throws Exception
{
return Array.get(obj, index0);
}
});
}
}
}
if (wedges != null)
{
Bool support = new Bool(false);
for (IWedge wedge : wedges)
{
AviatorObject result = wedge.call(aEnv, aArg1, aArg2, support);
if (support.get())
return result;
}
}
return aArg1.getElement(aEnv, aArg2);
}
}
上面的代码,之所以没有出现JSONObject是因为我们的JSONObject实现了java.util.Map接口,它自然就具备了Aviator关于Map的所有操作和语法。而之所以为出现JSONArray,是因为我们的JSONArray并没有实现java.util.List接口,这主要是因为JSONArray上已经有很多和List同质但不同名的方法,此处并没有绝对的必要去实现List接口。
对于JSONArray对象的ja[0] = -1这种设置语法,通过下面的类支持上:
- com.cimstech.sailboat.aviator.AviatorJaRuntimeJavaType
package com.cimstech.sailboat.aviator;
import java.util.Map;
import com.cimstech.xfront.common.json.JSONArray;
import com.googlecode.aviator.runtime.type.AviatorObject;
import com.googlecode.aviator.runtime.type.AviatorRuntimeJavaType;
public class AviatorJaRuntimeJavaType extends AviatorRuntimeJavaType
{
private static final long serialVersionUID = 1L;
private final int mIndex;
public AviatorJaRuntimeJavaType(final JSONArray aContainer, final int aIndex)
{
super(null);
object = aContainer;
mIndex = aIndex;
callable = this::get;
}
Object get()
{
return ((JSONArray) object).get(mIndex);
}
/**
* a[0] = -1 这种赋值语法会调用此方法
*/
@Override
public AviatorObject setValue(final AviatorObject aValue, final Map<String, Object> aEnv)
{
return AviatorRuntimeJavaType.valueOf(((JSONArray) object).put(mIndex, aValue.getValue(aEnv)));
}
}
而要应用上这些扩展,只需要在程序/模块初始化的时候,调用如下代码:
AviatorEvaluator.getInstance().addOpFunction(OperatorType.INDEX , new IndexOverload()) ;