Schema Registry

1,449 阅读29分钟

本章内容包括:

  • 使用字节意味着序列化规则
  • 什么是模式以及为什么需要使用模式
  • 模式注册表是什么
  • 确保与变更的兼容性 — 模式演进
  • 理解主题名称
  • 使用引用重用模式

在第2章中,你了解了Kafka流平台的核心,即Kafka broker。特别是,你了解了broker作为存储层将传入消息追加到主题,作为不可变的分布式事件日志。一个主题表示包含日志文件的目录。

由于生产者通过网络发送消息,它们必须首先将消息序列化为二进制格式,即字节数组。Kafka broker不会以任何方式更改消息,它以相同的格式存储消息。当broker响应消费者的提取请求时,情况也是如此;它检索已序列化的消息并通过网络发送。

通过仅将消息作为字节数组处理,broker完全不关心消息所代表的数据类型,也完全独立于生产和消费消息的应用程序及这些应用程序使用的编程语言。将broker与数据格式解耦使得任何使用Kafka协议的客户端都能够生产或消费消息。

Objects

虽然字节对于存储和通过网络传输至关重要,但开发人员在更高抽象层次上工作效率更高:即对象级别。那么,这种转换发生在哪里呢?发生在客户端层面,即消息的生产者和消费者(见图3.1)。

image.png

从这个示例图中可以看出,消息生产者使用序列化器的实例将消息对象转换为字节,然后将其发送到broker上的主题。消息消费者则执行相反的过程:从主题接收字节,并使用反序列化器的实例将字节转换回相同的对象格式。生产者和消费者与(反)序列化器解耦;它们分别调用序列化或反序列化方法(见图3.2)。

image.png

正如图3.2所示,生产者期望使用序列化器接口的实例,并调用Serializer.serialize方法,传递给定类型的对象,并获取字节流。消费者则使用反序列化器接口。消费者通过Deserializer.deserialize方法提供一个字节数组,并返回一个给定类型的对象。

生产者和消费者通过配置参数获取(反)序列化器。我们将在本章后面看到示例。

请注意,我在本章及整个章节中提到了生产者和消费者,但我们只会深入到足以理解本章所需的上下文。生产者和消费者客户端的详细内容将在下一章讨论。

我在这里强调的重点是,生产者为给定主题序列化的对象类型预期与消费者反序列化的对象类型相同。由于生产者和消费者完全不了解对方,因此这些消息或事件域对象代表了生产者和消费者之间的隐式契约。

那么,现在的问题是,是否存在一种开发者可以使用的东西,可以告诉他们消息的正确结构?对于这个问题的答案是,模式(schema)存在。

什么是模式,为什么需要它?

当你对开发者提到“模式”这个词时,他们首先想到的是数据库模式。数据库模式描述了其结构,包括数据库表中列的名称和启动项,以及表之间的关系。但我在这里所指的模式,虽然在目的上有些相似,但实际上有很大区别。

对于我们的目的,我指的是一个与语言无关的对象描述,包括对象的名称、对象上的字段以及每个字段的类型。以下示例是一个可能的JSON格式模式的清单:

{
"name":"Person",         #1
  "fields": [                            #2
    {"name": "name", "type":"string"},   #3
    {"name": "age", "type": "int"},
    {"name": "email", "type":"string"}
  ]
}

在这里,我们的虚构模式描述了一个名为Person的对象,带有我们预期在这样一个对象上找到的字段。现在,我们有了一个结构化的对象描述,生产者和消费者可以将其用作关于对象在序列化前后应该如何看起来的协议或契约。

在第3.2.9节中,我将详细介绍如何在消息构建和(反)序列化中使用模式。

但目前,我想回顾一些我们到目前为止已经建立的关键点:

  • Kafka broker只处理二进制格式的消息(字节数组)。

  • Kafka生产者和消费者负责消息的(反)序列化。此外,由于这两者互不了解,记录之间形成了一种契约。

  • 我们还学到了,通过使用模式,可以明确生产者和消费者之间的契约。所以,我们知道为什么要使用模式,但到目前为止我们定义的内容有些抽象,我们需要回答以下关于如何实现的问题:

    • 在应用程序开发生命周期中如何使用模式?
    • 鉴于序列化和反序列化与Kafka生产者和消费者解耦,他们如何使用确保消息格式正确的序列化?
    • 如何强制使用正确版本的模式?毕竟,变更是不可避免的。

这些问题的答案是模式注册表(Schema Registry)。

模式注册表是什么?

模式注册表提供了一个集中式应用程序,用于存储模式、模式验证和合理的模式演进(消息结构变更)过程。更重要的是,它作为生产者和消费者客户端可以快速发现的模式真实来源。模式注册表提供了序列化器和反序列化器,您可以用它们来配置Kafka生产者和Kafka消费者,从而简化与Kafka一起工作的应用程序的开发过程。

模式注册表的序列化代码支持Avro(avro.apache.org/docs/curren…)和Protocol(avro.apache.org/docs/curren…) Buffers(developers.google.com/protocol-bu…)。我将在接下来称Protocol Buffers为“Protobuf”。此外,模式注册表还支持使用JSON Schema(json-schema.org/)编写的模式,但这更像是一种规范而非框架。随着本章的进展,我将详细介绍如何使用Avro和Protobuf JSON Schema,但现在让我们从高层次上了解模式注册表的工作原理,如图3.3所示。

image.png

让我们快速浏览一下基于这个插图的模式注册表工作原理:

  1. 生产者调用serialize方法时,一个模式注册表感知的序列化器通过HTTP检索并将模式存储在本地缓存中。
  2. 嵌入在生产者中的序列化器对记录进行序列化。
  3. 生产者将序列化的消息(字节)发送到Kafka。
  4. 消费者读取字节。
  5. 消费者中的模式注册表感知的反序列化器检索模式并将其存储在本地缓存中。
  6. 消费者根据模式对字节进行反序列化。
  7. 模式注册表服务器生成包含模式的消息,并将其存储在 _schemas 主题中。

提示:虽然我将模式注册表呈现为Kafka事件流平台的重要部分,但它并非必需。请记住,Kafka生产者和消费者与它们使用的序列化器和反序列化器是解耦的。只要提供一个实现适当接口的类,就可以与生产者或消费者一起正常工作。但是,您将失去使用模式注册表时带来的验证检查。我将在本章末尾介绍如何在没有模式注册表的情况下进行序列化。

虽然前面的插图让您了解了模式注册表的工作原理,但我想在这里指出一个重要的细节。尽管序列化器或反序列化器会向模式注册表检索给定记录类型的模式,但它只会在第一次遇到没有该记录类型模式的情况下执行此操作。之后,用于(反)序列化操作的模式将从本地缓存中检索。

获取模式注册表

我们的第一步是启动模式注册表。同样,为了加快学习和开发过程,您将使用Docker Compose,所以请从书籍源代码的根目录获取docker-compose.yml文件。

这个文件类似于您在第2章中使用的docker-compose.yml文件。但是,除了Kafka镜像之外,还有一个用于模式注册表镜像的条目。请执行 docker-compose up -d 命令。为了提醒您Docker命令的用法,选项 -d 表示“后台模式”,这意味着Docker容器将在后台运行,释放出您执行命令的终端窗口。

架构

在深入讨论如何使用模式注册表之前,我们应该先了解其设计的高层视图。模式注册表是一个分布式应用程序,独立于Kafka代理之外。客户端通过REST API与模式注册表进行通信。客户端可以是序列化器(生产者)、反序列化器(消费者)、构建工具插件,或者使用curl进行的命令行请求。在第3.2.6节中,我将介绍如何使用构建工具插件,例如Gradle。

模式注册表使用Kafka作为其存储(预写日志),用于存放所有模式的 _schemas 主题,这是一个单分区的紧凑型主题。它采用主要架构,这意味着部署中有一个主节点和其他节点为次要节点。

注意:双下划线字符(__)是Kafka主题命名约定,表示内部主题,不适合公开使用。从现在开始,我们简称这个主题为 schemas。

主要架构意味着部署中只有主节点可以写入 schemas 主题。部署中的任何节点都可以接受存储或更新模式的请求,但次要节点会将请求转发给主节点。让我们看看图3.4来说明这一点。

image.png

任何时候,当客户端注册或更新一个模式时,主节点都会向 _schemas 主题发送一个记录。模式注册表使用Kafka生产者进行写入,所有节点都使用消费者读取更新。因此,您可以看到模式注册表通过Kafka主题备份其本地状态,使模式变得非常持久。

注意:在书中的所有示例中使用模式注册表时,您将只使用单节点部署,适合本地开发。

但所有模式注册表节点都服务于客户端的读取请求。如果任何次要节点收到注册或更新请求,它们会将其转发给主节点。然后,次要节点会从主节点返回响应。让我们通过图3.5来看看这个架构的插图,以加深您对它的理解。

image.png

现在我们已经概述了架构,让我们通过使用模式注册表的REST API发出几个基本命令开始工作。

通信:使用模式注册表的REST API

到目前为止,我们已经介绍了模式注册表的工作原理。现在,是时候通过上传模式并运行一些额外的命令来实际操作了,以获取有关您上传的模式的更多信息。在最初的命令中,您将在终端窗口中使用 curl 和 jq。

