前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot返回前端Long型丢失精度咋办

SpringBoot返回前端Long型丢失精度咋办

作者头像
用户3467126
发布2021-11-09 10:59:48
4.2K0
发布2021-11-09 10:59:48
举报
文章被收录于专栏:爱编码

最近为Prong开发了一个基于snowflake算法的Java分布式ID组件,将实体主键从原来的String类型的UUID修改成了Long型的分布式ID。修改后发现前端显示的ID和数据库中的ID不一致。例如数据库中存储的是:812782555915911412,显示出来却成了812782555915911400,后面2位变成了0,精度丢失了:

代码语言:javascript
复制
console.log(812782555915911412);
812782555915911400

这是什么原因呢?

原来,JavaScript中数字的精度是有限的,Java的Long类型的数字超出了JavaScript的处理范围。JavaScript内部只有一种数字类型Number,所有数字都是采用IEEE 754 标准定义的双精度64位格式存储,即使整数也是如此。这就是说,JavaScript 语言的底层根本没有整数,所有数字都是小数(64位浮点数)。其结构如图:

各位的含义如下:

  • 1位(s) 用来表示符号位,0表示正数,1表示负数
  • 11位(e) 用来表示指数部分
  • 52位(f) 表示小数部分(即有效数字)

双精度浮点数(double)并不是能够精确表示范围内的所有数, 虽然双精度浮点型的范围看上去很大: 。可以表示的最大整数可以很大,但能够精确表示、使用算数运算的并没有这么大。因为小数部分最大是 52 位,因此 JavaScript 中能精准表示的最大整数是 ,十进制为 9007199254740991。

代码语言:javascript
复制
console.log(Math.pow(2, 53) - 1);
console.log(1L<<53);
9007199254740991

JavaScript 有所谓的最大和最小安全值:

代码语言:javascript
复制
console.log(Number.MAX_SAFE_INTEGER);
console.log(Number.MIN_SAFE_INTEGER);
9007199254740991
-9007199254740991

安全意思是说能够one-by-one表示的整数,也就是说在(-2^{53}, 2^{53})范围内,双精度数表示和整数是一对一的,在这个范围以内,所有的整数都有唯一的浮点数表示,这叫做安全整数。

而超过这个范围,会有两个或更多整数的双精度表示是相同的;即超过这个范围,有的整数是无法精确表示的,只能大约(round)到与它相近的浮点数(说到底就是科学计数法)表示,这种情况下叫做不安全整数,例如:

代码语言:javascript
复制
console.log(Number.MAX_SAFE_INTEGER + 1);   // 结果:9007199254740992,精度未丢失
console.log(Number.MAX_SAFE_INTEGER + 2);   // 结果:9007199254740992,精度丢失
console.log(Number.MAX_SAFE_INTEGER + 3);   // 结果:9007199254740994,精度未丢失
console.log(Number.MAX_SAFE_INTEGER + 4);   // 结果:9007199254740996,精度丢失
console.log(Number.MAX_SAFE_INTEGER + 5);   // 结果:9007199254740996,精度未丢失

而Java的Long类型的有效位数是63位(扣除一位符号位),其最大值为2^{63}-1,十进制为9223372036854775807。

代码语言:javascript
复制
public static void main(String[] args) {
    System.out.println(Long.MAX_VALUE);
    System.out.println((1L<<63) -1);
}
9223372036854775807
9223372036854775807

所以只要java传给JavaScript的Long类型的值超过9007199254740991,就有可能产生精度丢失,从而导致数据和逻辑出错。

和其他编程语言(如 C 和 Java)不同,JavaScript 不区分整数值和浮点数值,所有数字在 JavaScript 中均用浮点数值表示,所以在进行数字运算的时候要特别注意精度缺失问题。容易造成混淆的是,某些运算只有整数才能完成,此时 JavaScript 会自动把64位浮点数,转成32位整数,然后再进行运算,由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。

