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

文章目录

  • 一、问题/现象
  • 二、原因
    • 2.1、分析
    • 2.2、疑问
  • 三、复现
    • 3.1、代码
      • 3.1.1、entity
      • 3.1.2、controller
      • 3.1.3、index.html
    • 3.2、验证
      • 3.2.1 、ajax请求/thymeleaf模板
      • 3.2.1 、rest客户端请求
  • 四、解决(Jackson序列化方式)
    • 4.1、处理特定字段
    • 4.2、全局处理
      • 4.2.1、Springboot自动装配yml
      • 4.2.2、自定义Long序列化器
  • 五、Swagger接口文档问题
    • 5.1、验证
  • 六、示例代码

一、问题/现象

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

二、原因

2.1、分析

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

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

【SpringBoot】Jackson序列化方式解决JavaScript超大数字精度损失问题_第1张图片

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(
            ""+""+""+""+""+""+""+""+""+""+""+""+""+""+""+""+""+""+""+""+"
Ajax请求,Javascript生成
类别 正常(2^53) 精度损失(2^53+1) Long负数(-2^53-1)
数字 " + result.longThreshold + " " + result.longOverflow + " " + result.longNegative + "
字符串 " + result.longThresholdStr + " " + result.longOverflowStr + " " + result.longNegativeStr + "
"
); } }); });
script> <body> <div id="table_ajax">div> <div id="table_thymeleaf" style="margin-top: 30px"> <table th:object="${numberEntity}" border="1"> <caption>thymeleafcaption> <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模板正常,不存在精度问题
【SpringBoot】Jackson序列化方式解决JavaScript超大数字精度损失问题_第2张图片

3.2.1 、rest客户端请求

postman太耗资源,个人不喜欢用,用的chrome浏览器插件API Tester,响应正常,不存在精度丢失问题。postman也一样,可以自行测试一下
【SpringBoot】Jackson序列化方式解决JavaScript超大数字精度损失问题_第3张图片

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

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

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

4.1、处理特定字段

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

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

如下图,虽然VO中定义的字段类型为 Long,接口响应为String,且不存在精度丢失问题
【SpringBoot】Jackson序列化方式解决JavaScript超大数字精度损失问题_第4张图片

4.2、全局处理

4.2.1、Springboot自动装配yml

spring:
  jackson:
    generator:
      write_numbers_as_strings: true

如下图,所有数字类型响应都是字符串类型了
【SpringBoot】Jackson序列化方式解决JavaScript超大数字精度损失问题_第5张图片

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序列化器
     *
     * 
     *  【强制】对于需要使用超大整数的场景,服务端一律使用 String 字符串类型返回,禁止使用 Long 类型。
     *  说明:Java 服务端如果直接返回 Long 整型数据给前端,Javascript 会自动转换为 Number 类型(注:此类型为双精度浮点数,表示原理与取值范围等同于 Java 中的 Double)。
     *  Long 类型能表示的最大值是 2^63 -1,在取值范围之内,超过 2^53(9007199254740992)的数值转化为 Javascript 的 Number 时,有些数值会产生精度损失。
     *  扩展说明,在 Long 取值范围内,任何 2 的指数次的整数都是绝对不会存在精度损失的,所以说精度损失是一个概率问题。
     *  若浮点数尾数位与指数位空间不限,则可以精确表示任何整数,但很不幸,双精度浮点数的尾数位只有 52 位。
     *  反例:通常在订单号或交易号大于等于 16 位,大概率会出现前后端订单数据不一致的情况。
     *  比如,后端传输的 "orderId":362909601374617692,前端拿到的值却是:362909601374617660
     * 
* * @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.xiaoymingroupId>
    <artifactId>knife4j-spring-boot-starterartifactId>
    <version>2.0.9version>
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. 查看接口文档和响应
    【SpringBoot】Jackson序列化方式解决JavaScript超大数字精度损失问题_第6张图片

六、示例代码

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

你可能感兴趣的:(Java,java,spring,boot)