curl(curl.se/)是一个用于通过URL处理数据的命令行实用工具。jq(stedolan.github.io/jq/)是一个命令行JSON处理器。要为您的平台安装jq,请访问jq下载站点:stedolan.github.io/jq/download…。对于curl,在Windows]() 10+和macOS上应该已经包含。在Linux上,您可以通过包管理器安装它。如果您使用的是macOS,可以使用homebrew(brew.sh/)安装这两者。

在后续的章节中,您将使用Gradle插件与模式注册表进行交互。在了解了不同的REST API调用如何工作之后,您将使用Gradle插件和一些简单的生产者和消费者示例来实际运行序列化操作。

通常情况下,您将使用构建工具插件来执行模式注册表的操作。首先,它们比从命令行运行API调用要快得多,其次,它们将自动生成来自模式的源代码。我们将在第3.2.6节中介绍如何使用构建工具插件。

注意:有用于与模式注册表工作的Maven和Gradle插件,但本书的源代码项目使用Gradle,因此您将使用该插件。

注册模式

在开始之前,确保您已经运行了 docker-compose up -d 以启动一个模式注册表实例。但现在还没有任何已注册的模式,所以您的第一步是注册一个模式。让我们来点有趣的,为漫威漫画的超级英雄——复仇者联盟创建一个模式。您将使用 Avro 作为第一个模式,现在让我们花点时间讨论一下这种格式。

{"namespace": "bbejeck.chapter_3",   #1
 "type": "record",                         #2
 "name": "Avenger",                         #3
 "fields": [                                 #4
     {"name": "name", "type": "string"},
     {"name": "real_name", "type": "string"},          #5
     {"name": "movies", "type":
                      {"type": "array", "items": "string"},
      "default": []         #6
    }
  ]
}

你已经使用 JSON 格式定义了 Avro 架构。在 3.2.6 节中讨论用于代码生成和与 Schema Registry 交互的 Gradle 插件时,你将使用相同的架构文件。由于 Schema Registry 支持 Protobuf 和 JSON Schema 格式,我们也来看看相同类型在这些架构格式中的表示。

syntax = "proto3";           #1

package bbejeck.chapter_3.proto;        #2
option java_multiple_files = true;    #3

message Avenger {                  #4
    string name = 1;           #5
    string real_name = 2;
    repeated string movies = 3;   #6

}

Protobuf 架构看起来更像常规代码,因为其格式不是 JSON。Protobuf 使用分配给字段的数字来标识消息二进制格式中的那些字段。虽然 Avro 规范允许设置默认值,但在 Protobuf(版本 3)中,每个字段都被视为可选的,但你不提供默认值。相反,Protobuf 使用字段的类型来确定默认值。例如,数值字段的默认值是 0;字符串字段的默认值是空字符串,重复字段的默认值是空列表。

请注意,Protobuf 是一个深奥的主题,而本书讨论的是 Kafka 事件流模式,所以我只会涵盖足够的 Protobuf 规范,让你可以入门并使用它感到得心应手。有关详细信息,你可以阅读这里的语言指南:mng.bz/5oB1

现在让我们看看 JSON Schema 版本。

{
  "$schema": "http://json-schema.org/draft-07/schema#",    #1
  "title": "Avenger",
  "description": "A JSON schema of Avenger object",
  "type": "object",                                             #2
  "javaType": "bbejeck.chapter_3.json.SimpleAvengerJson",    #3
  "properties": {     #4
    "name": {
      "type": "string"
    },
    "realName": {
      "type": "string"
    },
    "movies": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "default": []     #5
    }
  },
  "required": [
    "name",
    "realName"
  ]
}

JSON Schema 架构类似于 Avro 版本,因为两者都使用 JSON 作为架构文件。两者之间最大的区别在于,在 JSON Schema 中,你在 properties 元素下列出对象字段,而不是在 fields 数组中列出,并且在字段本身中,你只需声明名称,而不是使用 name 元素。

请注意,JSON 格式编写的架构和遵循 JSON Schema 格式的架构之间存在区别。JSON Schema 是“JSON Schema 是一种词汇表,使得 JSON 数据在大规模下保持一致性、有效性和互操作性”(json-schema.org/。与 Avro 和 Protobuf 一样,我将重点介绍足够的内容,以便你在项目中使用它,但如果需要深入了解,你应该访问 json-schema.org/ 获取更多信息。

我在这里展示了不同的架构格式以供比较。但在本章的其余部分,为了节省空间,我通常只会在示例中展示一个版本的架构。

现在我们已经回顾了这些架构,接下来让我们注册一个。使用命令行通过 REST API 注册架构的命令如下所示。

jq '. | {schema: tojson}' src/main/avro/avenger.avsc | \   #1
curl -s -X
   POST http://localhost:8081/subjects/avro-avengers-value/versions\    #2
   -H "Content-Type: application/vnd.schemaregistry.v1+json" \     #3
   -d @-  \           #4
   | jq    #5

运行此命令后的结果应如下所示。

{
  "id": 1
}

POST 请求的响应是 Schema Registry 分配给新架构的 ID。Schema Registry 为每个新添加的架构提供一个唯一的 ID(单调递增的数字)。客户端使用这个 ID 将架构存储在它们的本地缓存中。

在我们继续讨论其他命令之前,我想提醒你注意列表 3.5,特别是 subjects/avro-avengers-value/,它指定了架构的主题名称。Schema Registry 使用主题名称来管理对架构所做的任何更改的范围。在这种情况下,它是 avro-avengers-value,这意味着进入 avro-avengers 主题的值(在键值对中)需要符合注册架构的格式。我们将在 3.3 节中讨论主题名称及其在更改中的作用。

接下来,让我们看看一些从 Schema Registry 中检索信息的可用命令。假设你正在构建一个新的应用程序以与 Kafka 一起使用。你听说过 Schema Registry,并且你想查看一个同事开发的特定架构,但你不记得名字,而且现在是周末,你不想打扰任何人。你可以使用以下列表中的命令列出所有注册架构的主题。

curl -s "http://localhost:8081/subjects" | jq

此命令的响应是一个包含所有主题的 JSON 数组。由于我们目前只注册了一个架构,结果应如下所示:

[  "avro-avengers-value"]

很好,你在这里找到了你要找的内容——注册在 avro-avengers 主题下的架构。

现在假设最新的架构有一些更改,你想查看之前的版本。但问题是你不知道版本历史。下一个列表展示了给定架构的所有版本。

curl -s "http://localhost:8081/subjects/avro-avengers-value/versions" | jq

这个命令返回给定架构的版本的 JSON 数组。在我们这里的情况下,结果应该是这样的:

[  1]

现在你已经得到了所需的版本号,可以运行另一个命令来检索特定版本的模式。

curl -s "http://localhost:8081/subjects/avro-avengers-value/versions/1"\
 | jq '.'

运行此命令后,你应该会看到类似如下内容:

{
  "subject": "avro-avengers-value",
  "version": 1,
  "id": 1,
  "schema": "{\"type\\":\\"record\\",\\"name\\":\\"AvengerAvro\\",
      \"namespace\\":\\"bbejeck.chapter_3.avro\\",\\"fields\\"
      :[{\"name\\":\\"name\\",\\"type\\":\\"string\\"},{\\"name\\"
        :\"real_name\\",\\"type\\":\\"string\\"},{\\"name\\"
          :\"movies\\",\\"type\\":{\\"type\\":\\"array\\"
            ,\"items\\":\\"string\\"},\\"default\\":[]}]}"
}

架构字段的值被格式化为字符串,因此引号被转义,所有换行符都被移除。通过几个简单的控制台窗口命令,你已经能够找到架构、确定版本历史并查看特定版本的架构。

顺便说一下,如果你不关心架构的先前版本,只想要最新的版本,则不需要知道实际的最新版本号。你可以使用以下列表中的 REST API 调用来检索最新的架构。

curl -s "http://localhost:8081/subjects/avro-avengers-value/
  versions/latest" | jq '.'

这里我就不展示这个命令的结果了,因为它和前一个命令的结果是相同的。

这只是一个关于 Schema Registry 的 REST API 的部分命令的快速介绍。这只是可用命令的一小部分。完整参考请访问 mng.bz/OZEo

接下来,我们将继续介绍如何使用 Gradle 插件与 Schema Registry 以及 Avro、Protobuf 和 JSON Schema 架构进行工作。

插件和序列化平台工具

到目前为止,你已经了解到由生产者写入和消费者读取的事件对象代表了生产者和消费者客户端之间的契约。你还了解到这种隐式契约可以通过模式具体化。此外,你已经看到如何使用 Schema Registry 存储模式,并在生产者和消费者客户端需要序列化和反序列化记录时提供这些模式。

在接下来的部分中,你将看到 Schema Registry 的更多功能,包括测试模式的兼容性、不同的兼容模式以及如何相对轻松地更改或进化模式,使参与的生产者和消费者客户端受益。

但是到目前为止,你只处理了一个模式文件,这仍然有点抽象。如本章前面所述,开发人员在构建应用程序时是与对象打交道的。我们的下一步是看看如何将这些模式文件转换为你可以在应用程序中使用的具体对象。

Schema Registry 支持 Avro、Protobuf 和 JSON Schema 格式的模式。Avro 和 Protobuf 是序列化平台,它们提供了用于处理各自格式模式的工具。最重要的工具之一是从模式生成对象的能力。

