大约一个星期多前发布了 Jackson 2.18 的第一个候选版本——2.18.0-rc1。在等待官方的 2.18.0 版本发布(预计在一周或两周后,如果一切顺利)的同时,让我们来看看将包含的内容。(和往常一样,可以在 Jackson 2.18 发布 页面上查看详细信息)。
2.18 统计信息:从 2.17.0 版本发布以来,开发大约历时了 5 个月,共有约 60 项改动(新功能和修复问题)横跨所有官方 Jackson 组件。
这可以再次被归类为一个“非常小的版本”——尽管从一个特定角度来看,它具有重要的里程碑意义:它完成了我视为进入 3.0.0 发行候选阶段前必不可少的一个关键功能。
在2.18要特别关注的一件事是实现databind#4515——“在Jackson 2.x中重写Bean属性内省逻辑”。这个计划已经持续了八年(2016年发布的Jackson 2.8发行说明中提到,尝试将其包含在2.9版本中)。
而现在我终于强迫自己在完成重写之前不开发任何其他特性,专心于这项任务。因此,在2024年5月,我花了大约两周的时间来弄清楚如何完成这项任务……然后,完成了它。呼,总算搞定了!
事后看来,我本应该早点做这件事:虽然这可能是这几年来我遇到的最具挑战性的任务,但五年之前做这件事并不比现在容易。不过:总比不做要好。
所以为什么要进行这件事呢?这次重写的主要动机是,有大约半打难以甚至无法修复的 Bug,这些 Bug 与属性内省逻辑中的几个问题相关。具体来说,就是“常规”属性(通过 getter、setter 或字段访问)和“创建者”属性(通过构造函数或工厂方法传递)的处理方式不统一。如果不统一这些处理方式,一个访问器中存在的注解将不会被其他访问器使用。这导致了在处理 Java 记录以及 Kotlin(和 Lombok)数据类时出现了一些难以解决的问题。
有时缺乏统一性也会阻碍“无注解”创建者对POJO的完全功能——现在应该可以顺利运行了:因此,如果你已经开启了“参数名称”模块,并且POJO只有一个 public
构造函数,那么就无需任何注解,所以以下内容现在应该可以正常运行:
public class 我的类型 {
private int a, b; // a, b可以替换成有意义的中文变量名
public 我的类型(int a, int b) {
this.a = a;
this.b = b;
}
// 构造函数,初始化a和b的值
public int getA() { return a; }
// 获取a的值
public int getB() { return b; }
// 获取b的值
}
这次重写是我待办事项清单上必须要完成的一件特定事项,以便在2024年的剩余时间里为杰克逊3.0的预发布版本做准备——我想要2.x和3.x的代码库都采用“New Introspection”功能。
这对我来说很重要;希望这也将显著提升创作者(无论是显式的注释还是隐含的)的用户体验,这将消除一类以前无法解决的问题,即“为什么这里的注释不起作用呢?”。
最想要的功能:允许通过创建者使用@JsonAnySetterJackson-databind的9個“最想具備”的功能之一,即databind#562,在2.18版本中實現了。
這正是你所期待的:現在這個問題解決了。
public class MyAnyType {
private final int id;
private final String name;
@JsonCreator
public MyAnyType(@JsonProperty("id") int id,
// 可以传入一个 'JsonNode',而不是 'Map'
@JsonAnySetter JsonNode extra) {
this.id = id;
name = extra.path("name").asText(); // 将 'name' 字段的值从 extra 中提取出来
// 等等
}
}
除了能够对 Map<String, ValueType>
和 JsonNode
类型的值进行标注之外,它还能和 Java 的 Record 类型(以及 Kotlin 的数据类)一起使用(正如它应该的那样)。我们稍后会再详细讨论这一点。
更快的浮点读取
我已经写过另一个重要的改进:“Jackson 2.18 浮点读取更快!”简单来说,从 2.17 升级到 2.18,读取 double
和 BigDecimal
值的速度提升了 10–20%。性能提升的原因是消除了在常见解码路径上的 String
分配。无需更改任何代码,就能免费获得性能提升——不过,通常建议启用“快速浮点读取”功能,例如在 ‘Jackson 2.16 更快的 BigDecimal 读取’ 中有介绍:
JsonFactory f = JsonFactory.builder()
.enable(StreamReadFeature.USE_FAST_DOUBLE_PARSER) // 启用快速双精度解析器
.enable(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER) // 启用快速大数解析器
.build(); // 构建JsonFactory实例
ObjectMapper mapper = new JsonMapper(f); // 创建一个新的JsonMapper实例,并传入之前构建的JsonFactory
CSV:支持如向量值之类的修饰
这实际上是一个相当罕见的功能:我自己也需要的那种东西!我一直都在寻找包含大量向量的数据集——用于测试处理 JSON 中浮点值的性能(见上文),但似乎我没有收藏它(需要再找一次)。数据以 CSV 格式存在,而不是像这样的形式。
id,数值
123,0.5,0.25,-0.1
Jackson的CSV模块可以处理数组值,数组值会用括号包围,如下所示:
id, 向量值
A123,[0.5,0.25,-0.1]
感谢实现了dataformats-text#442,可以读取这些所谓的“装饰值”,以及实现了dataformats-text#495,可以写入这些值,这些(以及许多类似的情况)现在可以被妥善处理。读取可以像这样进行:
@JsonPropertyOrder({"id", "vector" })
static class Embedding {
public String id;
public double[] vector;
}
// 可以手动构建 Schema,也可以像这里这样从类中自动检测:
CsvSchema schema = CsvMapper.schemaFor(Embedding.class)
.withHeader()
.withArrayElementSeparator(",")
// 可以使用一个默认的装饰器,还有简单的 '前缀/后缀' 装饰器,用于不同的开始/结束标记;严格的/可选的变体
.withColumn("vector",
col -> col.withValueDecorator(CsvValueDecorators.STRICT_BRACKETS_DECORATOR)
);
MappingIterator<Embedding> it = MAPPER.readerFor(Embedding.class)
.with(schema)
.readValues(new File("data.csv"));
// 可以读取所有数据,也可以像这里这样逐个处理
while (it.hasNextValue()) {
doSomething(it.nextValue());
}
// ... 就这样完成了!
写的时候也会用类似的 CsvSchema
来添加装饰。
请注意,此功能不仅限于数组值:它也可用于添加或移除(写入或读取)这些值的装饰。因此,它可能对处理 CSV 文件中的各种类型的值非常有用。
最后……我需要重新开始测试这种所谓的“稠密”向量数据的端到端性能提升。希望不久能在博客上分享我的进展。
对 Java Record 类型(及其类似)和 jackson-jr 的全面支持上述提到的功能中,有两项改进了对Java Records(以及类似类型,如Kotlin数据类、Lombok生成的类)的处理。属性内省功能修复了注解使用中的问题;并且现在可以通过创建器使用@JsonAnySetter
(由于Records是不可变的,无法使用字段或setter),这使得构建方式更加灵活。
除了改进 Jackson-databind 之外,小杰克森还通过 jackson-jr#162 添加了对 Java 记录的基本支持,这样记录值就可以像普通的 POJO 一样读取和写入。
Apache Avro 1.11 的 CVEs长期以来,杰克逊 Avro 模块的一个版本问题是无法使用最新版本的 Apache Avro 库作为依赖项(该模块可以使用或不使用此库解码 Avro 内容,但在编码时会用到它):依赖项限制在 1.8.x 版本。
这主要是因为较旧版本的 Apache Avro 发布了 CVE。在dataformats-binary#167 之后,这些问题得到了解决,现在可以放心使用 1.11.x 版本。
StreamReadConstraints
选项 (maxTokenCount
)
最近几次发布的版本中,我们为 jackson-core
(即 JsonParser
和 JsonGenerator
)添加了一些“处理限制”设置:这些可配置的限制为防止潜在的 DoS 攻击提供保护。
Jackson 2.18 还新增了一个读取端的限制:maxTokenCount
。
它是在 jackson-core#1310 中添加的(详情请参见该链接)。
设置这样一个限制可以按照JSON Tokens的数量(而不是原始的byte
长度,例如)来限制最大文档读取大小,还可以防止传递类似假的兆字节JSON。
[[[[[[[[[],[],[],[]]]]]]]]
其中一个小文档可能会生成大量的标记(tokens),而且(如果使用databind)会占用比输入文档本身更多的内存。默认情况下,token的数量是无限制的;你可以将其设置为例如10,000个token,如下所示:
final JsonFactory JSON_FACTORY = JsonFactory.builder()
.streamReadConstraints(StreamReadConstraints.builder().设置最大令牌数(10000).build())
.build();
下一步会是什么呢?
好的,下一步显然是发布 2.18.0 版本。到目前为止,2.18.0-rc1 版本中有一个回归问题被报告了,但现在这个问题已经被解决了,因此目前没有阻碍发布的障碍。不过我想等上一周左右,让 Jackson 的用户和开发者有更多时间进行测试。
但是除此之外——现在终于可以更专注于让Jackson 3.0.0准备好进入候选发布阶段了。分支本身已经准备了好几年,通过了测试套件等测试。但是还有一些决定要做,无论是项目和仓库结构方面——我们应该在现有仓库中使用单独的2.x和3.x分支,还是创建新的?——还是关于功能集。比如,是否还有大重构或重命名等待完成?目前,任何改变都是可能的:但是,一旦3.0.0发布,API将再次部分冻结(对于3.x系列)。因此,任何破坏性更改应在3.0.0发布前完成。
共同学习,写下你的评论
评论加载中...
作者其他优质文章