关于浏览器精度问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于浏览器精度问题相关的知识,希望对你有一定的参考价值。

参考技术A     查找资料之后发现,是浏览器精度问题。js中的number类型不能完全表示long类型的数据,当long长度大于17位时,会出现精度丢失的问题,浏览器会自动把超出部分用0表示。

chrome从第17位就开始不正常(有时候正常有时候+1),18位及以后均补0

关于Long转String和js丢失精度截断的问题

背景

Java后端的 Long(long) 类型字段,在web接口返回这种类型的值给前端时,会发生截断(即js会精度丢失,因为后端的Long的最大值超过了前端js能表示的值)

哪些情况下发生截断

  • 如果前端不是用 js框架则不会截断,比如用postman调用就不会截断,浏览器地址栏直接请求也不会截断
  • 前端用 js 类型的框架可能会发生截断,但是只有在实际值超过js的最大范围的时候才会截断
  • 截断的一般特征是 000 结尾,比如 9223372036854776000
  • 如果 Long longValue 实际值没有超过前端js的限制,也不会出现截断

如何解决

  1. 建议项目代码编写的一开始就用String类型不要用Long

  2. 如果是接手的代码,并且按1的方法改造不切实际,可以配置全局Long转String

  3. 用注解,如下(其实第2点会有问题,我在实际项目中遇到过不生效的情况。原来生效后来可能是jar包版本升级了或者其他未知的原因导致了问题)

    @JsonSerialize(using = ToStringSerializer.class)
    

    如上注解加在想要转字串的字段上,比如Long/long/Integer/int等字段上

    上述详细的包名是

    import com.fasterxml.jackson.databind.annotation.JsonSerialize;
    import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

总结:

总体来说我觉得第2中方法应该是最好的,

如果实在没办法用3也不错,用3的话不需要改太多代码。

1这种虽然最彻底,改动也大,容易引发新的bug,虽然 Long 改 String 这种会触发编译上的报错,提示你需要改动的地方,但是也有一些bug很隐蔽

第一个是 Map#get(Object key) 方法,就算你用了泛型,比如 Map<Long, Object>改成了 Map<String, Object>,但是 map.get(key) 不管这个 key 是 Long 还是 String 都不会报错,但是实际 get 的话会get 不到值。

第二个是equals方法,equals() 的入参是 Object

全局 Long 转 String 的配置

对于

@Data
public class DemoDTO {
    private Long longValue;
    private Integer intValue;

    private long primitiveLongValue;
    private int primitiveIntValue;
}
@ApiOperation("test1 接口")
@GetMapping("/test1")
public DemoDTO test1() {
    DemoDTO result = new DemoDTO();
    result.setLongValue(Long.MAX_VALUE);
    result.setIntValue(Integer.MAX_VALUE);
    result.setPrimitiveLongValue(Long.MAX_VALUE);
    result.setPrimitiveIntValue(Integer.MAX_VALUE);
    return result;
}
1、配置一
@Configuration
public class LongToStringConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder
                .serializerByType(Long.class, ToStringSerializer.instance)
                .serializerByType(Long.TYPE, ToStringSerializer.instance);
    }

实测可以将 Long/long 转字串,对于 Integer/int 等其他类型不会

2、配置二
@Configuration
public class LongToStringConfig {
    @Bean
    public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
        mapper.registerModule(simpleModule);
        converter.setObjectMapper(mapper);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return converter;
    }
}

这种写法也是可以的

3、配置三

这是个无效的方法,网上也列出了很多这种写法,列在这里是为了警示。主要是 @EnableWebMvc 注解导致 swagger 无法用,不加又导致Long转String没法用。

有解决办法,但是实在不想用这么麻烦的方法了,都有前面2种好的方法了,这里第3种的写法就作罢了

package com.example.demo.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.List;

/**
 * 【这是个无效的方法,网上也列出了很多这种写法,列在这里是为了警示。主要是 @EnableWebMvc 注解导致 swagger 无法用,不加又导致Long转String没法用】
 * 这是第三种方法,除了 LongToStringConfig 里的二选一,这是第三种方法解决 Long 转 String
 * 说明:
 * 网上有些资料还是继承 WebMvcConfigurerAdapter 的,这个已经 @Deprecated 了,说明得比较清晰了就是因为有default 关键字后,
 * 接口的方法也可以有默认的实现,所以不需要这个 adapter 了 (PS:可以看出,很多的 adapter 的设计就是为了提供方法的空实现,
 * 以便避开需要实现接口所有方法的麻烦,使用 extends adapter 只需要重写感兴趣的方法)
 */
@Configuration
// 这个注解会导致swagger打不开(已经打开的页面重新刷新一下就无法再次打开了),参考 https://blog.csdn.net/Apoca_lypse/article/details/115759895。
// 但是不用这个注解又会导致无法将Long转String
// 这里有解决办法,但是实在不想用这么麻烦的方法了,都有前面2种好的方法了,这里第3种的写法就作罢了  (解决:https://blog.csdn.net/axiang_/article/details/107103879)
@EnableWebMvc
public class LongToStringConfig2 implements WebMvcConfigurer /*extends WebMvcConfigurerAdapter*/ {


    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);

        converter.setObjectMapper(objectMapper);
        converters.add(converter);
    }
}