由于 JSON Schema 是一个标准,而不是一个库或平台,你需要使用开源工具进行代码生成。对于本书,我们使用 github.com/eirnym/js2p… 项目。对于不使用 Schema Registry 的(反)序列化,我推荐使用 github.com/FasterXML/j… 项目的 ObjectMapper。

从模式生成代码使你的开发工作更加轻松,因为它自动化了创建域对象的重复样板过程。此外,由于你在源代码控制(我们的情况是 Git)中维护模式,错误的机会(例如在创建域对象时将字段类型设为字符串而不是长整型)显著减少。另外,当你对模式进行更改时,你提交更改,其他开发人员拉取更新并重新生成代码,这样每个人都能快速更新。

在本书中,我们将使用 Gradle 构建工具 (gradle.org/) 来管理书中的源代码。幸运的是,我们可以使用 Gradle 插件来处理 Schema Registry、Avro、Protobuf 和 JSON Schema。具体来说,我们将使用以下插件:

注意:需要注意的是使用 JSON 编写的模式文件(如 Avro 模式)与使用 JSON Schema 格式(json-schema.org/)的文件之间的区别。对于 Avro 文件,它们是以 JSON 编写的,但遵循 Avro 规范。JSON Schema 文件则遵循官方的 JSON Schema 规范。

使用 Avro、Protobuf 和 JSON Schema 的 Gradle 插件,你不需要学习如何使用每个组件的单独工具;插件会处理所有工作。我们还将使用一个 Gradle 插件来处理与 Schema Registry 的大多数交互。让我们开始使用 Gradle 命令上传模式,而不是在控制台中使用 REST API 命令。

上传模式文件

我们首先将使用 Gradle 注册一个模式。我们将使用 REST API 命令部分的相同 Avro 模式。要上传模式,请确保将当前目录 (CD) 更改为项目的基目录,并运行以下 Gradle 命令:

./gradlew streams:registerSchemasTask

运行此命令后,你应该会在控制台中看到类似 “BUILD SUCCESSFUL” 的信息。注意,你只需要在命令行中输入 Gradle 任务的名称(来自 Schema Registry 插件),该任务会注册 streams/build.gradle 文件中 register { } 块内的所有模式。

现在,让我们看看 streams/build.gradle 文件中 Schema Registry 插件的配置。

schemaRegistry {        #1
    url = 'http://localhost:8081'     #2


register {
        subject('avro-avengers-value',             #3
                'src/main/avro/avenger.avsc',    #4
                'AVRO')                        #5

     //other entries left out for clarity
    }

  // other configurations left out for clarity
}

register 块中,你提供了相同的信息,只是以方法调用的形式,而不是 REST 调用中的 URL。实际上,插件代码仍然通过 SchemaRegistryClient 使用 Schema Registry 的 REST API。顺便说一下,你会在源码中的 register 块中注意到几个条目。在浏览源码示例时,你会使用到所有这些条目。

我们很快会介绍更多使用 Gradle Schema Registry 任务的内容,但现在让我们继续讨论如何从模式生成代码。

从模式生成代码

正如我之前所说,使用 Avro 和 Protobuf 平台的一个最佳优势是代码生成工具。使用这些工具的 Gradle 插件通过抽象使用各个工具的细节,将便利性更进一步。要生成由模式表示的对象,只需运行以下 Gradle 任务即可。

代码清单 3.12 生成模型对象

./gradlew clean build

运行此 Gradle 命令会为项目中的所有模式类型(Avro、Protobuf 和 JSON Schema)生成 Java 代码。现在我们应该讨论在项目中放置模式的位置。Avro 和 Protobuf 模式的默认位置分别是 src/main/avrosrc/main/proto 目录。JSON Schema 模式的位置是 src/main/json 目录,但你需要在 build.gradle 文件中显式配置这个位置。

代码清单 3.13 配置 JSON Schema 模式文件的位置

jsonSchema2Pojo {
    source = files("${project.projectDir}/src/main/json")        #1
    targetDirectory = file("${project.buildDir}/generated-main-json-java")  #2
    // 为简洁起见,省略了其他配置
}
  1. source 配置指定了生成工具可以找到模式的位置
  2. targetDirectory 是工具写入生成的 Java 对象的位置

注意:除非另有说明,这里的所有示例均指 streams 子目录中的模式。

在这里你可以看到 js2p-gradle 插件输入和输出目录的配置。Avro 插件默认将生成的文件放在名为 generated-main-avro-java 的 build 目录下的子目录中。对于 Protobuf,我们在 build.gradle 文件的 Protobuf 块中配置输出目录,以匹配 JSON Schema 和 Avro 的模式,如下所示。

代码清单 3.14 配置 Protobuf 输出

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.25.0'     #1
    }
}
  1. 指定 protoc 编译器的位置

我想快速讨论一下代码清单 3.14 中的注释。要使用 Protobuf,你需要安装编译器 protoc。默认情况下,插件会搜索 protoc 可执行文件。但我们可以使用来自 Maven Central 的预编译版本的 protoc,这意味着你不必显式安装它。但如果你更喜欢使用本地安装,可以在 protoc 块内使用 path = path/to/protoc/compiler 指定路径。

我们已经完成了从模式生成代码的工作。现在是时候运行一个端到端的示例了。

端到端示例

我们将综合你所学的内容,运行一个简单的端到端示例。到目前为止,你已经注册了模式并生成了所需的 Java 文件。接下来的步骤是:

  1. 从生成的 Java 文件创建一些领域对象。
  2. 将你创建的对象发送到 Kafka 主题。
  3. 从相同的 Kafka 主题消费你刚刚发送的对象。

虽然步骤 2 和 3 与客户端关系更大,但我们希望从这个角度来看待它。你正在创建从模式文件生成的 Java 对象实例,因此请注意字段,并观察对象如何符合模式的结构。同时,关注与 Schema Registry 相关的配置项、序列化器或反序列化器以及用于与 Schema Registry 通信的 URL。

注意:在这个示例中,你将使用 Kafka 生产者和消费者,但我不会介绍它们的详细工作。如果你还不熟悉生产者和消费者客户端也没关系。我将在下一章详细介绍生产者和消费者。但现在,请按照示例操作。

如果你还没有注册模式文件并生成 Java 代码,请现在执行这些步骤。我会重复这些步骤并确认你已运行 docker-compose up -d 以确保你的 Kafka broker 和 Schema Registry 正在运行。

代码清单 3.15 注册模式并生成 Java 文件

./gradlew streams:registerSchemasTask       #1

./gradlew clean build     #2
  1. 注册模式文件
  2. 从模式生成 Java 对象

现在让我们关注 Schema Registry 的特定配置。请查看源码中的 bbejeck.chapter_3.producer.BaseProducer 类。目前,我们只需要查看以下两个配置;我们将在下一章介绍更多生产者的配置:

producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
    keySerializer);                                  #1
producerProps.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG,
    "http://localhost:8081");      #2
  1. 指定要使用的序列化器
  2. 设置 Schema Registry 的位置

第一个配置设置了生产者将使用的序列化器。记住,KafkaProducer 与序列化器的类型是解耦的;它只是调用 serialize 方法并返回一个字节数组进行发送。所以你需要提供正确的序列化器类。

在这个例子中,我们将使用从 Avro 模式生成的对象,所以你使用 KafkaAvroSerializer。如果你查看 bbejeck.chapter_3.producer.avro.AvroProducer 类(它扩展了 BaseProducer),你会看到它传递 KafkaAvroSerializer.class 给父对象构造函数。第二个配置指定序列化器用于与 Schema Registry 通信的 HTTP 端点。这些配置使得图 3.3 中描述的交互成为可能。

接下来,让我们快速看看如何创建一个对象。

代码清单 3.16 从生成的代码实例化一个对象

var blackWidow = AvengerAvro.newBuilder()
                .setName("Black Widow")
                .setRealName("Natasha Romanova")
                .setMovies(List.of("Avengers", "Infinity Wars",
                  "End Game")).build();

现在你可能会想,“这段代码创建了一个对象,有什么大不了的?”虽然这可能是一个小点,但我想强调的是你在这里不能做的事情。你只能用正确的类型填充预期的字段,从而强制执行以所需格式生成记录的合同。你可以更新模式并重新生成代码。

但是,通过进行更改,你必须注册新的模式,并且更改必须与主题名称的当前兼容性格式相匹配。现在你可以看到 Schema Registry 如何强制执行生产者和消费者之间的“合同”。我们将在第 3.4 节介绍兼容性模式和允许的更改。

现在,让我们运行以下 Gradle 命令,将对象生成到 avro-avengers 主题。

代码清单 3.17 运行 AvroProducer

./gradlew streams:runAvroProducer

运行此命令后,你会看到类似以下的输出:

DEBUG [main] bbejeck.chapter_3.producer.BaseProducer - Producing records
 [{"name": "Black Widow", "real_name": "Natasha Romanova", "movies":["Avengers", "Infinity Wars", "End Game"]},
{"name": "Hulk", "real_name": "Dr. Bruce Banner", "movies":
["Avengers", "Ragnarok", "Infinity Wars"]},
{"name": "Thor", "real_name": "Thor", "movies":
["Dark Universe", "Ragnarok", "Avengers"]}]

