知識庫 / JSON / Jackson RSS 訂閱

從指定Java類生成Avro Schema

Data,Jackson
HongKong
6
09:41 PM · Dec 05 ,2025

1. 引言

在本教程中,我們將討論從現有 Java 類生成 Avro 模式的不同選項。雖然這不是標準工作流程,但這種轉換方向也可能發生,並且瞭解其基本原理,藉助現有的庫可以以最簡單的方式實現。

2. Avro 是什麼?

在深入探討如何將現有類重新轉換為模式之前,我們先回顧一下 Avro 的概念。

根據文檔,Avro 是一種數據序列化系統,能夠按照預定義的模式進行數據的序列化和反序列化。 這種模式是該系統的核心。 模式本身使用 JSON 格式表達。 更多關於 Avro 的信息,請參考已發佈的指南。

3. 從現有 Java 類生成 Avro 模式的動機

使用 Avro 的標準工作流程包括定義模式,然後生成所選語言中的類。 儘管這是最流行的做法,但也可以反向生成從項目中的類中生成的 Avro 模式。

設想一個場景:我們正在與遺留系統一起工作,並希望通過消息代理髮出數據,我們決定使用 Avro 作為 (解)序列化解決方案。 在瀏覽代碼時,我們可以通過從現有類中發出數據來快速符合新的規則。

手動將 Java 代碼翻譯為 Avro JSON 模式將非常繁瑣。 相反,我們可以使用可用的庫來自動執行此操作,從而節省時間。

4. 使用 Avro 反射 API 生成 Avro 模式

首選的方法是快速將現有的 Java 類轉換為 Avro 模式,即使用 Avro 反射 API。 使用此 API,我們需要確保我們的項目依賴於 Avro 庫

<dependency>
    <groupId>org.apache.avro</groupId>
    <artifactId>avro</artifactId>
    <version>1.12.0</version>
</dependency>

4.1. 簡單記錄

假設我們要使用 ReflectData API 來創建一個簡單的 Java 記錄:

record SimpleBankAccount(String bankAccountNumber) {
}

我們可以使用 ReflectData 的單例實例來為任何給定的 Java 類生成一個 org.apache.avro.Schema 對象。然後,我們可以調用 Schema 實例的 toString() 方法來獲取 Avro 模式作為 JSON String

為了驗證生成的字符串與我們的預期是否匹配,我們可以使用 JsonUnit:

@Test
void whenConvertingSimpleRecord_thenAvroSchemaIsCorrect() {
    Schema schema = ReflectData.get()
      .getSchema(SimpleBankAccount.class);
    String jsonSchema = schema.toString();

    assertThatJson(jsonSchema).isEqualTo("""
        {
          "type" : "record",
          "name" : "SimpleBankAccount",
          "namespace" : "com.baeldung.apache.avro.model",
          "fields" : [ {
            "name" : "bankAccountNumber",
            "type" : "string"
          } ]
        }
        """);
}

儘管我們為了簡化使用了 Java 記錄,但它同樣可以與普通的 Java 對象一起使用。

4.2. 可為空字段

讓我們為我們的 Java 記錄添加另一個 String 字段。我們可以使用 @org.apache.avro.reflect.Nullable 註解將其標記為可選的:

record BankAccountWithNullableField(
    String bankAccountNumber, 
    @Nullable String reference
) {
}

如果我們重複測試,我們可以預期 參考 的空值性將被反映。

@Test
void whenConvertingRecordWithNullableField_thenAvroSchemaIsCorrect() {
    Schema schema = ReflectData.get()
        .getSchema(BankAccountWithNullableField.class);
    String jsonSchema = schema.toString(true);

    assertThatJson(jsonSchema).isEqualTo("""
        {
          "type" : "record",
          "name" : "BankAccountWithNullableField",
          "namespace" : "com.baeldung.apache.avro.model",
          "fields" : [ {
            "name" : "bankAccountNumber",
            "type" : "string"
          }, {
            "name" : "reference",
            "type" : [ "null", "string" ],
            "default" : null
          } ]
        }
        """);
}

如我們所見,在將 @Nullable 註解應用於新字段後,生成的模式聯合體中 引用字段 變為 null。

4.3. 忽略字段

Avro 庫還允許我們在生成模式時忽略某些字段。例如,我們不想通過網絡傳輸敏感信息。為此,只需在特定字段上使用 @AvroIgnore 註解即可:

