Jsonnet 一种“新”的Json数据转换工具

图片

本文介绍了 Jsonnet 这一功能强大且灵活的 JSON 数据转换工具,作为解决复杂 JSON 生成与变换需求的选择。文章首先回顾了在实际项目中对 JSON 处理工具的需求背景,对比了 jsonpath、jsonpatch、jsonata 等工具后,最终选用图灵完备的配置语言 Jsonnet。Jsonnet 不仅是 JSON 的超集,支持生成 JSON、YAML 等多种格式,还具备良好的性能和扩展性,广泛应用于 Kubernetes、Grafana 等系统。

图片

背景

在最近的工作中场景中,需要一种对 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

公司

Google

Google

个人

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}

java 测试代码:

@Test    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().toString      Val.Str(pos, uuid)    }    override def staticSafe: Boolean = false  }
}

脚本

local uuid = tb.random.uuid;uuid()

结果

"01ba45ac-5ef6-4c12-b544-014d4f1a35b6"

  3. 性能优化

在目前的实现中,如果要进一步优化性能可以做以下几点:

  1. 参数值使用解析好的结构化(Expr)输入,即使用 ExternalVariable#expr 来提供参数值,这样避免了重复解析。

  2. 利用好 Parser Cache,从而避免jsonnet的重复解析和静态优化。

  3. 尽可能使用 Java 语言实现自定义的扩展函数。

  4. 针对经常用到的脚本,在应用启动的时候进行预热,从而避免首次执行时解析耗时。

  5. 其他在 sjsonnet 中的优化包括:使用 tableSwitch 、lazy 值裁剪、静态优化、避免重复计算、缓存 等手段。性能对比 jsonnet、sjsonnet 和 jrsonnet 如下:


小结

本文我们分析了一个 jsonnet 执行器的典型实现,一些典型场景的应用和常见的性能优化手段。目前我们已经在项目中善用了 jsonnet 来解决复杂的参数映射和转换问题,同时通过利用自定义函数来进一步的改善 jsonnet 的使用体验,于此同时也将部分的性能优化和问题修复提交给上游项目。在社区中也有 pkl 、cue 等新型项目可以支持类似的能力,但是目前在 JVM 平台,可能使用 jsonnet 更加简便。与此同时,也希望可以抛砖引玉,如果大家也有类似的场景,也欢迎相互交流,一起参与到 jsonnet 的建设中来。

团队介绍

本文作者虎鸣,来自淘天集团-终端平台团队。本团队支撑淘宝、天猫核心电商以及闲鱼、点淘等创新业务,服务数亿消费者,并作为核心技术团队,为每一年的双十一购物狂欢节提供基础技术保障。

  1. 在底层技术上,我们具备深厚的Android和iOS底层技术积累,拥有丰富的编译器、链接器、解释器技术应用实践。

  2. 在研发模式上,我们负责原生研发模式DX演进,服务数千开发者、承载数百亿日PV,深耕系统原生渲染技术,致力于建立下一代终端研发模式。

  3. 在网络技术上,我们在终端网络、传输和超大规模网关有深厚技术积累,负责开源方案XQUIC/Tengine,承载亿级长连和千万级QPS;在国际IETF标准、顶会SIGCOMM均有建树。

  4. 在终端技术上,我们打造领先行业的移动技术产品,涵盖多端架构、性能体验、组件框架、用户增长等关键领域,致力于移动端系统及厂商特性前沿探索。

  5. 在后端技术上,我们负责移动基础设施,有百万级QPS API网关、消息/推送、Serverless平台、自适应流控等柔性高可用解决方案。打造覆盖移动App全生命周期工程技术平台。

  6. 在跨端技术上,我们负责Weex2.0和核心Web容器,研究领域涉及W3C标准、WebKit内核、脚本引擎和自绘渲染引擎,面向Web标准提供一流跨端能力。通过卡片级小部件和小游戏技术,丰富创意供给,提供差异化的购物体验。

  7. 在前端技术上,我们在前端框架、工程、低代码领域长期深耕,支撑大促营销ProCode、LowCode、NoCode跨端页面研发;配套前沿的页面托管;负责ICE、微前端等开源方案,致力于提供简单友好的研发体系。

在这里,我们拥有最前沿的技术以及最优秀的人才;在这里,我们给你提供平台,师兄助你快速成长;在这里,校园化的办公环境,更有不定期团建high不停; 加入我们,让淘天成为你梦想起航的地方~

¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值