在应用程序生成这几条记录后,它会自动关闭。

注意:务必要按照这里显示的方式运行此命令,包括前面的 : 字符。我们有三个不同的 Gradle 模块用于我们的 Schema Registry 练习。我们需要确保运行的是特定模块的命令。在这种情况下,: 只执行主模块;否则,它将为所有模块运行生产者,示例将失败。

运行此命令并不会带来什么激动人心的事情,但它演示了使用 Schema Registry 进行序列化的简便性。生产者检索模式,存储在本地,并以正确的序列化格式将记录发送到 Kafka——所有这些都无需你编写任何序列化或领域模型代码。恭喜你,你已经将序列化的记录发送到 Kafka!

提示:查看运行此命令生成的日志文件可能会有所帮助。它位于书籍源码的 streams/logs/ 目录中。log4j 配置会在每次运行时覆盖日志文件,因此在运行下一步之前先检查它。

现在,让我们运行一个消费者来反序列化这些记录。但就像我们对生产者所做的那样,我们将重点放在反序列化和使用 Schema Registry 所需的配置上。

代码清单 3.18 使用 Avro 的消费者配置

consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
    KafkaAvroDeserializer.class);                   #1
consumerProps.put(KafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG,
    true);    #2
consumerProps.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG,
    "http://localhost:8081");       #3
  1. 使用 Avro 反序列化
  2. 配置为使用 SpecificAvroReader
  3. Schema Registry 的主机:端口

你会注意到你将 SPECIFIC_AVRO_READER_CONFIG 设置为 true。SPECIFIC_AVRO_READER_CONFIG 做什么呢?为了回答这个问题,让我们简要讨论一下处理 Avro、Protobuf 和 JSON Schema 序列化对象。

当反序列化 Avro、Protobuf 或 JSON Schema 对象时,反序列化的对象类型可以是特定对象类型或非特定的容器对象。例如,当 SPECIFIC_AVRO_READER_CONFIG 设置为 true 时,消费者内部的反序列化器将返回一个类型为 AvroAvenger 的对象,即特定的对象类型。但是,如果将 SPECIFIC_AVRO_READER_CONFIG 设置为 false,反序列化器将返回一个类型为 GenericRecord 的对象。GenericRecord 仍然遵循相同的模式并具有相同的内容,但对象本身没有任何类型意识。顾名思义,它只是一个字段的通用容器。以下代码示例应该能清楚地说明我的意思。

代码清单 3.19 特定 Avro 记录与 GenericRecord

AvroAvenger avenger = // returned from consumer with
  //SPECIFIC_AVRO_READER_CONFIG=true
avenger.getName();
avenger.getRealName();      #1
avenger.getMovies();

GenericRecord genericRecord = // returned from consumer with
  //SPECIFIC_AVRO_READER_CONFIG=false
if (genericRecord.hasField("name")) {
   genericRecord.get("name");
}

if (genericRecord.hasField("real_name")) {     #2
    genericRecord.get("real_name");
}

if (GenericRecord.hasField("movies")) {
    genericRecord.get("movies");
}
  1. 访问特定对象上的字段
  2. 访问通用对象上的字段

这个简单的代码示例展示了特定返回类型和通用类型之间的区别。使用 AvroAvenger 对象时,我们可以直接访问可用的属性,因为对象“知道”它的结构并提供了访问这些字段的方法。但使用 GenericRecord 对象时,你需要查询它是否包含特定字段,然后再尝试访问它。

注意:特定版本的 Avro 模式不仅是一个 POJO(普通 Java 对象),而且扩展了 SpecificRecordBase 类。

注意到使用 GenericRecord 时,你需要精确访问模式中指定的字段,而特定版本使用更熟悉的驼峰命名法。两者之间的区别在于,使用特定类型时,你知道结构,但使用通用类型时,由于它可以表示任意类型,你需要查询不同的字段以确定其结构。你将像操作 HashMap 一样操作 GenericRecord

然而,你不必完全在黑暗中操作。你可以通过调用 GenericRecord.getSchema().getFields()GenericRecord 获取字段列表。然后,你可以迭代 Field 对象列表,并通过调用 Field.name() 获取名称。此外,你还可以通过调用 GenericRecord.getSchema().getFullName() 获取模式的名称;此时,你应该知道记录包含哪些字段。更新字段时,你将遵循类似的方法。

代码清单 3.20 更新或设置特定和通用记录上的字段

avenger.setRealName("updated name")
genericRecord.put("real_name", "updated name")

这个小示例表明特定对象为你提供了熟悉的 setter 功能。但在通用版本中,你必须显式声明要更新的字段。同样,你会注意到在更新或设置字段时,通用版本具有 HashMap 的行为。

Protobuf 也提供了类似的功能来处理特定或任意类型。要处理 Protobuf 中的任意类型,你会使用 DynamicMessage。与 Avro 的 GenericRecord 类似,DynamicMessage 提供了发现类型和字段的函数。使用 JSON Schema 时,特定类型只是从 Gradle 插件生成的对象;没有像 Avro 或 Protobuf 那样的框架代码。通用版本是 JsonNode 类型,因为反序列化器使用 jackson-databindgithub.com/FasterXML/j…API进行序列化和反序列化。

注意:本章的源码包含了处理 Avro、Protobuf 和 JSON Schema 特定和通用类型的示例。

那么,什么时候使用特定类型而不是通用类型呢?如果 Kafka 主题中只有一种记录类型,你将使用特定版本。然而,如果主题中有多种事件类型,你将希望使用通用版本,因为每个消费的记录可能是不同的类型。我们将在本章后面讨论在单个主题中使用多种事件类型,并在第 4 章讨论 Kafka 客户端时再次讨论。

最后要记住的是使用特定记录类型:将 kafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG 设置为 true。SPECIFIC_AVRO_READER_CONFIG 的默认值为 false,因此如果未设置配置,消费者将返回 GenericRecord 类型。

在讨论了不同的记录类型后,让我们继续走完使用 Schema Registry 的第一个端到端示例。你已经使用之前上传的模式生成了一些记录。现在,你需要启动一个消费者来演示如何使用模式反序列化这些记录。同样,查看日志文件可能会有所帮助。你会看到嵌入的反序列化器仅下载第一个记录的模式,因为它在初次检索后会被缓存。

我还要注意,以下示例使用 bbejeck.chapter_3.consumer.avro.AvroConsumer,它使用了特定类类型和 GenericRecord 类型。示例运行时,代码会打印出消费记录的类型。

注意:源码中有类似的 Protobuf 和 JSON Schema 示例。

让我们现在通过执行以下命令运行消费者示例,该命令位于书籍源码项目的根目录中。

代码清单 3.21 运行 AvroConsumer

./gradlew streams:runAvroConsumer

注意:再次强调这里关于运行命令的注意事项,包括前面的 : 字符;否则,它将为所有模块运行消费者,示例将无法工作。

AvroConsumer 打印出消费的记录并关闭。恭喜你,你已经使用 Schema Registry 序列化和反序列化了记录!

到目前为止,我们已经介绍了 Schema Registry 支持的序列化框架类型,如何编写和添加模式文件,以及使用模式的基本示例。在上传模式的部分,我提到了术语 subject 以及它如何定义模式演化的范围。下一节将教你如何使用不同的 subject 名称策略。

主题名称策略

Schema Registry 使用“subject”概念来控制模式演化的范围。可以将“subject”视为特定模式的命名空间。换句话说,随着业务需求的变化,你需要修改模式文件以适应领域对象的相应变化。例如,对于我们的 AvroAvenger 领域对象,你可能想要移除英雄的真实(平民)姓名,并添加他们的能力列表。Schema Registry 使用“subject”来查找现有模式,并将其与新模式进行比较。它执行此检查以确保变更与当前设置的兼容性模式兼容。我们将在第 3.4 节讨论兼容性模式。

“subject”名称策略确定了 Schema Registry 进行兼容性检查的范围。有三种类型的“subject”名称策略:TopicNameStrategy(主题名称策略)、RecordNameStrategy(记录名称策略)和 TopicRecordNameStrategy(主题记录名称策略)。你可以从策略名称推断名称空间的范围,但详细了解这些策略也很值得。现在让我们深入讨论这些不同的策略。

注意:默认情况下,所有序列化器在序列化时会尝试注册模式,如果在本地缓存中找不到对应的 ID。自动注册是一个很好的开发特性,但在生产环境中可能需要通过生产者配置设置 auto.register.schemas=false 来关闭它。另一个不希望自动注册的例子是当使用带有引用的 Avro union 模式时。我们将在本章后面更详细地讨论这个主题。

TopicNameStrategy

TopicNameStrategy 是 Schema Registry 中的默认主题名称策略。主题名称是从主题的名称派生而来。在本章前面注册模式时,你已经看到了 TopicNameStrategy 的运作方式。更准确地说,主题名称是 topic-name-key 或 topic-name-value,因为你可以为键和值定义不同类型,需要不同的模式。

TopicNameStrategy 确保一个主题只有一个数据类型,因为你不能在同一个主题名称下注册不同类型的模式。在许多情况下,每个主题仅包含一个类型是有道理的,例如,如果你根据它们存储的事件类型命名主题,那么主题将只包含一种记录类型。