record BankAccountWithIgnoredField(
    String bankAccountNumber, 
    @AvroIgnore String reference
) {
}

因此,生成的模式將與我們第一個示例中的模式相匹配。

4.4. 覆蓋字段名稱

默認情況下,生成的模式中的字段名稱直接來自 Java 字段名稱。儘管這種行為是默認設置,但可以進行調整:

record BankAccountWithOverriddenField(
    String bankAccountNumber, 
    @AvroName("bankAccountReference") String reference
) {
}

本記錄的版本生成的模式使用bankAccountReference而不是reference

{
  "type" : "record",
  "name" : "BankAccountWithOverriddenField",
  "namespace" : "com.baeldung.apache.avro.model",
  "fields" : [ {
    "name" : "bankAccountNumber",
    "type" : "string"
  }, {
    "name" : "bankAccountReference",
    "type" : "string"
  } ]
}

4.5. 具有多種實現的字段

有時,我們的類可能包含一個字段,其類型是子類型。

假設 AccountReference 是一個具有兩個實現——Java 記錄以簡潔起見:的接口。

interface AccountReference {
    String reference();
}

record PersonalBankAccountReference(
    String reference, 
    String holderName
) implements AccountReference {
}

record BusinessBankAccountReference(
    String reference, 
    String businessEntityId
) implements AccountReference {
}

在我們的 BankAccountWithAbstractField 中,我們使用 @org.apache.avro.reflect.Union 註解來指示 AccountReference 字段所支持的實現:

record BankAccountWithAbstractField(
    String bankAccountNumber,
    @Union({ PersonalBankAccountReference.class, BusinessBankAccountReference.class }) 
    AccountReference reference
) { 
}

因此,生成的Avro schema將包含一個聯合類型,允許分配這兩種類中的任何一種,而不是僅限於一種:

{
  "type" : "record",
  "name" : "BankAccountWithAbstractField",
  "namespace" : "com.baeldung.apache.avro.model",
  "fields" : [ {
    "name" : "bankAccountNumber",
    "type" : "string"
  }, {
    "name" : "reference",
    "type" : [ {
      "type" : "record",
      "name" : "PersonalBankAccountReference",
      "namespace" : "com.baeldung.apache.avro.model.BankAccountWithAbstractField",
      "fields" : [ {
        "name" : "holderName",
        "type" : "string"
      }, {
        "name" : "reference",
        "type" : "string"
      } ]
    }, {
      "type" : "record",
      "name" : "BusinessBankAccountReference",
      "namespace" : "com.baeldung.apache.avro.model.BankAccountWithAbstractField",
      "fields" : [ {
        "name" : "businessEntityId",
        "type" : "string"
      }, {
        "name" : "reference",
        "type" : "string"
      } ]
    } ]
  } ]
}

4.6. 邏輯類型

Avro 支持邏輯類型。 這些在模式級別上是基本類型,但包含額外的提示,告知代碼生成器應使用哪個類來表示特定字段。

例如,如果我們的模型使用時間字段或 UUID,我們可以利用邏輯類型功能:

record BankAccountWithLogicalTypes(
    String bankAccountNumber, 
    UUID reference, 
    LocalDateTime expiryDate
) {
}

此外,我們還將配置我們的 ReflectData 實例,添加我們需要的 Conversion 對象。我們可以創建自定義的 Conversion 對象,也可以使用內置的那些對象:

@Test
void whenConvertingRecordWithLogicalTypes_thenAvroSchemaIsCorrect() {
    ReflectData reflectData = ReflectData.get();
    reflectData.addLogicalTypeConversion(new Conversions.UUIDConversion());
    reflectData.addLogicalTypeConversion(new TimeConversions.LocalTimestampMillisConversion());

    String jsonSchema = reflectData.getSchema(BankAccountWithLogicalTypes.class).toString();
  
    // verify schema
}

Consequently, when we generate and validate the schema, we’ll notice that the new fields will include a logicalType 字段:

{
  "type" : "record",
  "name" : "BankAccountWithLogicalTypes",
  "namespace" : "com.baeldung.apache.avro.model",
  "fields" : [ {
    "name" : "bankAccountNumber",
    "type" : "string"
  }, {
    "name" : "expiryDate",
    "type" : {
      "type" : "long",
      "logicalType" : "local-timestamp-millis"
    }
  }, {
    "name" : "reference",
    "type" : {
      "type" : "string",
      "logicalType" : "uuid"
    }
  } ]
}

5. 使用 Jackson 生成 Avro Schema