那有什么解决方法呢?

解决办法之一就是让Javascript把数字当成字符串进行处理,对Javascript来说如果不进行运算,数字和字符串处理起来没有什么区别。但如果需要进行运算,只能采用其他方法,例如JavaScript的一些开源库 bignum、bigint等支持长整型的处理。在我们这个场景里不需要进行运算,且Java进行JSON处理的时候是能够正确处理long型的,所以只需要将数字转化成字符串就可以了。

大家都知道,用Spring cloud构建微服务架构时,API(controller)通常用@RestController进行注解,而 @Restcontroller@Controller@ResponseBody的结合体,而@ResponseBody用于将后台返回的Java对象转换为Json字符串传递给前台。

@Controller用于注解配合视图解析器InternalResourceViewResolver来完成页面跳转。如果要返回JSON数据到页面上,则需要使用@RestController注解。当数据库字段为date类型时,@ResponseBody注解在转换日期类型时会默认把日期转换为时间戳(例如:date:2017-10-25 转换为 时间戳:15003323990)。

在Spring boot中处理方法基本上有以下几种:

一、配置参数

Jackson有个配置参数WRITE_NUMBERS_AS_STRINGS,可以强制将所有数字全部转成字符串输出。其功能介绍为:Feature that forces all Java numbers to be written as JSON strings.。使用方法很简单,只需要配置参数即可:

代码语言:javascript
复制
spring:
  jackson:
    generator:
      write_numbers_as_strings: true

这种方式的优点是使用方便,不需要调整代码;缺点是颗粒度太大,所有的数字都被转成字符串输出了,包括按照timestamp格式输出的时间也是如此。

二、注解

另一个方式是使用注解JsonSerialize。使用官方提供的Serializer

代码语言:javascript
复制
@JsonSerialize(using=ToStringSerializer.class)
private Long bankcardHash;

指定了ToStringSerializer进行序列化,将数字编码成字符串格式。这种方式的优点是颗粒度可以很精细;缺点同样是太精细,如果需要调整的字段比较多会比较麻烦。

三、自定义ObjectMapper

可以单独根据类型进行设置,只对Long型数据进行处理,转换成字符串,而对其他类型的数字不做处理。Jackson提供了这种支持,即对ObjectMapper进行定制。根据SpringBoot的官方帮助,找到一种相对简单的方法,只对ObjectMapper进行定制,而不是完全从头定制,方法如下:

代码语言:javascript
复制
@Bean("jackson2ObjectMapperBuilderCustomizer")
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
    Jackson2ObjectMapperBuilderCustomizer customizer = new Jackson2ObjectMapperBuilderCustomizer() {
        @Override
        public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
            jacksonObjectMapperBuilder.serializerByType(Long.class, ToStringSerializer.instance)
                    .serializerByType(Long.TYPE, ToStringSerializer.instance);
        }
    };
    return customizer;
}

通过定义Jackson2ObjectMapperBuilderCustomizer,对Jackson2ObjectMapperBuilder对象进行定制,对Long型数据进行了定制,使用ToStringSerializer来进行序列化。问题终于完美解决。

四、使用HttpMessageConverter(建议方案)

4.1、定制HttpMessageConverter

关于HttpMessageConverter HttpMessageConverter接口提供了 5 个方法:

  • canRead:判断该转换器是否能将请求内容转换成 Java 对象
  • canWrite:判断该转换器是否可以将 Java 对象转换成返回内容
  • getSupportedMediaTypes:获得该转换器支持的 MediaType 类型
  • read:读取请求内容并转换成 Java 对象
  • write:将 Java 对象转换后写入返回内容

其中read和write方法的参数分别有有HttpInputMessage和HttpOutputMessage对象,这两个对象分别代表着一次 Http 通讯中的请求和响应部分,可以通过getBody方法获得对应的输入流和输出流。

代码语言:javascript
复制

package io.prong.boot.framework;