TopicNameStrategy 的另一个优点是,由于模式强制仅限于单个主题,你可以在另一个使用相同记录类型但不同模式的主题上进行注册(参见图 3.6)。考虑两个部门使用相同记录类型但使用其他主题名称的情况。使用 TopicNameStrategy,这些部门可以为相同的记录类型注册完全不同的模式,因为模式的范围仅限于特定的主题。

image.png

由于 TopicNameStrategy 是默认策略,你无需指定任何额外的配置。当你注册模式时,对于值模式,你会使用 <topic>-value 的格式作为主题,对于键模式,使用 <topic>-key 作为主题。在两种情况下,你都会用实际主题的名称替换 <topic> 标记。

但有些情况下可能存在 closely related events,你希望将这些记录发送到同一个主题中。在这种情况下,你将需要选择一种允许主题中包含不同类型和模式的策略。

RecordNameStrategy

RecordNameStrategy 使用 Java 对象表示的模式的完全限定类名作为主题名称(见图 3.7)。通过使用记录名称策略,你可以在同一个主题中拥有多种类型的记录。但关键点在于,这些记录具有逻辑上的关系,尽管它们的物理布局不同。

image.png

当何时选择RecordNameStrategy呢?想象一下,你部署了不同的物联网(IoT)传感器。一些传感器测量不同的事件,因此它们的记录结构是不同的。但是,你仍希望将它们放置在同一个Kafka主题中。

RecordNameStrategy适用于这种情况,因为它允许在同一个主题中放置具有不同模式的多种类型的记录。尽管它们的物理布局不同,但这些记录在逻辑上存在关联。

使用RecordNameStrategy时,会在具有相同记录名称的模式之间进行兼容性检查。此检查还会扩展到使用具有相同记录名称的所有主题。

要使用RecordNameStrategy,你需要在为特定记录类型注册模式时使用完全限定的类名。例如,在我们的示例中使用的AvengerAvro对象,你可以按以下清单配置模式注册:

subject('bbejeck.chapter_3.avro.AvengerAvro','src/main/avro/avenger.avsc', 'AVRO')

接下来,你必须配置生产者和消费者,以使用适当的主题名称策略。

生产者配置:

Map<String, Object> producerConfig = new HashMap<>();
producerConfig.put(KafkaAvroSerializerConfig.VALUE_SUBJECT_NAME_STRATEGY, RecordNameStrategy.class);
producerConfig.put(KafkaAvroSerializerConfig.KEY_SUBJECT_NAME_STRATEGY, RecordNameStrategy.class);

消费者配置:

Map<String, Object> consumerConfig = new HashMap<>();
consumerConfig.put(KafkaAvroDeserializerConfig.KEY_SUBJECT_NAME_STRATEGY, RecordNameStrategy.class);
consumerConfig.put(KafkaAvroDeserializerConfig.VALUE_SUBJECT_NAME_STRATEGY, RecordNameStrategy.class);

注意:如果只使用Avro进行值的序列化/反序列化,则无需为键添加配置。此外,键和值的主题名称策略不需要匹配;我在这里只是这样展示。

对于Protobuf,请使用KafkaProtobufSerializerConfigKafkaProtobufDeserializerConfig;对于JSON Schema,请使用KafkaJsonSchemaSerializerConfigKafkaJsonSchemaDeserializerConfig。这些配置仅影响序列化器/反序列化器与Schema Registry交互以查找模式。再次强调,序列化与生产和消费过程是分离的。

需要考虑的一点是,仅使用记录名称时,所有主题必须使用相同的模式。如果希望在一个主题中使用不同的记录,但只考虑该特定主题的模式,那么你需要使用另一种策略。

TopicRecordNameStrategy

正如你从名称中可以推断的那样,TopicRecordNameStrategy策略也允许在同一个主题中包含多种记录类型。然而,对于特定记录的注册模式仅在当前主题的范围内考虑。让我们看一下图3.8,以更好地理解这意味着什么。

image.png

如图3.8所示,topic-A可以针对记录类型Foo具有不同的模式,而topic-B则可以有另一种不同的模式。这种策略允许你在一个主题中拥有多个逻辑相关的类型,但与其他主题隔离,即使对于相同类型,也可以使用不同的模式。

为什么会选择TopicRecordNameStrategy?例如,考虑这种情况。你在interactions主题中有一个CustomerPurchaseEvent事件对象的版本,该主题组合了所有客户事件类型(如CustomerSearchEvent,CustomerLoginEvent等)。但是,在旧的purchases主题中也包含CustomerPurchaseEvent对象,但这是一个遗留系统,因此模式较旧,并包含与较新版本不同的字段。TopicRecordNameStrategy允许这两个主题包含相同类型的对象,但具有不同的模式版本。

类似于RecordNameStrategy,你需要配置该策略,如下所示的配置清单所示。

清单 3.25 TopicRecordNameStrategy的Schema Registry Gradle插件配置

subject('avro-avengers-bbejeck.chapter_3.avro.AvengerAvro',
  'src/main/avro/avenger.avsc', 'AVRO')

然后,你必须为生产者和消费者配置适当的主题名称策略。例如,如下清单所示。

清单 3.26 TopicRecordNameStrategy的生产者配置

Map<String, Object> producerConfig = new HashMap<>();
producerConfig.put(KafkaAvroSerializerConfig.VALUE_SUBJECT_NAME_STRATEGY,
  TopicRecordNameStrategy.class);
producerConfig.put(KafkaAvroSerializerConfig.KEY_SUBJECT_NAME_STRATEGY,
  TopicRecordNameStrategy.class);

清单 3.27 TopicRecordNameStrategy的消费者配置

Map<String, Object> consumerConfig = new HashMap<>();
config.put(KafkaAvroDeserializerConfig.KEY_SUBJECT_NAME_STRATEGY,
  TopicRecordNameStrategy.class);
config.put(KafkaAvroDeserializerConfig.VALUE_SUBJECT_NAME_STRATEGY,
  TopicRecordNameStrategy.class);

注意:这里也适用于注册键的策略。如果只使用Avro来序列化/反序列化值,则无需添加键的配置。此外,键和值的主题名称策略不需要匹配。

为什么会选择TopicRecordNameStrategy而不是TopicNameStrategy或RecordNameStrategy?如果你希望在一个主题中有多种事件类型,你需要灵活地为给定类型在不同主题中使用不同的模式版本。

但是,在考虑到主题中的多种类型时,无论是TopicRecordNameStrategy还是RecordNameStrategy都无法将主题限制为固定集合的类型。使用这些主题名称策略之一将打开主题以容纳无限数量的不同类型。在第3.5节中,我们将介绍如何在处理模式引用时改善这种情况。

这里是对不同主题名称策略的快速总结(表3.1)。将主题名称策略视为一个函数,接受主题名称和记录模式作为参数,并返回一个主题名称。TopicNameStrategy仅使用主题名称,忽略记录模式。RecordNameStrategy则相反:它忽略主题名称,仅使用记录模式。而TopicRecordNameStrategy则同时使用两者作为主题名称。

表3.1 模式策略总结

策略主题中的多种类型不同主题中对象的不同版本
TopicNameStrategy可能
RecordNameStrategy
TopicRecordNameStrategy

到目前为止,我们已经介绍了主题名称策略及其如何使用主题来命名模式的概念。但是,模式管理还有另一个维度:在模式本身内部进行进化性的更改。你如何处理诸如删除或添加字段这样的更改?你希望你的客户端具有前向或后向兼容性?接下来的部分将详细介绍如何处理模式的兼容性。

模式兼容性

在进行模式更改时,你需要考虑与现有模式以及生产者和消费者客户端的兼容性。如果你删除一个字段,这会如何影响生产者序列化记录或消费者反序列化这种新格式?

为了处理这些兼容性问题,Schema Registry提供了四种基本的兼容性模式:BACKWARD(向后兼容)、FORWARD(向前兼容)、FULL(全兼容)和NONE(无兼容性)。此外,还有三种附加的兼容性模式:BACKWARD_TRANSITIVE(向后传递兼容)、FORWARD_TRANSITIVE(向前传递兼容)和FULL_TRANSITIVE(全传递兼容),它们扩展了同名的基本兼容性模式。基本兼容性模式仅保证新模式与其直接前一个版本兼容。传递性兼容性指定新模式与给定模式的所有早期版本适用相同的兼容性模式兼容。

你可以指定全局兼容性级别或每个主题的兼容性级别。

接下来将描述给定兼容性模式的有效更改,并展示你需要对生产者和消费者进行的更改序列。请参阅附录C,了解关于如何更改模式的实际教程。

向后兼容性

向后兼容性是默认的迁移设置。使用向后兼容性,首先更新消费者代码以支持新的模式(见图3.9)。更新后的消费者可以读取使用新模式或直接前一个模式序列化的记录。

image.png

如图3.9所示,消费者可以处理先前的和新的模式。在向后兼容性下,允许的更改包括删除字段或添加可选字段。字段在模式中提供默认值时是可选的。如果序列化的字节不包含可选字段,反序列化器在将字节反序列化回对象时会使用指定的默认值。

正向兼容性

正向兼容性与向后兼容性在字段变更方面是镜像的。在正向兼容性下,您可以添加字段并删除可选字段(见图3.10)。

