1. 引言
本文将探讨在 Java 中使用 Apache Avro 时处理和写入 null 值的两种方法。通过这些方法,我们也将讨论处理可空字段的最佳实践。
2. Avro 中 Null 值的问题
Apache Avro 是一个数据序列化框架,提供丰富的数据结构和紧凑、快速的二进制数据格式。然而,在 Avro 中使用 null 值需要特别注意。
来看一个常见的问题场景:
GenericRecord record = new GenericData.Record(schema);
record.put("email", null);
// 写入文件时可能抛出 NullPointerException
*默认情况下,Avro 字段不可为空。尝试存储 null 值会在序列化时导致 NullPointerException。*
在开始第一个解决方案前,先添加正确的 依赖:
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.12.0</version>
</dependency>
3. 处理 Null 值的解决方案
本节将探讨处理 Avro 中 null 值的两种主要方法:Schema 定义和基于注解的方式。
3.1. 定义 Schema 的三种方式
我们可以通过三种方式定义支持 null 值的 Avro Schema。首先看 JSON 字符串方式:
private static final String SCHEMA_JSON = """
{
"type": "record",
"name": "User",
"namespace": "com.baeldung.apache.avro.storingnullvaluesinavrofile",
"fields": [
{"name": "id", "type": "long"},
{"name": "name", "type": "string"},
{"name": "active", "type": "boolean"},
{"name": "lastUpdatedBy", "type": ["null", "string"], "default": null},
{"name": "email", "type": "string"}
]
}""";
public static Schema createSchemaFromJson() {
return new Schema.Parser().parse(SCHEMA_JSON);
}
这里使用 union 类型语法 ["null", "string"]
定义可空字段。
接下来使用 SchemaBuilder 的编程式方式定义 Schema:
public static Schema createSchemaWithOptionalFields1() {
return SchemaBuilder
.record("User")
.namespace("com.baeldung.apache.avro.storingnullvaluesinavrofile")
.fields()
.requiredLong("id")
.requiredString("name")
.requiredBoolean("active")
.name("lastUpdatedBy")
.type() // 配置开始
.unionOf()
.nullType()
.and()
.stringType()
.endUnion()
.nullDefault() // 配置结束
.requiredString("email")
.endRecord();
}
此例中,我们使用 SchemaBuilder 创建 Schema,其中 lastUpdatedBy 字段可以是 null 或 string 值。
最后创建类似但方式不同的 Schema:
public static Schema createSchemaWithOptionalFields2() {
return SchemaBuilder
.record("User")
.namespace("com.baeldung.apache.avro.storingnullvaluesinavrofile")
.fields()
.requiredLong("id")
.requiredString("name")
.requiredBoolean("active")
.requiredString("lastUpdatedBy")
.optionalString("email") // 使用可选字段
.endRecord();
}
这里用 optionalString()
替代了冗长的 type().unionOf().nullType().andStringType().endUnion().nullDefault()
链式调用。
快速对比后两种方式:
- ✅ 长版本:提供更精细的 null 值控制
- ✅ 短版本:SchemaBuilder 提供的语法糖
- ⚠️ 本质上两者功能相同
3.2. 使用 @Nullable 注解
下一种方式使用 Avro 内置的 @Nullable 注解:
public class AvroUser {
private long id;
private String name;
@Nullable
private Boolean active;
private String lastUpdatedBy;
private String email;
// 其他代码
}
此注解告诉 Avro 的反射式代码生成机制:该字段可接受 null 值。
4. 写入文件的实现
现在看如何序列化包含 null 值的 Record:
public static void writeToAvroFile(Schema schema, GenericRecord record, String filePath) throws IOException {
DatumWriter<GenericRecord> datumWriter = new GenericDatumWriter<>(schema);
try (DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<>(datumWriter)) {
dataFileWriter.create(schema, new File(filePath));
dataFileWriter.append(record);
}
}
关键步骤解析:
- 初始化 GenericDatumWriter 处理 GenericRecord 对象
- 将 Schema 作为构造参数,告知序列化规则
- 初始化 DataFileWriter 处理实际数据写入和元数据管理
- 通过
create()
方法创建带指定 Schema 的 Avro 文件 - *最后写入 record。若字段标记为 @Nullable 或 union 类型,其 null 值将被正确序列化*
5. 测试解决方案
现在验证实现是否正确:
@Test
void whenSerializingUserWithNullPropFromStringSchema_thenSuccess(@TempDir Path tempDir) {
user.setLastUpdatedBy(null);
schema = AvroUser.createSchemaWithOptionalFields1();
String filePath = tempDir.resolve("test.avro").toString();
GenericRecord record = AvroUser.createRecord(AvroUser.createSchemaFromJson(), user);
assertDoesNotThrow(() -> AvroUser.writeToAvroFile(schema, record, filePath));
File avroFile = new File(filePath);
assertTrue(avroFile.exists());
assertTrue(avroFile.length() > 0);
}
测试中先将 lastUpdatedBy 设为 null,然后用字符串 Schema 创建 Schema 对象。测试表明记录可成功序列化 null 值。
第二个测试使用长配置的 SchemaBuilder:
@Test
void givenSchemaBuilderWithOptionalFields1_whenCreatingSchema_thenSupportsNull(@TempDir Path tempDir) {
user.setLastUpdatedBy(null);
String filePath = tempDir.resolve("test.avro").toString();
schema = AvroUser.createSchemaWithOptionalFields1();
GenericRecord record = AvroUser.createRecord(schema, user);
assertTrue(schema.getField("lastUpdatedBy").schema().isNullable(),
"Union type field should be nullable");
assertDoesNotThrow(() -> AvroUser.writeToAvroFile(schema, record, filePath));
File avroFile = new File(filePath);
assertTrue(avroFile.exists());
assertTrue(avroFile.length() > 0);
}
最后测试短配置的 SchemaBuilder:
@Test
void givenSchemaBuilderWithOptionalFields2_whenCreatingSchema_thenSupportsNull(@TempDir Path tempDir) {
user.setEmail(null);
String filePath = tempDir.resolve("test.avro").toString();
schema = AvroUser.createSchemaWithOptionalFields2();
GenericRecord record = AvroUser.createRecord(schema, user);
assertTrue(schema.getField("email").schema().isNullable(),
"Union type field should be nullable");
assertDoesNotThrow(() -> AvroUser.writeToAvroFile(schema, record, filePath));
File avroFile = new File(filePath);
assertTrue(avroFile.exists());
assertTrue(avroFile.length() > 0);
}
6. 结论
本文探讨了处理 Apache Avro 中 null 值的两种主要方法:
- 通过三种方式定义 Schema(JSON 字符串、SchemaBuilder 长配置、SchemaBuilder 短配置)
- 直接在类属性上使用 @Nullable 注解
两种方法都有效,但 Schema 方式提供更细粒度的控制,通常是生产环境的首选。