赞
踩
数据库中有一个bigint类型数据,对应java后台类型为Long型,在某个查询页面中碰到了问题:页面上显示的数据和数据库中的数据不一致。例如数据库中存储的是:1475797674679549851,显示出来却成了1475797674679550000,后面几位全变成了0,精度丢失了。
这是因为Javascript中数字的精度是有限的,bigint类型的的数字超出了Javascript的处理范围。JS 遵循 IEEE 754 规范,采用双精度存储(double precision),占用 64 bit。其结构如图:
各位的含义如下:
尾数位最大是 52 位,因此 JS 中能精准表示的最大整数是 Math.pow(2, 53),十进制即 9007199254740992。而Bigint类型的有效位数是63位(扣除一位符号位),其最大值为:Math.pow(2,63)。任何大于 9007199254740992 的就可能会丢失精度:
1 2 3 | 9007199254740992 >> 10000000000000...000 // 共计 53 个 0 9007199254740992 + 1 >> 10000000000000...001 // 中间 52 个 0 9007199254740992 + 2 >> 10000000000000...010 // 中间 51 个 0 |
实际上值却是:
1 2 3 4 | 9007199254740992 + 1 // 丢失 9007199254740992 + 2 // 未丢失 9007199254740992 + 3 // 丢失 9007199254740992 + 4 // 未丢失 |
解决办法就是让Javascript把数字当成字符串进行处理。对Javascript来说,不进行运算,数字和字符串处理起来没有什么区别。当然如果需要进行运算,只能采用其他方法,例如使用JavaScript的一些开源库bignumber之类的处理了。Java进行JSON处理的时候是能够正确处理long型的,只需要将数字转化成字符串就可以了。例如:
1 2 3 4 5 | { ... "bankcardHash": 1475797674679549851, ... } |
变为:
1 2 3 4 5 | { ... "bankcardHash": "1475797674679549851", ... } |
这样Javascript就可以按照字符串方式处理,不存在数字精度丢失了。在Springboot中处理方法基本上有以下几种:
Jackson有个配置参数WRITE_NUMBERS_AS_STRINGS
,可以强制将所有数字全部转成字符串输出。其功能介绍为:Feature that forces all Java numbers to be written as JSON strings.
。使用方法很简单,只需要配置参数即可:
1 2 3 4 | spring: jackson: generator: write_numbers_as_strings: true |
这种方式的优点是使用方便,不需要调整代码;缺点是颗粒度太大,所有的数字都被转成字符串输出了,包括按照timestamp格式输出的时间也是如此。
另一个方式是使用注解JsonSerialize
:
1 2 | @JsonSerialize(using=ToStringSerializer.class) private Long bankcardHash; |
指定了ToStringSerializer进行序列化,将数字编码成字符串格式。这种方式的优点是颗粒度可以很精细;缺点同样是太精细,如果需要调整的字段比较多会比较麻烦。
实现方法:
在dto所在项目中,新建一个helper包(名字自定义,也可以放现有包里)。PS:为什么要建到dto项目中?因为,这个包最后可能会给其他组使用,这样以来,所有的处理规则逻辑都是统一的,方便对接。 在包里添加类LongJsonSerializer,代码如下:
- /**
- * Long 类型字段序列化时转为字符串,避免js丢失精度
- *
- */
- public class LongJsonSerializer extends JsonSerializer<Long> {
- @Override
- public void serialize(Long value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
- String text = (value == null ? null : String.valueOf(value));
- if (text != null) {
- jsonGenerator.writeString(text);
- }
- }
- }
然后在包里再添加类LongJsonDeserializer,代码如下:
- /**
- * 将字符串转为Long
- *
- */
- public class LongJsonDeserializer extends JsonDeserializer<Long> {
- private static final Logger logger = LoggerFactory.getLogger(LongJsonDeserializer.class);
-
- @Override
- public Long deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
- String value = jsonParser.getText();
- try {
- return value == null ? null : Long.parseLong(value);
- } catch (NumberFormatException e) {
- logger.error("解析长整形错误", e);
- return null;
- }
- }
- }
好了,接下来是使用这两个类。在需要处理的id字段上,加上注解,比如如下代码:
- /**
- * id
- */
- @JsonSerialize(using = LongJsonSerializer.class)
- @JsonDeserialize(using = LongJsonDeserializer.class)
- private Long id;
最后想到可以单独根据类型进行设置,只对Long型数据进行处理,转换成字符串,而对其他类型的数字不做处理。Jackson提供了这种支持。方法是对ObjectMapper进行定制。根据SpringBoot的官方帮助https://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html#howto-customize-the-jackson-objectmapper 找到一种相对简单的方法,只对ObjectMapper进行定制,而不是完全从头定制,方法如下:
- @Bean
- public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
-
- return jacksonObjectMapperBuilder -> {
- jacksonObjectMapperBuilder.serializerByType(Long.TYPE, ToStringSerializer.instance);
- jacksonObjectMapperBuilder.serializerByType(Long.class, ToStringSerializer.instance);
- jacksonObjectMapperBuilder.serializerByType(BigInteger.class, ToStringSerializer.instance);
- jacksonObjectMapperBuilder.serializerByType(BigDecimal.class, ToStringSerializer.instance);
- };
- }
通过定义Jackson2ObjectMapperBuilderCustomizer
,对Jackson2ObjectMapperBuilder
对象进行定制,对Long
型数据进行了定制,使用ToStringSerializer
来进行序列化。问题终于完美解决。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。