Avro4k序列化BigDecimal时动态适配未知精度与小数位数的实现方案咨询
嘿,这个问题我之前踩过坑!Avro本身的decimal类型设计就是要求明确指定precision和scale的,这也是你遇到 rounding error的核心原因——固定的精度配置没法适配未知的BigDecimal数值。下面给你几个可行的解决方案,你可以根据自己的场景选:
方案1:用Bytes类型存储BigDecimal(最省心的方案)
如果不需要让其他依赖Avro decimal逻辑类型的系统读取你的数据,直接把BigDecimal序列化成字节数组是最省事的办法,完全不用管精度和小数位的问题。
你只需要写一个自定义的序列化器,把BigDecimal转成字符串再存成字节(用toPlainString()避免科学计数法的坑),反序列化的时候再转回来:
@Serializable class Aggregator( @Serializable(with = DynamicBigDecimalBytesSerializer::class) val value: BigDecimal ) object DynamicBigDecimalBytesSerializer : KSerializer<BigDecimal> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("BigDecimalBytes", PrimitiveKind.BYTE_ARRAY) override fun serialize(encoder: Encoder, value: BigDecimal) { val bytes = value.toPlainString().toByteArray(Charsets.UTF_8) encoder.encodeByteArray(bytes) } override fun deserialize(decoder: Decoder): BigDecimal { val bytes = decoder.decodeByteArray() return BigDecimal(String(bytes, Charsets.UTF_8)) } }
这个方案的优点是实现简单,完全保留BigDecimal的原始精度,不会有任何截断;缺点是Avro Schema里这个字段会被标记为bytes类型,而不是decimal,如果有其他系统依赖decimal类型的话就不适用了。
方案2:动态生成Decimal Schema(保留Avro Decimal类型)
如果必须要保留Avro的decimal逻辑类型,那只能手动针对每个BigDecimal实例生成对应的Schema,因为Avro4k默认是先生成固定Schema再序列化对象,没法动态调整精度。
大致思路是先获取当前BigDecimal的精度和小数位,再动态构建Schema,最后用Avro的GenericRecord来序列化:
// 假设这是你要序列化的BigDecimal实例 val targetValue = BigDecimal("987.6543210987654321") val precision = targetValue.precision() val scale = targetValue.scale() // 动态生成Decimal字段的Schema val decimalSchema = Schema.createDecimal(precision, scale) // 生成Aggregator的完整Schema val aggregatorSchema = SchemaBuilder.record("Aggregator") .fields() .name("value") .type(decimalSchema) .noDefault() .endRecord() // 构建GenericRecord并序列化 val record = GenericData.Record(aggregatorSchema) // 注意Avro的decimal需要传入无缩放值的字节数组 record.put("value", targetValue.unscaledValue().toByteArray()) val writer = DatumWriter<GenericRecord>(GenericDatumWriter(aggregatorSchema)) val outputStream = ByteArrayOutputStream() val encoder = BinaryEncoder(outputStream) writer.write(record, encoder) encoder.flush() val serializedData = outputStream.toByteArray()
这个方案能保留Avro的decimal类型,但缺点是没法用Avro4k的自动序列化注解,需要手动处理Schema和序列化逻辑,代码量会大很多,适合对Schema类型有严格要求的场景。
方案3:扩展Avro4k的Schema生成器(不推荐)
理论上可以扩展Avro4k的SchemaGenerator来动态生成精度,但实际操作起来非常受限——因为Schema生成是提前完成的,没法在序列化时获取到具体的BigDecimal实例的精度值,除非你把Schema生成和序列化绑定到一起,这会破坏Avro4k的原有流程,维护成本很高,所以不太推荐这个方向。
总结一下:如果没有强依赖Avro decimal类型,优先选方案1;如果必须保留decimal类型,就用方案2。
备注:内容来源于stack exchange,提问作者AmsterdamLuis

