在最近的工作中场景中,需要一种对 JSON 进行灵活 “生成” 、“转换” 的工具,需要同时兼具性能和灵活性,可以对任意复杂的 JSON 进行处理。在手淘【多语言项目】中的我们基于自研 JSONPath 结合自定义规则的方式对响应进行字段增强。该场景虽然很好的满足了项目的需求,但是对于更加灵活的场景却显得有点捉襟见肘。为此在经过一番探索和对比后,从 jsonpath、jsonpatch、jsonata、 和 jsonnet 、pkl 、graphql 等工具中,最终选择了 jsonnet,本文将对 jsonnet 的进行简单介绍和分析,希望可以抛砖引玉和大家进行交流。
jsonnet 是一个谷歌开源的规范和实现,是一种图灵完备的配置编程语言(configuration language), 主要用于:生成数据(json、yaml、ini等),同时,jsonnet 也是 json 的超集,一个合法的 json 就是一个合法的 jsonnet 程序,并且带有完整的 IDE 和 社区支持,广泛应用于 k8s、grafana 等。目前拥有 cpp、rust、go 、java 等多个平台的实现。
{person1: {name: "Alice",welcome: "Hello " + self.name + "!",},person2: self.person1 { name: "Bob" },}
将会生成数据:
{"person1": {"name": "Alice","welcome": "Hello Alice!"},"person2": {"name": "Bob","welcome": "Hello Bob!"}}
在本文中,我将主要分析 sjsonnet, 一种 Jvm 平台上的 jsonnet 实现,实际上其他的语言的实现也具备类似的结构,下面的表格做简单的归纳:
jsonnet |
go-jsonnet |
jrsonnet |
sjsonnet |
|
性能 |
低 |
高 |
高 |
高 |
解释器模式 |
是 |
是 |
是 |
是 |
支持native产物 |
是 |
是 |
是 |
是 |
实现语言 |
CPP |
Go |
Rust |
Scala |
支持作为库使用 |
是 |
是 |
是 |
是 |
标准库实现 |
jsonnet |
Go |
Rust |
Scala |
公司 |
个人 |
DataBricks |
||
最新支持版本 |
0.21 |
0.21 |
0.20 |
0.21 |
▐ sjsonnet 的实现结构
sjsonnet 的实现主要包含 Parser、StaticOptimizer、Evaluator、Materializer 几个部分。