image.png

首先升级生产者代码可以确保新字段被正确填充,并且只有符合新格式的记录可用。需要更新的消费者仍然可以使用新的模式,因为它会忽略新字段,并且删除的字段具有默认值。

现在,您已经看到了两种兼容性类型:向后兼容性和向前兼容性。正如兼容性名称所示,您必须考虑单向的变更。在向后兼容性中,您首先更新了消费者,因为记录可以以新旧格式的任一形式到达。在向前兼容性中,您首先更新了生产者,以确保从那时起的记录采用新格式。接下来要探讨的最后一种兼容性策略是完全兼容性模式。

完全兼容性

完全兼容性模式允许您添加或删除字段,但有一个限制:您所做的任何更改必须仅限于可选字段。可选字段是指在模式定义中为其提供默认值的字段,这样原始反序列化记录如果没有提供该特定字段,反序列化过程就会使用默认值。

注:Avro 和 JSON Schema 支持显式提供默认值。对于 Protocol Buffers,版本 3(本书中使用的版本),每个字段根据其类型自动具有默认值。例如,数字类型为 0,字符串为 "",集合为空等。

image.png

由于更新后的模式中的字段是可选的,因此这些更改与现有的生产者和消费者客户端是兼容的。因此,在这种情况下,升级顺序由您决定。消费者将继续处理使用新或旧模式生成的记录。

NONE兼容性

指定NONE兼容性意味着Schema Registry不执行任何兼容性检查。不进行兼容性检查意味着可以添加新字段、删除现有字段或更改字段类型。任何更改都会被接受。

不进行任何兼容性检查提供了很大的灵活性。但是,这种折衷之处在于您容易受到破坏性更改的影响,这些更改可能直到最糟糕的时刻——生产环境中才被发现。

可能的做法是每次更新模式时同时升级所有生产者和消费者。另一种可能性是为客户端创建一个新的主题。应用程序可以使用新主题,而不必担心包含旧的、不兼容的模式的记录。

现在,您已经学会了如何在不同的模式兼容性模式下迁移使用新版本带有更改的模式。回顾一下,表3.2是各种兼容性类型的快速摘要表。

模式允许的更改客户端更新顺序保证的兼容性回溯
Backward删除字段,添加可选字段消费者,生产者以前的版本
Backward transitive删除字段,添加可选字段消费者,生产者所有以前的版本
Forward添加字段,删除可选字段生产者,消费者以前的版本
Forward transitive添加字段,删除可选字段生产者,消费者所有以前的版本
Full删除可选字段,添加可选字段无影响以前的版本
Full transitive删除可选字段,添加可选字段无影响所有以前的版本

但是,您可以使用模式做更多的事情。就像处理对象一样,您可以共享公共代码以减少重复并使维护更加可管理。您也可以对模式引用执行相同的操作。

模式引用

模式引用就像其字面意义一样,是指在当前模式中引用另一个模式。重用是软件工程的核心原则之一,因为能够重复使用已经构建的东西解决了两个问题。首先,通过不重写某些现有代码,可以节省时间。其次,当需要更新原始工作(这总是会发生的)时,所有使用原始工作的下游组件会自动更新。

什么时候会使用模式引用呢?假设您有一个应用程序,提供商业企业和大学的信息。为了建模企业,您有一个名为Company的模式;对于大学,您有一个名为College的模式。一个公司有高管,一个大学有教授。您希望使用嵌套的Person域对象来表示这两者。这些模式将看起来像以下清单所示。

清单 3.28 大学模式

{
  "namespace": "bbejeck.chapter_3.avro",
  "type": "record",
  "name": "CollegeAvro",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "professors", "type":
    {"type": "array", "items": {              #1
      "namespace": "bbejeck.chapter_3.avro",
      "name":"PersonAvro",                 #2
      "fields": [
        {"name": "name", "type":"string"},
        {"name": "address", "type": "string"},
        {"name": "age", "type": "int"}
      ]
    }},
      "default": []
    }
  ]
}

#1 教授数组

#2 数组中的项类型是Person对象

如您所见,大学模式中有一个嵌套的记录类型,这在模式中并不罕见。现在让我们看一下公司模式。

清单 3.29 公司模式

{
  "namespace": "bbejeck.chapter_3.avro",
  "type": "record",
  "name": "CompanyAvro",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "executives", "type":
    {"type": "array", "items": {                 #1
      "type":"record",
      "namespace": "bbejeck.chapter_3.avro",
      "name":"PersonAvro",                     #2
      "fields": [
        {"name": "name", "type":"string"},
        {"name": "address", "type": "string"},
        {"name": "age", "type": "int"}
      ]
    }},
      "default": []
    }
  ]
}

#1 高管数组

#2 项类型是PersonAvro

再次看到,这两者都有一个用于模式数组中的类型的嵌套记录。将高管或教授类型建模为Person是很自然的,因为这允许您将所有细节封装到一个对象中。但是,正如您在这里看到的,模式中存在重复。如果需要更改Person模式,则需要更新包含嵌套Person定义的每个文件。此外,随着添加更多定义,模式的大小和复杂性可能会由于所有类型的嵌套而迅速变得难以管理。

最好将定义与数组定义在同一个文件中。接下来我们来做。我们将把您在这里看到的定义放在单独的文件中。现在我们来看看您如何更新college.avsc和company.avsc模式文件。

清单 3.30 更新后的大学模式

{
  "namespace": "bbejeck.chapter_3.avro",
  "type": "record",
  "name": "CollegeAvro",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "professors", "type":
    {"type": "array", "items": "bbejeck.chapter_3.avro.PersonAvro"},    #1
      "default": []
    }
  ]
}

#1 这是新增的部分;它是对Person对象的引用

注意:在使用模式引用时,您提供的引用模式必须与正在使用的模式相同类型。例如,您不能在Protocol Buffers模式中引用Avro模式或JSON模式;引用必须是另一个Protocol Buffers模式。

现在,通过引用person.avsc模式文件中创建的对象,您已经整理好了。现在让我们来看看更新后的公司模式。

清单 3.31 更新后的公司模式

{
  "namespace": "bbejeck.chapter_3.avro",
  "type": "record",
  "name": "CompanyAvro",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "executives", "type":
      {
        "type": "array", "items": "bbejeck.chapter_3.avro.PersonAvro"},   #1
        "default": []
       }
  ]
}

#1 这是新增的部分;它是对Person对象的引用。

现在,两个模式都引用了由person模式文件创建的同一对象。为了完整起见,让我们看看如何在JSON Schema和Protocol Buffers中实现模式引用。首先我们来看JSON Schema版本。

清单 3.32 JSON Schema中的公司模式引用

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Exchange",
  "description": "A JSON schema of a Company using Person refs",
  "javaType": "bbejeck.chapter_3.json.CompanyJson",
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "executives": {
      "type": "array",
      "items": {
        "$ref": "person.json"    #1
      }
    }
  }
}

#1 对Person对象模式的引用

在JSON Schema中,引用的概念是相同的,但您提供了一个明确的$ref元素,指向被引用的模式文件。假定被引用的文件位于与引用模式相同的目录中。

现在我们来看看Protocol Buffers中的等效引用。

清单 3.33 Protocol Buffers中的公司模式引用

syntax = "proto3";

package bbejeck.chapter_3.proto;

import "person.proto";        #1

option java_outer_classname = "CompanyProto";
option java_multiple_files = true;

message Company {
  string name = 1;
  repeated Person executives = 2;       #2
}

#1 引入引用模式的声明

#2 引用Person proto

在Protocol Buffers中,您需要额外提供一个导入引用到proto文件的声明。

但现在问题是(反)序列化程序将如何知道如何将对象序列化和反序列化为正确的格式。您已将定义从文件内部移除,因此还需要获取对模式的引用。幸运的是,Schema Registry允许模式引用。

首先,您需要先为person对象注册一个模式。然后,在注册college和company模式时,您引用已经注册的person模式。

使用Gradle schema-registry插件使此过程变得简单。以下清单展示了如何配置它以使用模式引用。

清单 3.34 Gradle插件模式引用配置

register {

    subject('person','src/main/avro/person.avsc', 'AVRO')       #1
    subject('college-value','src/main/avro/college.avsc', 'AVRO')
        .addReference("bbejeck.chapter_3.avro.PersonAvro", "person", 1)   #2
    subject('company-value','src/main/avro/company.avsc', 'AVRO')
        .addReference("bbejeck.chapter_3.avro.PersonAvro", "person", 1)   #3
    }

#1 注册person模式

#2 注册college模式并添加对person模式的引用

#3 注册company模式并添加对person模式的引用

因此,您首先注册了person.avsc文件,但在这种情况下,主题仅简单地是person,因为在这种情况下,它没有直接与任何主题相关联。然后,您使用<主题名称> - value模式注册了college和company模式,因为这些模式与相同名称的主题相关联,并且使用默认的主题名称策略(TopicNameStrategy)。addReference方法接受三个参数: 引用的名称。由于您使用的是Avro,这是模式的完全限定名称。对于Protobuf,它是proto文件的名称,对于JSON Schema,它是模式中的URL。 注册模式的主题名称。 引用的版本号。

现在,引用已经就位,您注册了模式,您的生产者和消费者客户端可以正确地序列化和反序列化带有引用的对象。