实验过程

@Data
public class DemoDTO {
    private Long longValue;
    private Integer intValue;

    private long primitiveLongValue;
    private int primitiveIntValue;
}

@ApiOperation("test1 接口")
@GetMapping("/test1")
public DemoDTO test1() {
    DemoDTO result = new DemoDTO();
    result.setLongValue(Long.MAX_VALUE);
    result.setIntValue(Integer.MAX_VALUE);
    result.setPrimitiveLongValue(Long.MAX_VALUE);
    result.setPrimitiveIntValue(Integer.MAX_VALUE);
    return result;
}
  • 原生 swagger UI 发生了截断,因为它是用js解析并展示值的

  • 第三方的新的 swagger UI 并不会发生截断,因为它类似postman一样不是将值用js展示

  • 浏览器地址栏直接访问(刚好接口是GET,否则也不能直接访问),不截断

  • postman 工具访问,不截断 (要截断就大事情了,一个工具做成这样子)

  • 如果虽然这个字段定义是 Long(long) 但是如果还没超过前端 js 的限制,也不会发生截断

    @ApiOperation("test1 接口")
    @GetMapping("/test1")
    public DemoDTO test1() {
        DemoDTO result = new DemoDTO();
        result.setLongValue((long) Integer.MAX_VALUE);// 低于阈值,不会被截断  (这个值前端js还不会发生截断)
        result.setIntValue(Integer.MAX_VALUE);
        result.setPrimitiveLongValue((long) Integer.MAX_VALUE);// 低于阈值,不会被截断
        result.setPrimitiveIntValue(Integer.MAX_VALUE);
        return result;
    }
    

  • 如下,需要的字段加上 @JsonSerialize(using=ToStringSerializer.class) 即可

    @Data
    public class DemoDTO {
        @JsonSerialize(using = ToStringSerializer.class)
        private Long longValue;
        private Integer intValue;
    
        @JsonSerialize(using = ToStringSerializer.class)
        private long primitiveLongValue;
        private int primitiveIntValue;
    }
    

    传统 swagger UI 上显示的结果为

    {
      "longValue": "9223372036854775807",
      "intValue": 2147483647,
      "primitiveLongValue": "9223372036854775807",
      "primitiveIntValue": 2147483647
    }
    

    不限制于 Long/long,对于 Integer/int 也可以,实际上对于任何类型的字段都是可以加这个注解的

  • 试一下其他类型加 @JsonSerialize(using=ToStringSerializer.class)

    @Data
    public class DemoDTO {
        @JsonSerialize(using = ToStringSerializer.class)
        private Long longValue;
        @JsonSerialize(using = ToStringSerializer.class)
        private Integer intValue;
    
        @JsonSerialize(using = ToStringSerializer.class)
        private long primitiveLongValue;
        @JsonSerialize(using = ToStringSerializer.class)
        private int primitiveIntValue;
    
        @JsonSerialize(using = ToStringSerializer.class)
        private List<String> list;
    
        @JsonSerialize(using = ToStringSerializer.class)
        private Date date;
    }
    
    
    @ApiOperation("test1 接口")
    @GetMapping("/test1")
    public DemoDTO test1() {
        DemoDTO result = new DemoDTO();
        result.setLongValue(Long.MAX_VALUE);
        result.setIntValue(Integer.MAX_VALUE);
        result.setPrimitiveLongValue(Long.MAX_VALUE);
        result.setPrimitiveIntValue(Integer.MAX_VALUE);
        List<String> list = new ArrayList<>();
        list.add("one");
        result.setList(list);
        result.setDate(new Date());
        return result;
    }
    

    传统 swagger UI 上显示的结果为

    {
      "longValue": "9223372036854775807",
      "intValue": "2147483647",
      "primitiveLongValue": "9223372036854775807",
      "primitiveIntValue": "2147483647",
      "list": "[one]",
      "date": "Wed Aug 11 14:46:15 CST 2021"
    }
    

    如果都没有注解,可以看到结果:(着重观察下 list 和 date)

    {
      "longValue": 9223372036854776000,
      "intValue": 2147483647,
      "primitiveLongValue": 9223372036854776000,
      "primitiveIntValue": 2147483647,
      "list": [
        "one"
      ],
      "date": "2021-08-11T06:49:10.238+00:00"
    }
    

以上是关于关于浏览器精度问题的主要内容,如果未能解决你的问题,请参考以下文章

浏览器响应数据long型超长自动转换精度丢失-JavaScript 整数精度丢失问题-springboot解决Long类型数据传入前端损失精度

有没有办法检测浏览器是不是具有亚像素精度?

浏览器接收Long型数据精度丢失问题的解决方案

浏览器接收Long型数据精度丢失问题的解决方案

主流浏览器的亚像素精度现状如何?

关于百度地图