從特定 Java 類別生成 Avro Schema
1. 簡介
在本教程中,我們將討論從現有 Java 類別產生 Avro 模式的不同選項。雖然不是標準工作流程,但這種轉變方向也可能發生,並且最好以最簡單的方式了解現有的庫。
2.什麼是 Avro?
在我們深入研究將現有類別轉換回模式的細節之前,讓我們先回顧一下什麼是 Avro。
根據文檔,它是一個資料序列化系統,能夠按照預先定義的模式對資料進行序列化和反序列化,這是該系統的核心。模式本身以 JSON 格式表示。有關 Avro 的更多資訊可以在已發布的指南中找到。
3.從現有 Java 類別生成 Avro Schema 的動機
使用 Avro 時的標準工作流程包括定義模式,然後用所選語言產生類別。儘管這是最受歡迎的方式,但也可以回過頭來從專案中現有的類別產生 Avro 模式。
讓我們想像這樣一個場景:我們正在使用一個遺留系統,並且想要透過訊息代理程式發送數據,我們決定使用 Avro 作為(反)序列化解決方案。在查閱程式碼的過程中,我們發現,透過發出現有類別所表達的數據,我們可以快速地遵守新規則。
手動將 Java 程式碼轉換為 Avro JSON 模式會很繁瑣。相反,我們可以使用可用的庫來為我們完成這項工作並節省時間。
4. 使用 Avro Reflection API 產生 Avro Schema
允許我們快速將現有 Java 類別轉換為 Avro 模式的第一個選項是使用 Avro Reflection 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
) {
}
如果我們重複這個測試,我們可以預期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 模式將包含一個聯合,允許分配這兩個類別中的任意一個,而不是限制我們只分配一個:
{
"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
或使用現成的 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
}
因此,當我們產生並驗證模式時,我們會注意到新欄位將包含一個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 Reflection API 很有用,並且應該能夠滿足不同甚至複雜的需求,但了解替代方案總是值得的。
在我們的例子中,我們剛剛嘗試過的庫的替代方案是 Jackson Dataformats Binary 庫,特別是其與 Avro 相關的子模組。
首先,讓我們將[jackson-core](https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core)
和[jackson-dataformat-avro](https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-avro/2.18.2)
依賴項加入我們的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 則相當小眾。
我們將建立AvroMapper
和AvroSchemaGenerator
實例並使用它們來檢索org.apache.avro.Schema
實例。
從那裡,我們只需呼叫toString()
方法,就像前面的範例一樣:
@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
}
透過這些修改,我們將能夠觀察在產生的Temporal
物件 Avro 模式中使用的邏輯類型功能。
6. 結論
在本文中,我們展示了允許我們從現有 Java 類別產生 Avro 模式的不同方法。可以使用標準 Avro Reflection API,以及帶有二進位 Avro 模組的 Jackson。
儘管 Avro 的方式及其 API 不太為廣大受眾所知,但它似乎是一種比使用 Jackson 更可預測的解決方案,如果將其納入我們正在進行的主要專案中,很容易導致錯誤。
本文中的範例並未詳盡介紹 Avro 或 Jackson 提供的可能性。請查看 GitHub 上的程式碼以查看不太常用的功能的範例,或參考這兩個庫之一的官方文件。
本文中介紹的所有程式碼均可在 GitHub 上找到。