import java.math.BigInteger;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

import io.prong.boot.framework.json.CustomMappingJackson2HttpMessageConverter;

/**
 * prong boot 自动配置
 * 
 * @author tangyz
 *
 */
@Configuration
public class ProngBootAutoConfig {

    /**
     * 解决前端js处理大数字丢失精度问题,将Long和BigInteger转换成string
     * 
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public MappingJackson2HttpMessageConverter getMappingJackson2HttpMessageConverter() {
        CustomMappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new CustomMappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();
        SimpleModule simpleModule = new SimpleModule();
        // 序列换成json时,将所有的long变成string 因为js中得数字类型不能包含所有的java long值
        simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);
        jackson2HttpMessageConverter.setObjectMapper(objectMapper);
        return jackson2HttpMessageConverter;
    }

}
4.2、CustomMappingJackson2HttpMessageConverter

因为全局地对所有的long转string的粒度太粗了,我们需要对不同的接口进行区分,比如限定只对web前端的接口需要转换,但对于内部微服务之间的调用或者第三方接口等则不需要进行转换。CustomMappingJackson2HttpMessageConverter的主要作用就是为了限定long转string的范围为web接口,即符合/web/xxxxx风格的url(当然这个你需要根据自己产品的规范进行自定义)。

代码语言:javascript
复制
package io.prong.boot.framework.json;

import java.lang.reflect.Type;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * 自定义的json转换器,匹配web api(以/web/开头的controller)中的接口方法的返回参数
 * 
 * @author tangyz
 *
 */
public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {

    private final static Logger logger = LoggerFactory.getLogger(CustomMappingJackson2HttpMessageConverter.class);

    /**
     * 判断该转换器是否能将请求内容转换成 Java 对象
     */
    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        // 不需要反序列化
        return false;
    }

    /**
     * 判断该转换器是否能将请求内容转换成 Java 对象
     */
    @Override
    public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
        // 不需要反序列化
        return false;
    }

    /**
     * 判断该转换器是否可以将 Java 对象转换成返回内容.
     * 匹配web api(形如/web/xxxx)中的接口方法的返回参数
     */
    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        if (super.canWrite(clazz, mediaType)) {
            ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (ra != null) { // web请求
                HttpServletRequest request = ra.getRequest();
                String uri = request.getRequestURI(); // 例如: "/web/frontApplicationPage"
                logger.debug("Current uri is: {}", uri);
                if (uri.startsWith("/web/")) {
                    return true;
                }
            }
        }
        return false;
    }
    
}
4.3、排除例外

定义自己的Serializer

上面的MappingJackson2HttpMessageConverter将所有的long都转成了string,对于有些例外的情况,例如前端antd列表组件的总记录数为number,java后端使用了pagehelper分页组件,pagehelper的Page类返回的记录总数total为long型,如果转为string给前端就会有问题,因此,我们通过自定义的Serializer来排除这种例外。

代码语言:javascript
复制
import java.io.IOException;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

public class LongJsonSerializer extends JsonSerializer<Long> {

    @Override
    public void serialize(Long value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
            throws IOException {
        if (value != null) {
            jsonGenerator.writeNumber(value);
        }
    }

}

如何使用?

使用自定义的PageBean类替换官方的PageInfo,并在PageBean类中使用:

代码语言:javascript
复制
@JsonSerialize(using = LongJsonSerializer.class)
private long total;     // 总记录数
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 爱编码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 这是什么原因呢?
  • 那有什么解决方法呢?
  • 在Spring boot中处理方法基本上有以下几种:
    • 一、配置参数
      • 二、注解
        • 三、自定义ObjectMapper
          • 四、使用HttpMessageConverter(建议方案)
            • 4.1、定制HttpMessageConverter
            • 4.2、CustomMappingJackson2HttpMessageConverter
            • 4.3、排除例外
        相关产品与服务
        微服务引擎 TSE
        微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档