源代码中有使用模式引用的示例。因为您已经运行了./gradlew streams

来设置主模块的引用。要查看模式引用的实际操作,请运行以下清单中的代码。

清单 3.35 使用模式引用的任务

./gradlew streams:runCompanyProducer
./gradlew streams:runCompanyConsumer

./gradlew streams:runCollegeProducer
./gradlew streams:runCollegeConsumer

模式引用和单个主题中的多个事件

我们已经讨论了不同的主题策略RecordNameStrategy和TopicRecordNameStrategy,以及它们如何允许为主题生产不同类型的记录。但是,使用RecordNameStrategy时,您必须为给定类型的所有主题使用相同的模式版本。如果要更改或演进模式,则所有主题都必须使用新模式。而使用TopicRecordNameStrategy允许一个主题中存在多个事件,并将模式限制在单个主题内,使您能够独立于其他主题演进模式。

但是,这两种方法都无法控制向主题中生产的不同类型的数量。如果有人想要生产不希望的其他类型的记录,您没有办法强制执行此策略。

然而,通过使用模式引用,可以实现向主题中生产多种事件类型并限制生产到主题的记录类型。在结合使用TopicNameStrategy和模式引用时,所有记录都将受单个主题的约束。换句话说,模式引用允许您拥有多种类型,但仅限于模式引用的那些类型。通过一个示例场景来理解这一点是最好的。

假设您是一家在线零售商,已经开发了一个系统,精确追踪您向客户运送的包裹。您拥有一队卡车和飞机,可以将包裹运送到全国各地。每当包裹沿途处理时,它会被扫描到您的系统中,生成三种可能的事件,分别由这些领域对象表示:PlaneEvent(飞机事件)、TruckEvent(卡车事件)或DeliveryEvent(交付事件)。

这些是不同的事件,但它们彼此之间密切相关。另外,由于这些事件的顺序很重要,您希望它们在同一个主题上生成,这样您就可以将所有相关事件聚集在一起,并按照其发生顺序进行排列。在第4章中,当我们涉及客户端时,我将更详细地介绍将相关事件合并到单个主题中有助于排序的情况。

假设您已经为PlaneEvent、TruckEvent和DeliveryEvent创建了模式,您可以像以下清单中所示这样创建包含不同事件类型的模式。

清单 3.36 具有多个事件的Avro模式all_events.avsc

[  "bbejeck.chapter_3.avro.TruckEvent",  "bbejeck.chapter_3.avro.PlaneEvent",  "bbejeck.chapter_3.avro.DeliveryEvent"]

在all_events.avsc模式文件中,这是一个Avro union,即可能事件类型的数组。当字段或者在这种情况下是模式可能有多种类型时,您使用union。由于您在单个模式中定义了所有预期的类型,因此您的主题现在可以包含多种类型,但仅限于模式中列出的那些类型。在使用这种格式的模式引用时,对于Kafka生产者配置,始终将auto.register.schemas设置为false,并使用use.latest.version设置为true至关重要。以下是使用给定设置时需要使用这些配置的原因。

当Avro序列化器尝试序列化对象时,由于它位于union模式中,不会找到模式。结果,它将注册个体对象的模式,覆盖union模式。因此,将模式自动注册设置为false可以避免这种问题。此外,通过指定use.latest.version=true,序列化器将检索模式的最新版本(即union模式)并将其用于序列化。否则,它将在主题名称中查找事件类型,由于找不到,会导致失败。

提示:在Protocol Buffers中使用oneOf字段与引用时,引用的模式会自动递归注册,因此可以将auto.register.schemas配置设置为true。JSON Schema的oneOf字段也可以采用相同的方法。

现在让我们看看如何使用引用注册模式。

清单 3.37 使用引用注册all_events模式

subject('truck_event',
      'src/main/avro/truck_event.avsc', 'AVRO')
subject('plane_event','src/main/avro/plane_event.avsc', 'AVRO')
subject('delivery_event','src/main/avro/delivery_event.avsc', 'AVRO')

subject('inventory-events-value',
          'src/main/avro/all_events.avsc','AVRO')
  .addReference("bbejeck.chapter_3.avro.TruckEvent",
                                 "truck_event", 1)
  .addReference("bbejeck.chapter_3.avro.PlaneEvent", "plane_event", 1)
  .addReference("bbejeck.chapter_3.avro.DeliveryEvent", "delivery_event", 1)

清单 3.37 中的注释:

  • #1 注册在all_events.avsc文件中引用的个体模式。
  • #2 注册all_events模式。
  • #3 添加个体模式的引用。

正如您在第3.5节中看到的Avro使用中,您需要在带有引用的模式之前注册个体模式。然后,您可以注册带有引用的主模式。

在使用Protobuf时,不存在union类型,而是oneOf类型,本质上是相同的。然而,对于Protobuf,您不能在顶层使用oneOf;它必须存在于Protobuf消息中。例如,对于Protobuf示例,假设您想要跟踪客户互动、登录、搜索和购买作为独立事件。但由于它们之间关系密切,因此排序非常重要,因此希望它们出现在同一个主题中。以下清单显示了包含引用的Protobuf文件。

清单 3.38 使用引用的Protobuf文件

syntax = "proto3";

package bbejeck.chapter_3.proto;

import "purchase_event.proto";        #1
import "login_event.proto";
import "search_event.proto";

option java_multiple_files = true;
option java_outer_classname = "EventsProto";

message Events {

  oneof type {                          #2
    PurchaseEvent purchase_event = 1;
    LoginEvent login_event = 2;
    SearchEvent search_event = 3;
  }
  string key = 4;
}

清单 3.38 中的注释:

  • #1 导入个体Protobuf消息。
  • #2 oneOf字段,可以是列出的三种类型之一。

您之前在本章中已经看到了Protobuf模式,所以我这里不再逐一审查所有部分。但这个示例的关键在于oneOf字段类型,可以是PurchaseEvent、LoginEvent或SearchEvent。当您注册Protobuf模式时,已经有足够的信息来递归注册所有引用的模式,因此可以将auto.register配置设置为true。您可以类似地构建Avro引用。

清单 3.39 带有外部类引用的Avro模式

{
  "type": "record",
  "namespace": "bbejeck.chapter_3.avro",
  "name": "TransportationEvent",

  "fields" : [
    {"name": "event", "type"[            
      "bbejeck.chapter_3.avro.TruckEvent",        
      "bbejeck.chapter_3.avro.PlaneEvent",
      "bbejeck.chapter_3.avro.DeliveryEvent"
    ]}
  ]
}

清单 3.39 中的注释:

  • #1 外部类名。
  • #2 名为"event"的字段。
  • #3 字段类型的Avro union。

因此,这个Avro模式与之前带有引用的Avro模式的主要区别在于它有一个外部类,并且引用现在是类中的字段。此外,当您像这里所做的那样提供一个外部类与Avro引用时,您现在可以将auto.register配置设置为true,尽管您仍然需要提前注册引用对象的模式,因为与Protobuf不同,Avro没有足够的信息来递归注册引用对象。

关于生产者和消费者使用多个类型的一些额外考虑是,我指的是您在Java客户端中使用的泛型以及如何根据其具体类名确定对象的适当操作。这些主题更适合我们在下一章涵盖客户端时讨论,因此我们将在接下来的章节中涵盖这些主题。

现在,您已经了解了不同的模式兼容性策略、如何使用模式以及如何使用引用。在您运行的所有示例中,您都使用了Schema Registry提供的内置序列化器和反序列化器。接下来的部分将涵盖与生产者和消费者(反)序列化器相关的配置。但我们将只讨论(反)序列化器相关的配置,而不是一般的生产者和消费者配置,这些将在下一章中讨论。

模式注册中心的(反)序列化器

在本章的开头,我提到在Kafka中生产记录时,您需要对记录进行序列化,以便在网络上传输和存储到Kafka中。相反,当消费记录时,您需要对其进行反序列化,以便能够操作对象。

您必须配置生产者和消费者,使用所需的类来进行序列化和反序列化过程。模式注册中心为所有三种支持的类型(Avro、Protobuf、JSON)提供了序列化器、反序列化器和Serde(在Kafka Streams中使用)。

提供这些序列化工具是使用模式注册中心的一个强有力的论点,我在本章前面已经讨论过。解放开发人员不必编写序列化代码加快了开发速度,并增加了组织内的标准化。此外,使用一组标准的序列化工具可以减少错误,并降低一个团队实现自定义序列化框架的机会。

注释:什么是Serde?Serde类包含给定类型的序列化器和反序列化器。在使用Kafka Streams时,您将使用Serdes,因为无法访问嵌入的生产者和消费者。因此,提供一个包含正确序列化器和反序列化器的类是有意义的。在第6章中,当我们开始使用Kafka Streams时,您将看到Serdes的实际应用。

在接下来的几节中,我将讨论使用模式注册中心感知的序列化器和反序列化器的配置。重要的一点是,您不直接配置序列化器。您在配置KafkaProducer或KafkaConsumer时设置序列化器配置。如果以下几节内容不是很清楚,那没关系,因为我们将在下一章讨论客户端(生产者和消费者)。

Avro 序列化器和反序列化器

