【SpringBoot】Jackson序列化方式解决JavaScript超大数字精度损失问题

一、问题/现象

  1. 数据库中存的值为1572460317496086530,前端接口响应1572460317496086500,精度损失
  2. postman或者其他rest工具调用接口响应正常

二、原因

2.1、分析

若没接触过,自行排查的话,解决思路应该是:前端浏览器异常,postman等正常,怀疑与浏览器有关,即Javascript,vue等前端代码,然后查资料即可

由于对阿里开发手册比较熟,知道是超大整数问题,直接打开规范查找即可,约定及解释如下。

在这里插入图片描述

2.2、疑问

  1. 整个demo验证一下 Javascript number类型精度问题?
  2. Javascript的Number类型造成的,那 thymeleaf 等模板引擎不是用js实现的,是否有影响呢?

带着以上两个问题,写个demo复现一下

三、复现

3.1、代码

3.1.1、entity

package com.example.demo.entity;

import lombok.Data;
import lombok.experimental.Accessors;

/**
 * @author lgm
 */
@Accessors(chain = true)
@Data
public class NumberEntity {
    
    

    private Long longThreshold;

    private String longThresholdStr;

    private Long longOverflow;

    private String longOverflowStr;

    private Long longNegative;

    private String longNegativeStr;

}

3.1.2、controller

package com.example.demo.controller;

import com.example.demo.entity.NumberEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import static com.example.demo.config.JacksonConfig.CustomLongSerializer.MAX_THRESHOLD;

/**
 * @author lgm
 */
@Controller
public class LongTestController {
    
    

    @ResponseBody
    @GetMapping("/map")
    public NumberEntity map() {
    
    
        return this.initEntity();
    }

    @GetMapping("/index")
    public String index(Model model) {
    
    
        model.addAttribute("numberEntity", this.initEntity());
        return "index";
    }
    
    /**
     * 初始化实体数据
     */
    private NumberEntity initEntity() {
    
    
        // javaScript 损失精度(javaScript number类型)
        final long LONG_OVERFLOW = MAX_THRESHOLD + 1;
        final long LONG_NEGATIVE = -MAX_THRESHOLD - 1;

        final NumberEntity entity = new NumberEntity();
        entity.setLongThreshold(MAX_THRESHOLD)
                .setLongThresholdStr(String.valueOf(MAX_THRESHOLD))
                .setLongOverflow(LONG_OVERFLOW)
                .setLongOverflowStr(String.valueOf(LONG_OVERFLOW))
                .setLongNegative(LONG_NEGATIVE)
                .setLongNegativeStr(String.valueOf(LONG_NEGATIVE));

        return entity;
    }
}

3.1.3、index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>大整数精度损失验证</title>
  <script src="http://libs.baidu.com/jquery/1.10.2/jquery.min.js" rel="external nofollow"></script>
</head>
<script>
  $(document).ready(function () {
      
      

    $.ajax({
      
      
      url: "/map",
      type: 'GET',
      success: function (result) {
      
      
        $("#table_ajax").html(
            "<table border=\"1\">"
            + "<caption>Ajax请求,Javascript生成</caption>"
            + "<tr>"
            +     "<th>类别</th>"
            +     "<th> 正常(2^53) </th>"
            +     "<th> 精度损失(2^53+1) </th>"
            +     "<th> Long负数(-2^53-1) </th>"
            + "</tr>"
            + "<tr>"
            +     "<td>数字</td>"
            +     "<td>" + result.longThreshold + "</td>"
            +     "<td>" + result.longOverflow + "</td>"
            +     "<td>" + result.longNegative + "</td>"
            + "</tr>"
            + "<tr>"
            + "<td>字符串</td>"
            +     "<td>" + result.longThresholdStr + "</td>"
            +     "<td>" + result.longOverflowStr + "</td>"
            +     "<td>" + result.longNegativeStr + "</td>"
            + "</tr>"
            + "</table>"
        );
      }
    });

  });
</script>

<body>
<!-- ajax 通过javascript处理,显示异常 -->
<div id="table_ajax"></div>

<!-- thymeleaf 非javascript处理,显示正常 -->
<div id="table_thymeleaf" style="margin-top: 30px">
  <table th:object="${numberEntity}" border="1">
    <caption>thymeleaf</caption>
    <tr>
      <th>类别</th>
      <th> 正常(2^53)</th>
      <th> 精度损失(2^53+1)</th>
      <th> Long负数(-2^53-1)</th>
    </tr>
    <tr>
      <td>数字</td>
      <td th:text="${numberEntity.longThreshold}"></td>
      <td th:text="${numberEntity.longOverflow}"></td>
      <td th:text="${numberEntity.getLongNegative}"></td>
    </tr>
    <tr>
      <td>字符串</td>
      <td th:text="${numberEntity.longThresholdStr}"></td>
      <td th:text="${numberEntity.longOverflowStr}"></td>
      <td th:text="${numberEntity.getLongNegativeStr}"></td>
    </tr>

  </table>
</div>
</body>
</html>

