解决Long精度丢失问题
前言
记录一下之前在公司遇到的一个问题,后端ID
使用了Long
类型的雪花算法,ID
在返回给前端发现显示的 ID 和 数据库中的 ID 不一致,例如数据库中存储的 ID 是 112232531915911412
,显示出来却是 112232531915911400
,后面2位变成了0,精度丢失了
原因
这是因为JavaScript中的数字的精度是有限的, Java 的 Long 类型的数字超过 JavaScript 的处理范围。 在JavaScript 中只有一种数字类型 Number,所有数字都是采用IEEE 754 标准定义的双精度64位格式存储,即使整数也是如此。也就是说在JavaScript的底层根本没有整数,所有的数字都是小数(64位浮点数)。结构如下:
s 1 |
e 11 |
f 52 |
---|
各位的含义如下:
- 1位(s) 用来表示符号位,0表示正数,1表示负数
- 11位(e) 用来表示指数部分
- 52位(f) 表示小数部分(即有效数字)
双精度浮点数(double)并不是能够精确表示范围内的所有数, 虽然双精度浮点型的范围看上去很大。可以表示的最大整数可以很大,但能够精确表示、使用算数运算的并没有这么大。因为小数部分最大是 52 位,因此 JavaScript 中能精准表示的最大整数是 ,十进制为 9007199254740991。
所以只要Java传给JavaScript的Long类型的值超过9007199254740991,就有可能产生精度丢失,从而导致数据和逻辑出错。
解决方法
配置参数
在 SpringBoot 中默认使用的是 Jackson 序列化, 而 Jackson 有个配置参数WRITE_NUMBERS_AS_STRINGS
,可以强制将所有数字全部转成字符串输出。其功能介绍为:Feature that forces all Java numbers to be written as JSON strings.
。使用方法很简单,只需要配置参数即可:
spring:
jackson:
generator:
write_numbers_as_strings: true
注解
另一个方式是使用注解@JsonSerialize
。使用官方提供的Serializer
@JsonSerialize(using=ToStringSerializer.class)
private Long id;
指定了ToStringSerializer
进行序列化,将数字编码成字符串格式。这种方式的优点是颗粒度可以很精细;缺点同样是太精细,如果需要调整的字段比较多会比较麻烦
使用JsonComponent序列化配置
/**
* Spring MVC Json 配置
*/
@Slf4j
@JsonComponent
public class JsonConfig {
/**
* 添加 Long 转换 json 精度丢失的配置
*
* @param builder 构造器
* @return ObjectMapper
*/
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
log.info("jacksonObjectMapper 进行Long转换成String");
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
SimpleModule simpleModule = new SimpleModule();
//Long类型转String类型
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
//long类型转String类型
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
return objectMapper;
}
}
扩展
配置了全局配置,会把所有的 Long 类型都改成 String类型,如果想要有些字段不想 Long转换成 String 类型,有以下两种方法
- 使用 @JsonSerialize 注解,在需要保持数字的字段上添加注解,单独指定序列化方式:
public class PageQuery {
// 普通 Long 字段自动转 String(解决精度丢失)
private Long id;
// 特殊字段保持数字类型
@JsonSerialize(using = JsonSerializer.None.class)
// 禁用全局配置
private Long total;
@JsonSerialize(using = JsonSerializer.None.class)
// Integer 不受影响,但显式声明更清晰
private Integer size;
@JsonSerialize(using = JsonSerializer.None.class)
private Integer page;
}
- 自定义序列化器(按字段名过滤)
public class SmartLongSerializer extends JsonSerializer<Long> {
private static final Set<String> NUMERIC_FIELDS = Set.of("total", "size", "page");
@Override
public void serialize(Long value, JsonGenerator gen, SerializerProvider provider) throws IOException {
// 检查字段名:如果是特殊字段则输出数字,否则转字符串
String fieldName = gen.getOutputContext().getCurrentName();
if (fieldName != null && NUMERIC_FIELDS.contains(fieldName)) {
gen.writeNumber(value);
} else {
gen.writeString(value.toString());
}
}
}