KafkaAvroSerializer 和 KafkaAvroDeserializer 类用于序列化和反序列化 Avro 记录。在配置消费者时,您需要包含额外的属性 KafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG=true,这指示反序列化器创建一个 SpecificRecord 实例。否则,反序列化器将返回一个 GenericRecord。

以下是如何将这些属性添加到生产者和消费者配置中的示例。请注意,以下示例仅展示了序列化所需的配置,为了清晰起见,我省略了其他配置。我们将在第四章详细讨论生产者和消费者的配置。

// 生产者属性
producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
  StringSerializer.class);                                     //1
producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
  KafkaAvroSerializer.class);                                  //2
producerProps.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG,
  "http://localhost:8081");                                    //3

// 消费者属性单独设置
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
  StringDeserializer.class);                                    //4
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
  KafkaAvroDeserializer.class);                                 //5
props.put(KafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG,
  true);                                                         //6
props.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG,
  "http://localhost:8081");                                      //7
  • #1 Key 的序列化器
  • #2 Value 的序列化器
  • #3 设置序列化器的 Schema Registry URL
  • #4 Key 的反序列化器
  • #5 Value 的反序列化器
  • #6 表示要构造一个 SpecificRecord 实例
  • #7 设置反序列化器的 Schema Registry URL

接下来,我们将看一下如何配置处理 Protobuf 记录。

Protobuf

要处理 Protobuf 记录,您可以使用 KafkaProtobufSerializer 和 KafkaProtobufDeserializer 类。当使用带有模式注册表的 Protobuf 时,建议在 Protobuf 模式中将 java_outer_classname 和 java_multiple_files 设置为 true。如果您使用 RecordNameStrategy 来处理 Protobuf,则必须设置这些属性,以便反序列化器在从序列化字节创建实例时可以确定类型。

在本章前面,我们讨论了使用模式注册表感知的序列化器时,这些序列化器将尝试注册新模式。如果您的 Protobuf 模式通过导入引用其他模式,那么这些引用的模式也会被注册。只有 Protobuf 提供了这种能力;Avro 和 JSON 不会自动注册引用的模式。同样,如果您不希望自动注册模式,请使用以下配置来禁用它:auto.schema.registration = false。

接下来,让我们看一个类似的示例,展示了处理 Protobuf 记录所需的相关模式注册表配置。

// 生产者属性
producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
  StringSerializer.class);                                       //1
producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
  KafkaProtobufSerializer.class);                                 //2
producerProps.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG,
  "http://localhost:8081");                                   //3

// 消费者属性再次在消费者上单独设置
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
  StringDeserializer.class);                               //4
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
  KafkaProtobufDeserializer.class);                            //5
props.put(KafkaProtobufDeserializerConfig.SPECIFIC_PROTOBUF_VALUE_TYPE,
  AvengerSimpleProtos.AvengerSimple.class);                              //6
props.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG,
  "http://localhost:8081");      //7
  • #1 Key 的序列化器
  • #2 Protobuf 值的序列化器
  • #3 为消费者提供模式注册表的 URL
  • #4 Key 的反序列化器
  • #5 Protobuf 值的反序列化器
  • #6 反序列化器应实例化的特定类
  • #7 生产者的模式注册表位置

与 Avro 反序列化器一样,您必须指示它创建一个特定的实例。但在这种情况下,您配置的是实际的类名,而不是设置一个布尔标志来指示您需要一个特定的类。如果省略特定值类型的配置,反序列化器将返回 DynamicRecord 类型。我们在 C.2.5 节中讨论了 DynamicRecord。

书中源代码中的 bbejeck.chapter_3.ProtobufProduceConsumeExample 类演示了如何生产和消费 Protobuf 记录。现在,我们将继续进行 Schema Registry 支持的类型配置的最后一个示例,即 JSON 模式。

JSON Schema

模式注册表提供了 KafkaJsonSchemaSerializer 和 KafkaJsonSchemaDeserializer,用于处理 JSON 模式对象。配置应该对于 Avro 和 Protobuf 的配置来说感觉很熟悉。

注意:模式注册表还提供了 KafkaJsonSerializer 和 KafkaJsonDeserializer 类。虽然名称非常相似,但这些(反)序列化器是用于处理 Java 对象与 JSON 之间的转换,而不使用 JSON Schema。虽然名称相近,确保您使用带有 Schema 的名称的序列化器和反序列化器。我们将在下一节讨论通用的 JSON 序列化器。

Listing 3.42 JSON Schema 所需的配置

// 生产者配置
producerProps.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG,
  "http://localhost:8081");      //1
producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
  StringSerializer.class);                                       //2
producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
  KafkaJsonSchemaSerializer.class);                        //3

// 消费者配置
props.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG,
  "http://localhost:8081");                               //4
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
  StringDeserializer.class);       //5
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
  KafkaJsonSchemaDeserializer.class);             //6
props.put(KafkaJsonDeserializerConfig.JSON_VALUE_TYPE,
  SimpleAvengerJson.class);     //7
  • #1 为生产者提供模式注册表的 URL
  • #2 Key 的序列化器
  • #3 JSON Schema 值的序列化器
  • #4 为消费者提供模式注册表的 URL
  • #5 Key 的反序列化器
  • #6 指定 JSON Schema 值的反序列化器
  • #7 配置此反序列化器将创建的具体类

在这里,您可以看到与 Protobuf 配置的相似性:您需要在示例的最后一行中指定反序列化器应从序列化形式构造的类。如果省略值类型,反序列化器将返回一个 Map,这是 JSON 模式反序列化的通用形式。对于键也是如此。如果您的键是 JSON 模式对象,您需要为反序列化器提供 KafkaJsonDeserializerConfig.JSON_KEY_TYPE 配置,以便创建确切的类。

在书籍源代码中的 bbejeck.chapter_3.JsonSchemaProduceConsumeExample 中有一个处理 JSON 模式对象的简单生产者和消费者示例。与其他基本的生产者和消费者示例一样,这些示例展示了如何处理特定和通用返回类型。在 C.3.5 节中讨论了 JSON 模式通用类型的结构。

现在,我们已经介绍了模式注册表支持的每种序列化类型的不同序列化器和反序列化器。虽然推荐使用模式注册表,但并非强制要求。接下来的章节将概述如何在不使用模式注册表的情况下序列化和反序列化您的 Java 对象。

Serialization without Schema Registry

在本章开头,我提到你的事件对象,特别是它们的模式表示,是 Kafka 事件流平台的生产者和消费者之间的契约。模式注册表提供了这些模式的中央存储库,跨组织强制执行这些模式契约。此外,模式注册表还提供了序列化器和反序列化器,为您提供了一种方便的方法来处理数据,而无需编写自定义的序列化代码。

这是否意味着使用模式注册表是必需的?完全不是。有时候,您可能无法访问模式注册表或不希望使用它。编写自定义的序列化器和反序列化器很容易。请记住,生产者和消费者与序列化器和反序列化器的实现是解耦的;您只需要将类名作为配置设置即可。但是,当您使用模式注册表时,您可以在 Kafka Streams、Connect 和 ksqlDB 中使用相同的模式。

因此,要创建您的序列化器和反序列化器,您需要创建实现 org.apache.kafka.common.serialization.Serializer 和 org.apache.kafka.common.serialization.Deserializer 接口的类。对于 Serializer 接口,您只需实现一个方法:serialize。对于 Deserializer 接口,您只需实现 deserialize 方法。如果需要,这两个接口都有额外的默认方法(configure、close)可以进行覆盖。以下代码展示了使用 jackson-databind 的自定义序列化器的部分示例(为了清晰起见,某些细节被省略)。

Listing 3.43 自定义序列化器的 serialize 方法

@Override
public byte[] serialize(String topic, T data) {
    if (data == null) {
        return null;
    }
    try {
        return objectMapper.writeValueAsBytes(data);    //1
    } catch (JsonProcessingException e) {
        throw new SerializationException(e);
    }
}

#1 将给定的对象转换为字节数组的序列化表示形式

在这里,您调用 objectMapper.writeValueAsBytes(),它会返回传入对象的序列化表示形式的字节数组。现在让我们看一下反序列化器的例子(为了清晰起见,某些细节被省略)。

Listing 3.44 自定义反序列化器的 deserialize 方法

@Override
public T deserialize(String topic, byte[] data) {
    try {
        return objectMapper.readValue(data, objectClass);   //1
    } catch (IOException e) {
        throw new SerializationException(e);
    }
}

#1 将字节数组转换回由 objectClass 参数指定的对象

bbejeck.serializers 包中包含了这里展示的序列化器和反序列化器以及额外的 Protobuf 序列化器。您可以在本书的任何示例中使用这些序列化器/反序列化器,但请记住它们不使用模式注册表。或者,它们可以作为实现自己的序列化器和反序列化器的示例。

在本章中,我们讨论了事件对象,特别是它们的模式如何代表生产者和消费者之间的契约。我们讨论了模式注册表如何存储这些模式并在 Kafka 平台上强制执行隐含的契约。最后,我们覆盖了支持的 Avro、Protobuf 和 JSON 的序列化格式。在下一章中,您将进一步了解 Kafka 事件流平台,学习 KafkaProducer 和 KafkaConsumer。如果您将 Kafka 想象为数据的中枢神经系统,那么客户端就是其感知的输入和输出。