3.2、验证

3.2.1 、ajax请求/thymeleaf模板

ajax请求收到的响应,确认存在精度问题
thymeleaf模板正常,不存在精度问题
在这里插入图片描述

3.2.1 、rest客户端请求

postman太耗资源,个人不喜欢用,用的chrome浏览器插件API Tester,响应正常,不存在精度丢失问题。postman也一样,可以自行测试一下
在这里插入图片描述

四、解决(Jackson序列化方式)

由于是历史遗留老代码,改数据库字段或者javabean动静太大了。 于是通过jackson序列化方式来解决这个问题。

Tips:
使用swagger的话,存在接口文档响应类型和接口实际响应不一致问题

4.1、处理特定字段

如果只有个别字段会有超大整数问题,其余的可以明确不会,可以在个别字段上添加注解方式来处理

    @JsonSerialize(using = ToStringSerializer.class)
    private Long longOverflow;

如下图,虽然VO中定义的字段类型为 Long,接口响应为String,且不存在精度丢失问题
在这里插入图片描述

4.2、全局处理

4.2.1、Springboot自动装配yml

spring:
  jackson:
    generator:
      write_numbers_as_strings: true

如下图,所有数字类型响应都是字符串类型了
在这里插入图片描述

4.2.2、自定义Long序列化器

package com.example.demo.config;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.NumberSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;

import java.io.IOException;

/**
 * @author lgm
 */
@Slf4j
@Configuration
public class JacksonConfig {
    
    

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
    
    
        log.info("@============ 初始化jackson自定义long类型序列化器 =============@");

        return builder -> {
    
    
            // 序列化
            builder.serializerByType(Long.class, CustomLongSerializer.INSTANCE)
                    .serializerByType(Long.TYPE, CustomLongSerializer.INSTANCE);
        };
    }


    /**
     * 自定义Long序列化器
     *
     * <pre>
     *  【强制】对于需要使用超大整数的场景,服务端一律使用 String 字符串类型返回,禁止使用 Long 类型。
     *  说明:Java 服务端如果直接返回 Long 整型数据给前端,Javascript 会自动转换为 Number 类型(注:此类型为双精度浮点数,表示原理与取值范围等同于 Java 中的 Double)。
     *  Long 类型能表示的最大值是 2^63 -1,在取值范围之内,超过 2^53(9007199254740992)的数值转化为 Javascript 的 Number 时,有些数值会产生精度损失。
     *  扩展说明,在 Long 取值范围内,任何 2 的指数次的整数都是绝对不会存在精度损失的,所以说精度损失是一个概率问题。
     *  若浮点数尾数位与指数位空间不限,则可以精确表示任何整数,但很不幸,双精度浮点数的尾数位只有 52 位。
     *  反例:通常在订单号或交易号大于等于 16 位,大概率会出现前后端订单数据不一致的情况。
     *  比如,后端传输的 "orderId":362909601374617692,前端拿到的值却是:362909601374617660
     * </pre>
     *
     * @author lgm
     * @date 2023-01-03
     */
    public static class CustomLongSerializer extends NumberSerializer {
    
    

        private static final long serialVersionUID = -4406848951291696357L;

        public static final long MAX_THRESHOLD = 9007199254740992L;

        private static final long MIN_THRESHOLD = -9007199254740992L;

        public static final CustomLongSerializer INSTANCE = new CustomLongSerializer(Number.class);

        public CustomLongSerializer(Class<? extends Number> rawType) {
    
    
            super(rawType);
        }

        @Override
        public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
    
    
            // 方式一:超出范围序列化为字符串
            if (MIN_THRESHOLD <= value.longValue() && value.longValue() <= MAX_THRESHOLD) {
    
    
                super.serialize(value, gen, serializers);
            } else {
    
    
                gen.writeString(value.toString());
            }

            // 方式二:直接序列化为字符串
            // gen.writeString(value.toString());
        }
    }
}

五、Swagger接口文档问题

Tips:
使用swagger的话,存在接口文档响应类型和接口实际响应不一致问题

5.1、验证

  1. pom依赖
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>2.0.9</version>
</dependency>
  1. 配置
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;


@EnableSwagger2WebMvc
@Configuration
public class Knife4jConfig {
    
    

    @Bean(value = "defaultApi2")
    public Docket defaultApi2() {
    
    
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(new ApiInfoBuilder()
                        .title("javaScript超大数字精度丢失问题demo")
                        .description("# swagger-bootstrap-ui-demo RESTful APIs")
                        .termsOfServiceUrl("http://www.xx.com/")
                        .version("1.0")
                        .build())
                //分组名称
                .groupName("2.x版本")
                .select()
                //这里指定Controller扫描包路径
                .apis(RequestHandlerSelectors.basePackage("com.example.demo"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
}
  1. 接口和VO添加swagger注解
  2. 查看接口文档和响应
    在这里插入图片描述

六、示例代码

https://download.csdn.net/download/weixin_43582081/87360747

猜你喜欢

转载自blog.csdn.net/weixin_43582081/article/details/128532968