Jolt概览
目前json数据已经逐步取代xml成为主流的数据交换和存储格式。在数据交互过程中经常遇到数据格式不一致的问题,在xml时代有XSTL(Extensible Stylesheet Language Transformation)可以实现,在json时代,我们可以用jolt。在当今大数据场景主流的ETL pipeline 工具如NIFI和 StreamSets都支持jolt作为json转换插件,在读取ElasticSearch, MongoDb, Cassandra等数据时可以用jolt做数据格式转换。
Jolt的核心功能:
- 提供了一组转换,可以“链接”在一起形成整体JSON到JSON的转换。
- 专注于转换JSON数据的结构,而不是操纵特定值
- 消费并生成新结构的JSON:实际是内存中的Map,List,String等。
jolt插件主要转(transfrom)换方法
(1)shift:主要作用是拷贝,将输入json的某个结构拷贝到目标json
(2)default:主要作用是设置默认值,如果输入json某个value不存在,则使用默认值补齐
(3)remove:主要作用是删除,可以删除输入json结构的某个结构
(4)sort:对key值进行排序
(5)cardinality:jsonobject和jsonarray之间的切换
(6)modify:修改json数值
每个转换都有自己的DSL(Domain Special Language 领域特定语言),以便定义于它特定的工作。目前,所有默认插件变换只会影响数据的“结构”。要进行数据操作,需要自行编写Java代码。
Jolt转换语法规则DSL的说明
Jolt使用json数据定义(specification)转换规则。由于json语法结构的限制,jolt需要组合不同的DSL以便解析程序能够正确的执行、避免歧义。所以jolt约定转换规则的输入必须是一个json array,通过定义多个转换spec定义形成一个转换链从而实现在不同语法规则场景下的转换。
Jolt spec示例:
[
{
"operation": "shift",
"spec": {}
},
{
"operation": "modify-default-beta",
"spec": {}
}
,
{
"operation": "sort"
}
]
shift:
在shift语境中,json的key值表示输入json的路径,value值表示目标路径。简单理解就是key是“匹配”,value是“去哪”,告诉程序匹配哪部分数据输出到什么位置 。多少数据转换场景是同样业务含义的字段名称不一样,位置不一样等,这种可以通过shift语义实现。
shift通配符
- “*” 通配符:仅可以匹配input的key,不能匹配value
使用 “*”通配符的时候,需要保证输入json具有相同的层级结构,它可以匹配某一个层级结构中的所有key,也可以匹配一个key的部分字符。 - “&”通配符
“&”通配符有两个参数&(0,1),第一个参数0表示从当前层级回溯多少级去寻找所需key,第二个参数1表示取第几个匹配到的参数(0表示取整体,1表示取第一个匹配结果,2表示取第二个匹配结果)。参数可以省略,&=&0=&(0)=&(0,0) - “$”通配符
KaTeX parse error: Expected 'EOF', got '&' at position 5: 通配符和&̲通配符具有相同的语法结构,都有…通配符通常是将输入Json的key值作为目标json的value使用, - “@”通配符
“@”通配符也有两个参数@(0,1),"@“和” " 用 法 类 似 , 不 过 " @ " 取 得 是 指 定 位 置 的 v a l u e 作 为 目 标 J s o n 的 v a l u e 值 , 而 “ "用法类似,不过"@"取得是指定位置的value作为目标Json的value值,而“ "用法类似,不过"@"取得是指定位置的value作为目标Json的value值,而“”取的是指定位置的key作为目标Json的value值 - “#”通配符
“#”通配符最有用的一点是,可以按照输入Json某个字段的取值,对输出Json设置不同的值 - “|”通配符
“|”表示“或”的逻辑,很容易理解,它只会对列出的字符进行匹配
shift数据转换场景
(1)拷贝JSONObject
{
"*": {
"@": "[&1]"
}
}
(2)字典映射
定义
"TYPE": {
"A": {
"#AA": "TYPE"
},
"B": {
"#BB": "TYPE"
}
}
输入
{
"TYPE": "A"
}
输出
{
"TYPE" : "AA"
}
如前文所说“jolt所有默认插件变换只会影响数据的结构,要进行数据操作,需要自行编写Java代码”。数据字典码表映射是典型的数据值的操作,枚举值较少的情况下,可以通过以上变通的方法实现。但如果有大量的码表,则需要另外自行实现代码。
(3)json key输出成value
{
"foo": {
"*": {
"$": "test.&(1,0).id"
}
}
}
输入
{
"foo": {
"barKey":"barValue",
"bazKey":"bazValue"
}
}
输出
{
"test": {
"barKey": {
"id": "barKey"
},
"bazKey": {
"id": "bazKey"
}
}
}
modify:
jolt内置了一些修改json数据的方法,通过modify语法实现,包含modify-overwrite-beta、modify-default-beta、modify-define-beta。
在modify语境中,json的key值表示输入json的路径,value值表示对该节点value的变换。在modify语境中语法定义的叶子节点所指向的input结构中必须也是叶子节点,这样才能够对这个叶子节点的值进行修改。
modify的“=”操作符
Modifier定义了大量的内置函数,可以实现常见的数据转换,比如大小写转换、字符串处理、求和、计数等、排序等。在value定义字符串中以“=”号开头标识后续是方法引用。modify语境中vaule值只能是常量或者输入json中的值(以@符号引用)。
支持以下语法:
- =abs 这是个语法糖,等价于=abs(@(1,&0))
- =abs(@(1,&0)) 调用“abs”函数,参数是当前节点的value,@(1,&0)的含义同shift中一样,基于当前key的位置获得相对位置的value
- =abs(@(1,&0),-1,-3) 调用“abs”函数,除输入json中的value外,还有其他的两个参数。
自定义数据处理
jolt默认不处理数据值的转换,在复杂的数据字典映射时上文的定义方式非常繁琐,我们需要自定义数据字典的定义语义。
通常情况下,数据字典映射是两个字符串集合的一一对应,json的key-value结构刚好符合这个结构,在jolt整体语义体系下,我们可以做这样的约定:
{
"operation": "com.github.weixingluo.json.transfrom.DataDictionaryTransform",
"spec": {
"rating": {
"primary": {
"value": "=dict(@(1,name),dictName)"
}
},
"dictionary": {
"dictName": {
"x": "yy",
"y": "zz"
}
}
}
}
因为字典映射类似于修改,所以我们直接接用modity的语义定义,只是针对字典映射表做特殊处理。也就是说json的key值表示输入json的路径,value值表示对该节点value的变换。
在operation定义了一个新的语义,我们在spec中定义了一个特殊的属性“dictionary”,我们在这个对象中定义字典映射表。除"dictionary"外,我们认为其他的key值都是普通的json转换定义。我们新定义个函数“dict”,约定“dict(@(1,name),dictName)”含义为对当前key的值做映射,依赖的数据映射字典为“dictName”。这样我们就定义了一个基于字典映射当前节点value到一个新的集合的方案。
为了让我们的定义执行起来,我们需要在jolt的体系中实现自己的代码。jolt在编译spec定义时,如果operation的值不在默认的值范围时,jolt会默认这是一个类路径,并尝试通过默认构造函数反射出来。在spec中我们定义了一个新的映射,jolt在初始化DataDictionaryTransform时将spec整个map作为构造函数传给了DataDictionaryTransform。
核心代码
public class DataDictionaryFunction implements Function {
private Map<String,Object> dictionary;
public DataDictionaryFunction(Map<String,Object> dictionary) {
this.dictionary = Collections.unmodifiableMap(dictionary);
}
@Override
public Optional<Object> apply(Object... args) {
//...
Map dict = (Map)dictionary.get(args[1]);
Object value = dict.get(args[0]);
return value==null?Optional.empty():Optional.of(value);
}
}
public class DataDictionaryTransform implements ContextualTransform, SpecDriven {
//...
public DataDictionaryTransform(Object spec) {
//...
Object dict = spec.get(dictString);
Map<String, Object> dictionary = (Map<String, Object>) dict;
DataDictionaryFunction dictMapping = new DataDictionaryFunction(dictionary);
Map<String,Function> functionsMap = new HashMap<>();
functionsMap.put("dict", dictMapping);
functionsMap = Collections.unmodifiableMap(functionsMap);
TemplatrSpecBuilder templatrSpecBuilder = new TemplatrSpecBuilder(OpMode.OVERWRITR, functionsMap);
rootSpec = new ModifierCompositeSpec(ROOT_KEY, (Map<String, Object>) spec, OpMode.OVERWRITR,
templatrSpecBuilder);
}
@Override
public Object transform( final Object input, final Map<String, Object> context ) {
Map<String, Object> contextWrapper = new HashMap<>( );
contextWrapper.put( ROOT_KEY, context );
MatchedElement rootLpe = new MatchedElement( ROOT_KEY );
WalkedPath walkedPath = new WalkedPath();
walkedPath.add( input, rootLpe );
rootSpec.apply( ROOT_KEY, Optional.of( input), walkedPath, null, contextWrapper );
return input;
}
}
参考
Jolt官方文档: https://github.com/bazaarvoice/jolt
jolt插件使用简介: https://blog.csdn.net/m0_37674755/article/details/85530142
JSON JOLT介绍 及语法详解-shift篇: https://blog.csdn.net/weixin_36048246/article/details/89287200
jolt操作演示: https://jolt-demo.appspot.com