【Kotlin + Java】应对前端JS大数字精度丢失的Jackson Long值序列化解决方案
由于JavaScript的数字精度只有2^53, 所以如果在前后端分离的架构上你后端返回的数字大于这个数的话那就非常的操蛋,因为他会精度丢失。
比如你后端生产ID使用的是雪花算法,那前端将直接雪崩。
添加数据返回一个ID,然后根据ID却啥都查不到,在摸不着头脑的一般人看来这是很诡异的。
所以在后端返回数据时都得做个转换才行,将超过2^53的数字全部转换为字符串返回。
这里提供的是基于 SpringMVC 的解决方案, 包括Kotlin + Java两种实现。
主要是我最近用Kotlin + SpringBoot开发项目时又遇到了这坑,问题是我明明按照以往使用Java的经验给配置好了的,结果返回的Long值还一直都是数字而不是字符串。 所幸最后还是折腾出来了,所以在这儿记录一下。
1、定义Long 的Jackson序列化器, 以下是Java/Kotlin两个版本:
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import java.io.IOException; /** * Long 精度兼容的序列化器 */ public class LongAccuracyCompatibleSerializer extends JsonSerializer<Long> { public static LongAccuracyCompatibleSerializer INSTANCE = new LongAccuracyCompatibleSerializer(); // JavaScript 中能精准表示的最大整数 2^53: 9007199254740992 public static final Long JS_MAX_NUMBER = Double.valueOf(Math.pow(2, 53)).longValue(); private LongAccuracyCompatibleSerializer() { if (INSTANCE != null) { throw new UnsupportedOperationException(); } } @Override public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value.compareTo(JS_MAX_NUMBER) == -1) gen.writeNumber(value); else gen.writeString(value.toString()); } }
object LongAccuracyCompatibleSerializer : JsonSerializer<Long>() { // JavaScript 中能精准表示的最大整数 2^53: 9007199254740992 private val JS_MAX_NUMBER = 2.0.pow(53.0).toLong(); @Throws(IOException::class) override fun serialize(value: Long, gen: JsonGenerator, serializers: SerializerProvider) { if (value.compareTo(JS_MAX_NUMBER) == -1) gen.writeNumber(value) else gen.writeString(value.toString()) } }
2、覆盖SprigMVC的Jackson使用的ObjectMapper,注入自定义的序列化相关拓展
Java的例子基本如下所示:
@Configuration public class CommonConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper objectMapper = new ObjectMapper(); //序列化的时候序列对象的所有非空属性 objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); //反序列化时候遇到不匹配的属性并不抛出异常 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); //序列化时候遇到空对象不抛出异常 objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); //反序列化的时候如果是无效子类型,不抛出异常 objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false); //不使用默认的dateTime进行序列化, objectMapper.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false); //自定义的序列化类 SimpleModule module = new SimpleModule(); module.addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE); module.addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE); module.addSerializer(Long.TYPE, LongAccuracyCompatibleSerializer.INSTANCE); module.addSerializer(Long.class, LongAccuracyCompatibleSerializer.INSTANCE); //使用JSR310提供的序列化类,里面包含了大量的JDK8时间序列化类 objectMapper.registerModules(new JavaTimeModule(), module); return objectMapper; } }
Java的注册Long.TYPE + Long.class就可以。
而如果你使用的是Kotlin的话,则需要注意一下。
× 以下这种方式无效:
module.addSerializer(java.lang.Long.TYPE, LongAccuracyCompatibleSerializer)
module.addSerializer(Long::class.java, LongAccuracyCompatibleSerializer)
√ 需要使用这种方式:
module.addSerializer(Long::class.javaPrimitiveType, LongAccuracyCompatibleSerializer)
module.addSerializer(Long::class.javaObjectType, LongAccuracyCompatibleSerializer)
Kotlin的完整例子:
@Configuration class CommonConfig { @Bean fun objectMapper(): ObjectMapper { val objectMapper = ObjectMapper() //序列化的时候序列对象的所有非空属性 objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) //反序列化时候遇到不匹配的属性并不抛出异常 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) //序列化时候遇到空对象不抛出异常 objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) //反序列化的时候如果是无效子类型,不抛出异常 objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false) //不使用默认的dateTime进行序列化, objectMapper.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false) //自定义的序列化类 val module = SimpleModule() module.addSerializer(LocalDate::class.java, LocalDateSerializer) module.addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer) module.addDeserializer(LocalDateTime::class.java, LocalDateTimeDeserializer(CommonConst.DATA_TIME_FORMATTER)) //> Long serializer module.addSerializer(Long::class.javaPrimitiveType, LongAccuracyCompatibleSerializer) module.addSerializer(Long::class.javaObjectType, LongAccuracyCompatibleSerializer) //使用JSR310提供的序列化类,里面包含了大量的JDK8时间序列化类 objectMapper.registerModules(JavaTimeModule(), module) return objectMapper } }
配置完毕后即可实现将所有返回的超过JavaScript精度限制的Long值序列化为String的效果。
Over~