雖然 Avro 反射 API 既有用且能夠解決各種,甚至複雜的需求,但瞭解替代方案始終是有價值的。

在我們的案例中,我們剛剛實驗的庫的替代方案是 Jackson Dataformats Binary 庫,特別是其 Avro 相關子模塊

首先,讓我們將 jackson-corejackson-dataformat-avro 依賴項添加到我們的 pom.xml 中:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.17.2</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-avro</artifactId>
    <version>2.17.2</version>
</dependency>

5.1. 簡單轉換

讓我們通過編寫一個簡單的轉換器來探索 Jackson 能提供什麼。該實現具有使用知名 Java API 的優勢。事實上,Jackson 是最廣泛使用的庫之一,而直接使用 Avro API 則相對冷門。

我們將創建 <em >AvroMapper</em><em >AvroSchemaGenerator</em> 實例,並使用它們來檢索一個 <em >org.apache.avro.Schema</em> 實例。

然後,我們只需調用 <em >toString()</em> 方法,就像在之前的示例中一樣:

@Test
void whenConvertingRecord_thenAvroSchemaIsCorrect() throws JsonMappingException {
    AvroMapper avroMapper = new AvroMapper();
    AvroSchemaGenerator avroSchemaGenerator = new AvroSchemaGenerator();

    avroMapper.acceptJsonFormatVisitor(SimpleBankAccount.class, avroSchemaGenerator);
    Schema schema = avroSchemaGenerator.getGeneratedSchema().getAvroSchema();
    String jsonSchema = schema.toString();

    assertThatJson(jsonSchema).isEqualTo("""
        {
          "type" : "record",
          "name" : "SimpleBankAccount",
          "namespace" : "com.baeldung.apache.avro.model",
          "fields" : [ {
            "name" : "bankAccountNumber",
            "type" : [ "null", "string" ]
          } ]
        }
        """);
}

5.2. Jackson 註解

如果我們比較為 SimpleBankAccount 生成的兩個模式,我們會注意到一個關鍵的區別:使用 Jackson 生成的模式將 bankAccountNumber 字段標記為可為空。這是因為 Jackson 的工作方式與 Avro Reflect 不同。

Jackson 不依賴於反射,並且為了能夠識別需要移動到模式中的字段,它需要類具有訪問器。 此外,還請記住,默認行為假設字段是可為空。 如果我們不想在模式中使字段不可為空,則需要使用 @JsonProperty(required = true) 註解標記該字段。

讓我們創建一個不同的類變體並利用此註解:

record JacksonBankAccountWithRequiredField(
    @JsonProperty(required = true) String bankAccountNumber
) {
}

由於所有應用於原始 Java 類的 Jackson 註解仍然生效,因此我們需要仔細檢查轉換的結果。

5.3. 邏輯類型感知轉換器

Jackson,與 Avro Reflection 類似,默認情況下不考慮邏輯類型。 因此,我們需要顯式地啓用此功能。 我們通過對 AvroMapper AvroSchemaGenerator 對象進行小幅調整來實現這一點:

@Test
void whenConvertingRecordWithRequiredField_thenAvroSchemaIsCorrect() throws JsonMappingException {
    AvroMapper avroMapper = AvroMapper.builder()
        .addModule(new AvroJavaTimeModule())
        .build();

    AvroSchemaGenerator avroSchemaGenerator = new AvroSchemaGenerator()
        .enableLogicalTypes();

    avroMapper.acceptJsonFormatVisitor(BankAccountWithLogicalTypes.class, avroSchemaGenerator);
    Schema schema = avroSchemaGenerator.getGeneratedSchema()
        .getAvroSchema();
    String jsonSchema = schema.toString();

    // verify schema
}

通過這些修改,我們就能觀察到生成的Avro模式中Temporal對象所使用的邏輯類型特性。

6. 結論

在本文中,我們展示瞭如何從現有的 Java 類生成 Avro 模式的不同方法。可以使用標準的 Avro 反射 API,以及帶有二進制 Avro 模塊的 Jackson。

儘管 Avro 的方法和 API 對更廣泛的受眾來説可能不太為人所知,但與在主要項目中集成 Jackson 相比,它似乎是一個更可預測的解決方案,並且可能更容易導致錯誤。

本文中的示例並非對 Avro 或 Jackson 提供的可能性進行全面的展示。請查閲 GitHub 上的代碼以查看不太常用的功能示例,或查閲這兩個庫的官方文檔。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.