1. 引言

本文将探讨几种从现有Java类生成Avro Schema的方法。虽然这不是标准工作流,但在某些场景下确实需要,而使用现有库可以最简单地实现这个需求。

2. Avro是什么?

在深入讨论如何从类反向生成Schema之前,我们先快速回顾Avro的基本概念。

根据官方文档,Avro是一个数据序列化系统,能够按照预定义Schema进行数据的序列化和反序列化。Schema是整个系统的核心,以JSON格式表达。 更详细的Avro介绍可参考官方指南

3. 为什么需要从Java类生成Avro Schema?

使用Avro的标准工作流是先定义Schema,再生成目标语言的类。但反向操作——从项目中的类生成Avro Schema——同样可行且实用。

想象这样一个场景:我们在处理遗留系统时,需要通过消息代理发送数据,并决定使用Avro作为序列化方案。通过代码分析发现,直接使用现有类表达数据能快速满足新规范。

手动将Java代码翻译为Avro JSON Schema既繁琐又容易出错。此时利用现有库自动生成可以省时省力

4. 使用Avro反射API生成Schema

第一种方法是使用Avro的反射API(Reflection API)快速转换Java类。 需要先确保项目依赖Avro库

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

4.1. 简单记录类

以一个简单的Java记录类为例:

record SimpleBankAccount(String bankAccountNumber) {
}

通过ReflectData的单例实例即可为任意Java类生成org.apache.avro.Schema对象,再调用其toString()方法获取JSON格式的Schema。

使用JsonUnit验证生成的Schema:

@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记录类,但普通POJO同样适用。

4.2. 可空字段

添加一个可空的String字段,使用@org.apache.avro.reflect.Nullable注解标记:

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

生成Schema时,可空字段会被正确处理:

@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注解使Schema中的字段变为["null", "string"]联合类型。

4.3. 忽略字段

若需排除敏感字段,直接使用@AvroIgnore注解

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

生成的Schema将与4.1节的示例完全一致。

4.4. 覆盖字段名

默认字段名直接取自Java字段名,但可通过@AvroName覆盖:

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

生成的Schema将使用bankAccountReference而非reference

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

4.5. 多实现字段

当字段类型为接口或抽象类时,需用@Union注解指定所有可能实现:

interface AccountReference {
    String reference();
}

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

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

生成的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支持逻辑类型——Schema层是基本类型,但包含额外提示指导代码生成器使用特定类表示字段

例如处理时间字段或UUID时:

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

需配置ReflectData实例,添加必要的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();
  
    // 验证Schema...
}

生成的Schema将包含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生成Schema

虽然Avro反射API功能强大,但了解替代方案也很有价值。这里我们介绍Jackson的Binary Dataformats库,特别是其Avro子模块

先添加依赖:

<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. 基础转换

通过AvroMapperAvroSchemaGenerator实现转换:

@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" ]
          } ]
        }
        """);
}

✅ 优势:使用熟悉的Jackson API,相比原生Avro API更易上手。

5.2. Jackson注解

对比4.1节的Schema,会发现Jackson生成的Schema中bankAccountNumber被标记为可空。这是因为:

  • Jackson依赖getter方法而非直接反射
  • 默认行为假设所有字段可空

若需字段必填,必须使用@JsonProperty(required = true)

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

⚠️ 所有Jackson注解都会被保留,转换结果需仔细验证。

5.3. 支持逻辑类型

Jackson默认不处理逻辑类型,需显式启用:

@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();

    // 验证Schema...
}

这样生成的Schema就能正确处理时间等逻辑类型。

6. 总结

本文展示了从Java类生成Avro Schema的两种主要方案:使用Avro反射API或Jackson的Avro模块。

虽然Avro原生API知名度较低,但其行为更可预测。而Jackson方案虽然更易用,但在集成到生产项目时可能因默认行为(如字段可空)导致踩坑。

代码示例未涵盖所有功能,完整实现可参考GitHub仓库或查阅官方文档。


原始标题:Generate Avro Schema From Certain Java Class | Baeldung