1572460317496086530
,前端接口响应1572460317496086500
,精度损失若没接触过,自行排查的话,解决思路应该是:前端浏览器异常,postman等正常,怀疑与浏览器有关,即Javascript,vue等前端代码,然后查资料即可
由于对阿里开发手册比较熟,知道是超大整数问题,直接打开规范查找即可,约定及解释如下。
thymeleaf
等模板引擎不是用js实现的,是否有影响呢?带着以上两个问题,写个demo复现一下
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;
}
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;
}
}
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>
postman太耗资源,个人不喜欢用,用的chrome浏览器插件API Tester
,响应正常,不存在精度丢失问题
。postman也一样,可以自行测试一下
由于是历史遗留老代码,改数据库字段或者javabean动静太大了。 于是通过jackson序列化方式来解决这个问题。
Tips
:
使用swagger的话,存在接口文档响应类型和接口实际响应不一致问题
如果只有个别字段会有超大整数问题,其余的可以明确不会,可以在个别字段上添加注解方式来处理
@JsonSerialize(using = ToStringSerializer.class)
private Long longOverflow;
如下图,虽然VO中定义的字段类型为 Long,接口响应为String,且不存在精度丢失问题
spring:
jackson:
generator:
write_numbers_as_strings: true
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());
}
}
}
Tips
:
使用swagger的话,存在接口文档响应类型和接口实际响应不一致问题
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>knife4j-spring-boot-starterartifactId>
<version>2.0.9version>
dependency>
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;
}
}
https://download.csdn.net/download/weixin_43582081/87360747