☰
Jsonnet 到 Java:面向对象配置模板的结构保持翻译实践(java_comparison 详解)
2026/9/28 3:26:40 网站建设 项目流程
  • 编程语言
  • 模板引擎
  • CLI

【免费下载链接】jsonnet

Jsonnet - The data templating language

项目地址:https://gitcode.com/gh_mirrors/js/jsonnet
点击查看免费下载

导读

Jsonnet 是一种数据模板语言,其对象模型自带继承、字段覆写与延迟求值等面向对象语义,常被用来生成 Kubernetes 等系统的复杂配置。java_comparison目录以一份"微博同步管道(twitter-to-redis)"的 ReplicationController 配置为例,演示了如何把一份使用了面向对象特性的 Jsonnet 程序,以保持 JSON 输出结构完全一致的方式翻译成同样使用面向对象特性的 Java 程序。读完本文,你将掌握 Jsonnet 对象/字段/隐藏字段/继承在 Java 侧的等价映射规则、两套代码的编译运行方式,以及为什么这种翻译只能覆盖两个语言能力集合的交集部分。

一、示例背景与目录结构

java_comparison子目录位于仓库根目录 java_comparison/ 下,包含 8 个文件,构成一套完整的对照实验:

文件作用
base-template.libsonnetJsonnet 侧的基础模板(相当于 Java 侧BaseTemplate)
sub-template.jsonnetJsonnet 侧的子模板,通过+继承并覆写字段
sub-template.json参考输出 JSON,两份实现都应产出完全相同的这份结果
BaseTemplate.javaJava 侧的基础模板类
SubTemplate.javaJava 侧的子模板类(继承覆写)
JsonnetObject.java所有 Jsonnet 对象在 Java 侧的公共基类
JsonnetValue.javaJava 侧的价值物化(manifestation)访问器
Test.javaJava 入口,选择对象并输出到 stdout

其中 Java 侧的所有类都对应 Jsonnet 侧定义的对象:BaseTemplate↔ 基础模板、BaseTemplateSpec↔spec对象、BaseTemplateSpecSpec↔spec.spec、Container0↔containers[0]、Env0↔env[0],而SubTemplate则对应sub-template.jsonnet中通过对象拼接产生的覆写结果。

二、为什么可以用 Java 翻译 Jsonnet:能力交集的说明

原文档明确指出了这次翻译成立的前提——两个语言只在能力交集内可以互相翻译:

  • 不能从 Jsonnet 翻译到 Java 的部分:Jsonnet 具有 mixin(对象拼接/组合)和虚内部类(virtual inner classes)等特性,Java 没有直接对应的语言构造。例如仓库教程中展示的"把若干 mixin 对象用+叠加出一个新模板"的写法(参见 examples/mixins.jsonnet 与 doc/learning/tutorial.html 中的 mixins 小节),就无法用 Java 的继承体系逐字还原。
  • 不能从 Java 翻译到 Jsonnet 的部分:Java 具有可变状态(mutable state)和 I/O 能力,而 Jsonnet 是纯函数式、无副作用的求值语言,天然表达不了这类行为。
  • 执行顺序不同:Jsonnet 是惰性求值(lazy),字段只在被引用时才计算;Java 是急切求值(eager),构造对象时字段方法已经就绪。因此翻译时要求示例程序不能依赖求值顺序。

本次示例恰好完全落在两者的交集内:只用了继承、覆写、隐藏字段、参数化默认值这些 Jsonnet 与 Java 都能表达的特性,因此可以做到**结构保持(structure-preserving)**的翻译,即两边生成的 JSON 完全一致。

三、运行方式对照

3.1 运行 Jsonnet 版本

在java_comparison目录下执行:

jsonnet sub-template.jsonnet

输出即为 sub-template.json 中的内容。该命令要求本仓库的 Jsonnet 解释器已经构建完成(项目构建方式参见仓库根目录的 Makefile 与 CMakeLists.txt,也可直接make或按 README.md 指引构建)。