-
Parser : 主要将 jsonnet 的文本内容,转换为 抽象语法树 AST, 后续的所有操作都是在 AST 上进行的。
-
StaticOptimizer:静态优化器主要是进行常规的静态优化、比如 :分支裁剪、常量折叠、变量查找替换等,通过静态优化器优化之后,我们得到了一个优化的 AST。这个优化后的 AST 将会在 sjsonnet 内部进行缓存,后续针对同一个输入的 jsonnet 脚本,将直接返回这个优化后的 AST,从而避免了重复解析和优化的过程,这也是 sjsonnet 性能非常好的原因之一。在我们的实现中,我们基于 caffeine cache 实现了 parser cache。
-
Evaluator: 求值器的作用主要是结合输入的参数(jsonnet 中的 extVar 和 tlaVar) 以及 优化的 AST 进行求值,在过程中将会遍历 AST 并执行相关的逻辑,并最终生成一个 Val (即结果值)。这也就是我们一段 jsonnet 脚本的执行结果了。
-
Materializer:物化器,其核心的作用是 将 求值器产生的结果,转换为我们想要的格式,比如 json 字符串、JSONNode、yaml、ini 或则是 markdown格式的文本 或则 Spring 的 property 文件等。在 sjsonnet 中定义了 AstTransformer 类型,我们可以实现来转换为我们想要的结构。
通过上面的介绍,想必大家对 sjsonnet 的代码库都有了一个了解,那么如何在 Java 中使用呢。这里我们封装了 jsonnet 的 Java 实现。其中包含了不少的扩展和支持, 可以非常方便的在 Java 中使用,目前需要 Java 21。下面我们将结合这个库,介绍相关的用法。
▐ 1. 基本用法
1.1 字段提取
场景:如果原始的 json 是一个非常大的 json 对象,而我们只需要其中的一部分,则可以只定义我们感兴趣的字段,对原始的数据进行裁剪。
{"store": {"book": [{"category": "reference","author": "Nigel Rees","title": "Sayings of the Century","price": 8.95},{"category": "fiction","author": "Evelyn Waugh","title": "Sword of Honour","price": 12.99},{"category": "fiction","author": "Herman Melville","title": "Moby Dick","isbn": "0-553-21311-3","price": 8.99},{"category": "fiction","author": "J. R. R. Tolkien","title": "The Lord of the Rings","isbn": "0-395-19395-8","price": 22.99}],"bicycle": {"color": "red","price": 19.95}}}
#导入参数local input = std.extVar("input");#选择部分结果{myColor: input.store.bicycle.color,myPrice: input.store.bicycle.price,}
{"myColor": "red","myPrice": 19.95}
fun testOnlyBicycle() {val jsonnet = readFile("jsonnet/case_only_bicycle.jsonnet")val store = readFile("jsonnet/store.json")val extVars = mapOf<String, Any>("input" to ExternalVariable.code(store))val result = Jsonnet.interpretAsString(jsonnet, extVars, emptyMap<String, Any>())println(result.value())}
1.2 字段加减
场景:假设我们有一个较大的json,但是我们对其中的部分字段需要进行丢弃。
输入
{"store": {"book": [{"category": "reference","author": "Nigel Rees","title": "Sayings of the Century","price": 8.95},{"category": "fiction","author": "Evelyn Waugh","title": "Sword of Honour","price": 12.99},{"category": "fiction","author": "Herman Melville","title": "Moby Dick","isbn": "0-553-21311-3","price": 8.99},{"category": "fiction","author": "J. R. R. Tolkien","title": "The Lord of the Rings","isbn": "0-395-19395-8","price": 22.99}],"bicycle": {"color": "red","price": 19.95}}}
local input = std.extVar("input");std.objectRemoveKey(input.store, "book")
{"bicycle": {"color": "red","price": 19.95}}
1.3 字段替换
场景:有个一个对象,但是我们需要将其中的某个 key 的值更新为新的 key,值保持不变
{"name": "Alice","welcome": "Hello Alice!"}
local input = std.extVar("input");tb.obj.objectReplaceKey(input,"name", "newName")
输出
{"newName": "Alice","welcome": "Hello Alice!"}
1.4 字段计算
场景:比如我淘宝直播场景中,我们需要结合 tag 和 topic 生成新的 tagTopic。则可以利用jsonnet的字符串计算能力。
输入
{"topic": "957c3914-a83d-498d-b039-efad8de7f3c2","tag": "tb"}
local input = std.extVar("input");input + {tagTopic: self.topic + "-" + self.tag,}
{"tag": "tb","tagTopic": "957c3914-a83d-498d-b039-efad8de7f3c2-tb","topic": "957c3914-a83d-498d-b039-efad8de7f3c2"}
1.5 和 jsonpath 结合
场景:如果我们已经非常熟悉了jsonpath的写法,也借助jsonpath 来进行字段的筛选。
输入
{"store": {"book": [{"category": "reference","author": "Nigel Rees","title": "Sayings of the Century","price": 8.95},{"category": "fiction","author": "Evelyn Waugh","title": "Sword of Honour","price": 12.99},{"category": "fiction","author": "Herman Melville","title": "Moby Dick","isbn": "0-553-21311-3","price": 8.99},{"category": "fiction","author": "J. R. R. Tolkien","title": "The Lord of the Rings","isbn": "0-395-19395-8","price": 22.99}],"bicycle": {"color": "red","price": 19.95}}}
local input = std.extVar("input");tb.jsonpath.select(input, "$.store.book[?(@.price < 10)].title")
输出
["Sayings of the Century","Moby Dick"]
▐ 2. 高级用法
2.1 json数据变为 markdown
场景:需要将输出的内容转换为 markdown,而非 json
输入
{"name": "Alice","age": 2}
local input = std.extVar("input");{newName: "this is my new name " + input.name,newAge: input.age + 100}
输出
| newAge | newName || ------ | ------- || 102 | this is my new name Alice |
val inputData = """{"name":"Alice", "age":2}"""val jsonnet = readFile("jsonnet/test_ext_vars.jsonnet")val extVars = mutableMapOf("input" to Jsonnet.code(inputData)) as Map<String, Any>val markdown = Jsonnet.interpretAsMarkdown(jsonnet, extVars, emptyMap<String, Any>())println(markdown.value)
2.2. 自定义函数
场景:当标准库的函数已经不支持我们想要的功能时,我们可以考虑使用 jsonnet 本身来实现一个函数,也可以考虑使用 Java 或者 Scala 语言来实现一个函数。
比如要实现一个 uuid 函数:
private[functions] object RandomFunctions extends AbstractFunctionModule {override final val name: String = "random"/*** Module 的描述* */override val functions: Seq[(String, Val.Func)] = Seq(builtin(UUIDGen))private object UUIDGen extends Val.Builtin0("uuid") {override def evalRhs(ev: EvalScope, outerPos: Position): Val = {val uuid = java.util.UUID.randomUUID().toStringVal.Str(pos, uuid)}override def staticSafe: Boolean = false}}
脚本
local uuid = tb.random.uuid;uuid()
结果
"01ba45ac-5ef6-4c12-b544-014d4f1a35b6"
▐ 3. 性能优化
在目前的实现中,如果要进一步优化性能可以做以下几点:
-
参数值使用解析好的结构化( Expr)输入,即使用ExternalVariable#expr来提供参数值,这样避免了重复解析。 -
利用好 Parser Cache,从而避免jsonnet的重复解析和静态优化。 -
尽可能使用 Java 语言实现自定义的扩展函数。 -
针对经常用到的脚本,在应用启动的时候进行预热,从而避免首次执行时解析耗时。 -
其他在 sjsonnet 中的优化包括:使用 tableSwitch 、lazy 值裁剪、静态优化、避免重复计算、缓存 等手段。性能对比 jsonnet、sjsonnet 和 jrsonnet 如下:
本文我们分析了一个 jsonnet 执行器的典型实现,一些典型场景的应用和常见的性能优化手段。目前我们已经在项目中善用了 jsonnet 来解决复杂的参数映射和转换问题,同时通过利用自定义函数来进一步的改善 jsonnet 的使用体验,于此同时也将部分的性能优化和问题修复提交给上游项目。在社区中也有 pkl 、cue 等新型项目可以支持类似的能力,但是目前在 JVM 平台,可能使用 jsonnet 更加简便。与此同时,也希望可以抛砖引玉,如果大家也有类似的场景,也欢迎相互交流,一起参与到 jsonnet 的建设中来。
-
在底层技术上,我们具备深厚的Android和iOS底层技术积累,拥有丰富的编译器、链接器、解释器技术应用实践。 -
在研发模式上,我们负责原生研发模式DX演进,服务数千开发者、承载数百亿日PV,深耕系统原生渲染技术,致力于建立下一代终端研发模式。 -
在网络技术上,我们在终端网络、传输和超大规模网关有深厚技术积累,负责开源方案XQUIC/Tengine,承载亿级长连和千万级QPS;在国际IETF标准、顶会SIGCOMM均有建树。 -
在终端技术上,我们打造领先行业的移动技术产品,涵盖多端架构、性能体验、组件框架、用户增长等关键领域,致力于移动端系统及厂商特性前沿探索。 -
在后端技术上,我们负责移动基础设施,有百万级QPS API网关、消息/推送、Serverless平台、自适应流控等柔性高可用解决方案。打造覆盖移动App全生命周期工程技术平台。 -
在跨端技术上,我们负责Weex2.0和核心Web容器,研究领域涉及W3C标准、WebKit内核、脚本引擎和自绘渲染引擎,面向Web标准提供一流跨端能力。通过卡片级小部件和小游戏技术,丰富创意供给,提供差异化的购物体验。 在前端技术上,我们在前端框架、工程、低代码领域长期深耕,支撑大促营销ProCode、LowCode、NoCode跨端页面研发;配套前沿的页面托管;负责ICE、微前端等开源方案,致力于提供简单友好的研发体系。

