Avro 1.8.2序列化可选date类型字段失败,求解决方案
问题描述
我们使用Avro 1.8.2序列化包含可选date类型字段的数据并发布到Kafka Topic,对应的Avro Schema如下:
record aRecord { /** Variable: lastUpdate * lastUpdate indicates the latest date and time the reference asset was updated */ union {null, date} lastUpdate = null; /** Variable: businessDate * businessDate indicates the business date of the reference asset price */ union {null, date} businessDate = null; }
使用Avro工具生成的Java类序列化数据时,遇到如下异常:
Error serializing avro message Caused by: org.apache.avro.AvroRunTimeException: Unknown datum type org.joda.time.LocalDate: 2021-09-17 at org.apache.avro.generic.GenericData.getSchemaName(GenericData.java:772) at org.apache.avro.specific.SpecificData.getSchemaName(SpecificData.java:302) at org.apache.avro.generic.GenericData.resolveUnion
无论字段值为null还是非null(如示例中的2021-09-17),都会触发该异常。我们已进行以下排查但未找到原因:
- 将date字段设为必填后问题解决,因为Avro工具生成的Java类中会为该字段添加DATE_CONVERSION;但若字段设为可选且默认值为null,则不会添加DATE_CONVERSION。
- 使用Avro 1.9.1可解决问题,但我们必须使用Avro 1.8.2。
- 尝试了多个版本的kafka-avro-serializer和Spring Boot Kafka框架,均无效。
- 其他依赖Avro 1.8.2的项目可正常处理,其Avro Schema定义与我们一致,但生成的Java类中包含DATE_CONVERSION。
- 调试GenericData.java发现,若可选date字段启用了DATE_CONVERSION,则不会调用getSchemaName方法;该方法主要检查对象类型(如Int、Record、String等)。
- 我们了解到date是Joda的逻辑类型,其底层类型为int。
现提出两个问题:
- 如何在Avro 1.8.2中让Avro工具为可选date类型字段启用DATE_CONVERSION?
- 若DATE_CONVERSION并非解决问题的关键,Avro 1.8.2中序列化可为null的date类型字段的最佳实践是什么?
解决方案
问题1:让Avro 1.8.2为可选date字段生成DATE_CONVERSION
Avro 1.8.2的代码生成器有个已知局限:处理包含date的union类型时,默认不会自动添加DATE_CONVERSION。可以通过以下几种方式解决:
- 修改Avro代码生成模板:
找到avro-tools-1.8.2.jar里的specific/class.java.vmVelocity模板,修改字段生成逻辑——当字段类型是包含date的union时,强制为字段添加@AvroConversion(DateConversion.class)注解。 - 手动修改生成的Java类:
生成类后直接手动给可选date字段加上DATE_CONVERSION注解,同时确保getter/setter能正确处理Joda LocalDate和int的转换。示例代码:@AvroConversion(DateConversion.class) private org.joda.time.LocalDate lastUpdate; public void setLastUpdate(org.joda.time.LocalDate value) { this.lastUpdate = value; } public org.joda.time.LocalDate getLastUpdate() { return this.lastUpdate; } - 自定义代码生成插件:
基于Avro的代码生成API写个自定义插件,在生成Java类时自动为包含date的union字段添加DATE_CONVERSION。
问题2:Avro 1.8.2序列化可选date字段的最佳实践
如果没法修改代码生成逻辑,可采用以下替代方案:
- 手动处理类型转换:
序列化前把Joda LocalDate转成对应的int值(即从1970-01-01开始的天数),null直接传null;反序列化时再把int转回LocalDate。绕开Avro的逻辑类型处理,直接操作底层类型。示例代码:// 序列化转换 Integer lastUpdateDays = lastUpdate != null ? Days.daysBetween(new LocalDate(1970,1,1), lastUpdate).getDays() : null; aRecord.setLastUpdate(lastUpdateDays); // 反序列化转换 Integer days = aRecord.getLastUpdate(); LocalDate lastUpdate = days != null ? new LocalDate(1970,1,1).plusDays(days) : null; - 使用GenericRecord替代SpecificRecord:
放弃用Avro生成的Java类,直接用GenericRecord构建数据。手动把LocalDate转成int后放入GenericRecord,序列化时Avro能正确识别底层int类型。示例代码:Schema schema = new Schema.Parser().parse(new File("aRecord.avsc")); GenericRecord record = new GenericData.Record(schema); if (lastUpdate != null) { int days = Days.daysBetween(new LocalDate(1970,1,1), lastUpdate).getDays(); record.put("lastUpdate", days); } else { record.put("lastUpdate", null); } - 升级到Avro 1.8.2的补丁版本:
检查是否有社区维护的Avro 1.8.2补丁版本,部分补丁修复了union类型中逻辑类型的转换问题,若能使用这类版本可直接解决问题。
内容的提问来源于stack exchange,提问作者Jin Ma
相关产品推荐
相关产品推荐