3.2 运行 Java 版本

javac *.java && java Test | jsonnet -

这里有两个关键点:

  • javac *.java会一次性编译目录内的全部 5 个 Java 文件(JsonnetObject.java、JsonnetValue.java、BaseTemplate.java、SubTemplate.java、Test.java),因为SubTemplate依赖BaseTemplate,Test又依赖它们与框架类,整目录编译可自动解决这些依赖。
  • | jsonnet -表示把 Java 程序的输出管道给jsonnet命令并以-作为输入文件名,由 Jsonnet 解释器对单行 JSON 做格式化(pretty-print),弥补 Java 侧"单行输出"的简化处理,使最终效果与 Jsonnet 直接输出一致。

四、翻译规则:从 Jsonnet 对象模型到 Java 类

原文档给出了一套非常清晰的逐条映射规则,可归纳为下面这张对照表:

Jsonnet 概念Java 等价实现
动态类型(一切值无静态类型)所有值一律声明为Object
隐式的 public 可见性所有方法、类一律public
数组[...]Object[]
原始值Boolean/Double/String(数字统一用Double)
对象(object)命名类的单例实例,且该类继承JsonnetObject
对象字段(field)无参方法(0 参数方法),字段在 Jsonnet 中是虚的(可被覆写),因此 Java 方法天然是 virtual 的
隐藏字段(::)不直接作为可物化字段;通过nonHiddenFields()返回的字段名集合区分,不在集合中的字段视为隐藏字段
对象继承/覆写(+、field+:)Java 类继承 + 方法覆写

4.1 字段到方法的转换

看 Jsonnet 基础模板 base-template.libsonnet:

{ // Mandatory param accessToken:: error "accessToken must be defined", // Optional params image:: "gcr.io/cooltool-1009/pipeline_image:latest", extraEnv:: [], apiVersion: "v1", kind: "ReplicationController", ... }

对应 Java 侧 BaseTemplate.java:

// Mandatory param public Object accessToken() { throw new RuntimeException("accessToken must be defined"); } // Optional params public Object image() { return "gcr.io/cooltool-1009/pipeline_image:latest"; } public Object[] extraEnv() { return new Object[]{}; } public Object apiVersion() { return "v1"; } public Object kind() { return "ReplicationController"; }

注意两点:

  1. 必选参数的强制语义:Jsonnet 用accessToken:: error "accessToken must be defined"声明这是一个必须由子类提供的参数——若未覆写就物化该字段会触发error。Java 侧对应地让方法直接throw new RuntimeException("accessToken must be defined"),把"缺失必填参数"从运行时错误语义上对齐。
  2. 隐藏字段的默认值:image、extraEnv使用::声明为隐藏字段,意思是"这是我的参数,不该直接出现在最终 JSON 里,只允许被内部引用"。在 Java 侧它们只是普通方法,靠nonHiddenFields()不收录其名字来保证不会出现在输出中。

4.2 隐藏字段的显式建模:nonHiddenFields()

Jsonnet 中字段有两种状态:可见字段(:)会出现在物化(manifest)后的 JSON 中,隐藏字段(::)不会。Java 没有内建对应概念,因此框架引入约定:每个对象类实现nonHiddenFields()返回"应该出现在输出中的字段名集合",集合之外的方法一律视为隐藏字段。

基类 JsonnetObject.java 给出空实现:

public class JsonnetObject { public Set<String> nonHiddenFields() { return new HashSet<String>(){}; } }

子类在覆写时先取父类集合再追加自己的可见字段,例如 BaseTemplate.java:

public Set<String> nonHiddenFields() { Set<String> r = super.nonHiddenFields(); r.addAll(Arrays.asList("apiVersion", "kind", "spec")); return r; }

于是accessToken、image、extraEnv这些"参数/隐藏字段"方法虽然可被调用,却不会出现在物化输出里。这个机制与 Jsonnet 解释器在物化阶段跳过隐藏字段的实现逻辑一一对应——在 VM 实现 core/vm.cpp 的objectFields中可以看到,物化(manifesting)遍历对象字段时,仅当字段不是ObjectField::HIDDEN才纳入输出。

4.3 继承与覆写:参数注入的两种写法

Jsonnet 侧子模板 sub-template.jsonnet 使用对象拼接+实现"继承并注入参数":

(import "base-template.libsonnet") + { accessToken: "xxxxx", image: "gcr.io/cooltool-1009/pipeline_image@sha256:....", spec+: { replicas: 2, } }
  • 顶层+把子模板的accessToken、image覆写进基础模板(相当于 Java 的方法覆写);
  • spec+: { replicas: 2 }是字段级叠加,只覆写spec对象里的replicas字段,spec下其余结构(spec.spec.containers等)保持不变。

Java 侧 SubTemplate.java 用继承+覆写实现同样的效果:

public class SubTemplate extends BaseTemplate { public String accessToken() { return "xxxxx"; } public String image() { return "gcr.io/cooltool-1009/pipeline_image@sha256:...."; } public class SubTemplateSpec extends BaseTemplate.BaseTemplateSpec { public Object replicas() { return 2.0; } } public Object spec() { return new SubTemplateSpec(); } }

SubTemplateSpec覆写BaseTemplateSpec的replicas()(对应spec+的叠加语义),而spec()方法整体换成返回SubTemplateSpec实例。

4.4 虚内部类:用 Java 内部类表达 Jsonnet 的嵌套对象与$引用

Jsonnet 的spec是一棵嵌套对象树,内部通过$引用外层对象的字段(见 base-template.libsonnet 中value: $.accessToken、image: $.image)。Java 侧用内部类来保留这种"内部引用外层"的能力:

class Env0 extends JsonnetObject { public Object name() { return "ACCESSTOKEN"; } public Object value() { return BaseTemplate.this.accessToken(); } }

BaseTemplate.this.accessToken()显式引用外层实例的字段,等价于 Jsonnet 的$.accessToken。同理Container0.image()返回BaseTemplate.this.image(),对应 Jsonnet 的image: $.image。

这也解释了原文档"Jsonnet 有虚内部类(virtual inner classes)"的提法:Java 内部类天然绑定到外层对象实例,与 Jsonnet 嵌套对象通过$/super引用外层的语义非常接近,这也是本示例能落在两语言交集内的关键原因。

4.5 数组拼接:Jsonnet 的+与 Java 的手工实现

Jsonnet 里env: [...] + $.extraEnv一行完成"基础环境变量数组 + 追加自定义数组"的拼接。Java 没有数组级+,因此 BaseTemplate.java 中的env()用手工方式完成:

public Object[] env() { Object[] first = new Object[]{ new Env0() }; Object[] second = BaseTemplate.this.extraEnv(); Object[] result = Arrays.copyOf(first, first.length + second.length); System.arraycopy(second, 0, result, first.length, second.length); return result; }

这段代码把 Jsonnet 语言内建的数组连接运算展开成了 Java 的标准库操作,是"语言特性到框架辅助函数"翻译的典型例子。

五、框架辅助函数:物化(Manifestation)的实现

Jsonnet 语言把"把值序列化成 JSON 字符串"称为物化(manifestation),是语言内建能力(VM 中对应 core/vm.cpp 的manifestJson递归实现)。Java 没有这种能力,因此示例提供了两个辅助类:

5.1 JsonnetValue:递归物化访问器

JsonnetValue.java 的manifest(Object value)是一个按值类型分派的递归访问器,恰好对应 Jsonnet 物化语义:

  • null→null;
  • String→ 带引号字符串(源码FIXME: does not escape the string注明:没有做转义处理,这是文档提到的两个"自由度"之一);
  • Double/Boolean→ 直接toString();
  • Object[]→[ e1, e2, ... ];
  • JsonnetObject→{ "field": value, ... },其中字段集合来自nonHiddenFields();
  • 其余类型 → 抛RuntimeException("Got weird type: ..."),与 Jsonnet"函数不能物化进 JSON"的运行时错误语义对应。

5.2 反射调用字段方法

物化对象时,JsonnetValue对每个非隐藏字段名执行:

Method method = obj.getClass().getMethod(field); Object fieldValue = method.invoke(obj);

即用反射按名字找到对应的无参方法并调用,从而把"Jsonnet 对象按字段名取值"翻译成"Java 按方法名反射调用"。这一设计让子类覆写的方法(如SubTemplate.replicas())在物化时自然被调用,完整还原了 Jsonnet 的虚字段语义。

5.3 Test:入口与选择器

Test.java 扮演 Jsonnet 命令行求值的角色:main中直接JsonnetValue.manifest(new SubTemplate())并输出到 stdout,相当于把sub-template.jsonnet的顶层表达式求值并物化。

六、输出结果验证与两个已知自由度

运行两侧命令后得到的 JSON 应与 sub-template.json 完全一致:

{ "apiVersion": "v1", "kind": "ReplicationController", "spec": { "replicas": 2, "spec": { "containers": [ { "env": [ { "name": "ACCESSTOKEN", "value": "xxxxx" } ], "image": "gcr.io/cooltool-1009/pipeline_image@sha256:....", "name": "twitter-to-redis" } ] } } }

可以看到accessToken、image、extraEnv这些隐藏字段没有出现在输出中,而replicas从基础模板的 1 被覆写成了 2。

原文档诚实声明了 Java 实现的两处"自由度"(与 Jsonnet 输出存在格式差异、但不影响 JSON 语义):

  1. 字符串未做转义:JsonnetValue对字符串直接"\"" + value + "\"",若值含引号或反斜杠会与标准 JSON 不一致;
  2. 单行输出:Java 直接输出一行紧凑 JSON,靠| jsonnet -管道做格式化来对齐美观效果。

七、适用边界与局限总结

以本示例为窗口,可以总结 Jsonnet 与 Java 互相翻译的完整边界:

  • 可以翻译:纯数据结构的嵌套对象、继承与覆写、隐藏字段(参数)、延迟语义下无副作用的部分(本示例在交集内)。
  • Jsonnet 有而 Java 无法直接表达:mixin(用+组合任意对象)、虚内部类的部分动态行为,以及依赖惰性求值顺序的程序。
  • Java 有而 Jsonnet 无法表达:可变状态、I/O、异常副作用等命令式行为。

因此这套翻译方法适合的场景是:在必须嵌入 JVM 生态(如公司内部 Java 平台)时,把结构已知、纯声明式的 Jsonnet 配置模板用等价的 Java 类体系重新实现,并借助jsonnet -保证输出格式与参考 JSON 对齐。若配置中大量使用 mixin 或惰性求值技巧,则应优先考虑保留 Jsonnet 解释器而非翻译。

八、延伸阅读

  • 仓库根目录 README.md:Jsonnet 项目概览、构建与安装方式;
  • doc/learning/tutorial.html:Jsonnet 语言教程,其中包含对象、继承与 mixin 的详细讲解;
  • doc/language/spec.html:语言规范(字段隐藏、物化语义的权威定义);
  • examples/mixins.jsonnet:mixin 组合的典型示例;
  • case_studies/kubernetes/kube.libsonnet:真实场景中把 Jsonnet 面向对象特性用于 Kubernetes 配置生成的案例,与本示例同源;
  • core/vm.cpp:Jsonnet 物化(manifestation)的底层实现,可与 JsonnetValue.java 的物化逻辑对照阅读。
  • 编程语言
  • 模板引擎
  • CLI

【免费下载链接】jsonnet

Jsonnet - The data templating language

项目地址:https://gitcode.com/gh_mirrors/js/jsonnet
点击查看免费下载
上一篇:3分钟上手!B站会员购抢票神器:免费自动化购票终极指南
下一篇:猫抓插件终极指南:三步搞定网页视频下载,新手也能轻松上手

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询