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. 基础转换
通过AvroMapper
和AvroSchemaGenerator
实现转换:
@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仓库或查阅官方文档。