Spring Boot返回前端Long型丢失精度 - 简书
source link: https://www.jianshu.com/p/f46699ea331a?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
最近为Prong开发了一个基于snowflake算法的Java分布式ID组件,将实体主键从原来的String类型的UUID修改成了Long型的分布式ID。修改后发现前端显示的ID和数据库中的ID不一致。例如数据库中存储的是:812782555915911412
,显示出来却成了812782555915911400
,后面2位变成了0,精度丢失了:
console.log(812782555915911412);
812782555915911400
这是什么原因呢?
原来,JavaScript中数字的精度是有限的,Java的Long
类型的数字超出了JavaScript的处理范围。JavaScript内部只有一种数字类型Number
,所有数字都是采用IEEE 754 标准定义的双精度64位格式存储,即使整数也是如此。这就是说,JavaScript 语言的底层根本没有整数,所有数字都是小数(64位浮点数)。其结构如图:
各位的含义如下:
- 1位(s) 用来表示符号位,
0
表示正数,1
表示负数 - 11位(e) 用来表示指数部分
- 52位(f) 表示小数部分(即有效数字)
双精度浮点数(double
)并不是能够精确表示范围内的所有数, 虽然双精度浮点型的范围看上去很大: 。 可以表示的最大整数可以很大,但能够精确表示、使用算数运算的并没有这么大。因为小数部分最大是 52
位,因此 JavaScript 中能精准表示的最大整数是 ,十进制为 9007199254740991
。
console.log(Math.pow(2, 53) - 1);
console.log(1L<<53);
9007199254740991
JavaScript 有所谓的最大和最小安全值:
console.log(Number.MAX_SAFE_INTEGER);
console.log(Number.MIN_SAFE_INTEGER);
9007199254740991
-9007199254740991
安全
意思是说能够one-by-one
表示的整数,也就是说在(, )范围内,双精度数表示和整数是一对一的,在这个范围以内,所有的整数都有唯一的浮点数表示,这叫做安全整数
。
而超过这个范围,会有两个或更多整数的双精度表示是相同的;即超过这个范围,有的整数是无法精确表示的,只能大约(round)到与它相近的浮点数(说到底就是科学计数法)表示,这种情况下叫做不安全整数
,例如:
console.log(Number.MAX_SAFE_INTEGER + 1); // 结果:9007199254740992,精度未丢失
console.log(Number.MAX_SAFE_INTEGER + 2); // 结果:9007199254740992,精度丢失
console.log(Number.MAX_SAFE_INTEGER + 3); // 结果:9007199254740994,精度未丢失
console.log(Number.MAX_SAFE_INTEGER + 4); // 结果:9007199254740996,精度丢失
console.log(Number.MAX_SAFE_INTEGER + 5); // 结果:9007199254740996,精度未丢失
而Java的Long
类型的有效位数是63
位(扣除一位符号位),其最大值为,十进制为9223372036854775807
。
public static void main(String[] args) {
System.out.println(Long.MAX_VALUE);
System.out.println((1L<<63) -1);
}
9223372036854775807
9223372036854775807
所以只要java传给JavaScript的Long
类型的值超过9007199254740991
,就有可能产生精度丢失,从而导致数据和逻辑出错。
和其他编程语言(如 C 和 Java)不同,JavaScript 不区分整数值和浮点数值,所有数字在 JavaScript 中均用浮点数值表示,所以在进行数字运算的时候要特别注意精度缺失问题。容易造成混淆的是,某些运算只有整数才能完成,此时 JavaScript 会自动把64位浮点数,转成32位整数,然后再进行运算,由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。
那有什么解决方法呢?
解决办法之一就是让Javascript把数字当成字符串进行处理,对Javascript来说如果不进行运算,数字和字符串处理起来没有什么区别。但如果需要进行运算,只能采用其他方法,例如JavaScript的一些开源库 bignum、bigint等支持长整型的处理。在我们这个场景里不需要进行运算,且Java进行JSON处理的时候是能够正确处理long型的,所以只需要将数字转化成字符串就可以了。
大家都知道,用Spring cloud构建微服务架构时,API(controller)通常用@RestController
进行注解,而 @Restcontroller
是@Controller
和@ResponseBody
的结合体,而@ResponseBody
用于将后台返回的Java对象转换为Json字符串传递给前台。
@Controller用于注解配合视图解析器
InternalResourceViewResolver
来完成页面跳转。如果要返回JSON数据到页面上,则需要使用@RestController注解。
当数据库字段为date类型时,@ResponseBody注解在转换日期类型时会默认把日期转换为时间戳(例如: date:2017-10-25 转换为 时间戳:15003323990)。
在Spring boot中处理方法基本上有以下几种:
一、配置参数
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
这种方式的优点是使用方便,不需要调整代码;缺点是颗粒度太大,所有的数字都被转成字符串输出了,包括按照timestamp格式输出的时间也是如此。
另一个方式是使用注解JsonSerialize
。
使用官方提供的Serializer
@JsonSerialize(using=ToStringSerializer.class)
private Long bankcardHash;
指定了ToStringSerializer
进行序列化,将数字编码成字符串格式。这种方式的优点是颗粒度可以很精细;缺点同样是太精细,如果需要调整的字段比较多会比较麻烦。
三、自定义ObjectMapper
可以单独根据类型进行设置,只对Long型数据进行处理,转换成字符串,而对其他类型的数字不做处理。Jackson提供了这种支持,即对ObjectMapper进行定制。根据SpringBoot的官方帮助,找到一种相对简单的方法,只对ObjectMapper进行定制,而不是完全从头定制,方法如下:
@Bean("jackson2ObjectMapperBuilderCustomizer")
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
Jackson2ObjectMapperBuilderCustomizer customizer = new Jackson2ObjectMapperBuilderCustomizer() {
@Override
public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
jacksonObjectMapperBuilder.serializerByType(Long.class, ToStringSerializer.instance)
.serializerByType(Long.TYPE, ToStringSerializer.instance);
}
};
return customizer;
}
通过定义Jackson2ObjectMapperBuilderCustomizer
,对Jackson2ObjectMapperBuilder
对象进行定制,对Long
型数据进行了定制,使用ToStringSerializer
来进行序列化。问题终于完美解决。
四、使用HttpMessageConverter(建议方案)
关于HttpMessageConverter
HttpMessageConverter
接口提供了 5 个方法:
canRead
:判断该转换器是否能将请求内容转换成 Java 对象canWrite
:判断该转换器是否可以将 Java 对象转换成返回内容getSupportedMediaTypes
:获得该转换器支持的 MediaType 类型read
:读取请求内容并转换成 Java 对象-
write
:将 Java 对象转换后写入返回内容其中
read
和write
方法的参数分别有有HttpInputMessage
和HttpOutputMessage
对象,这两个对象分别代表着一次 Http 通讯中的请求和响应部分,可以通过getBody
方法获得对应的输入流和输出流。
当前 Spring 中已经默认提供了相当多的转换器,分别有:
名称 | 作用 | 读支持 MediaType | 写支持 MediaType |
---|---|---|---|
ByteArrayHttpMessageConverter | 数据与字节数组的相互转换 | */* | application/octet-stream |
StringHttpMessageConverter | 数据与 String 类型的相互转换 | text/* | text/plain |
FormHttpMessageConverter | 表单与 MultiValueMap的相互转换 | application/x-www-form-urlencoded | application/x-www-form-urlencoded |
SourceHttpMessageConverter | 数据与 javax.xml.transform.Source 的相互转换 | text/xml 和 application/xml | text/xml 和 application/xml |
MarshallingHttpMessageConverter | 使用 Spring 的 Marshaller/Unmarshaller 转换 XML 数据 | text/xml 和 application/xml | text/xml 和 application/xml |
MappingJackson2HttpMessageConverter | 使用 Jackson 的 ObjectMapper 转换 Json 数据 | application/json | application/json |
MappingJackson2XmlHttpMessageConverter | 使用 Jackson 的 XmlMapper 转换 XML 数据 | application/xml | application/xml |
BufferedImageHttpMessageConverter | 数据与 java.awt.image.BufferedImage 的相互转换 | Java I/O API 支持的所有类型 | Java I/O API 支持的所有类型 |
注意到AbstractMessageConverterMethodProcessor
类的getProducibleMediaTypes
、writeWithMessageConverters
等方法在每次消息解析转换都要作GenericHttpMessageConverter
分支判断,为什么呢?
package org.springframework.web.servlet.mvc.method.annotation;
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<MediaType>(mediaTypes);
}
else if (!this.allSupportedMediaTypes.isEmpty()) {
List<MediaType> result = new ArrayList<MediaType>();
for (HttpMessageConverter<?> converter : this.messageConverters) {
// 分支判断
if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
if (((GenericHttpMessageConverter<?>) converter).canWrite(declaredType, valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
return result;
}
else {
return Collections.singletonList(MediaType.ALL);
}
}
}
GenericHttpMessageConverter
接口继承自HttpMessageConverter
接口,用于提供支持泛型信息(java.lang.reflect.Type
)参数的canRead
/read
/canWrite
/write
方法。它的实现类为 AbstractGenericHttpMessageConverter
。
定制HttpMessageConverter
package io.prong.boot.framework;
import java.math.BigInteger;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.prong.boot.framework.json.CustomMappingJackson2HttpMessageConverter;
/**
* prong boot 自动配置
*
* @author tangyz
*
*/
@Configuration
public class ProngBootAutoConfig {
/**
* 解决前端js处理大数字丢失精度问题,将Long和BigInteger转换成string
*
* @return
*/
@Bean
@ConditionalOnMissingBean
public MappingJackson2HttpMessageConverter getMappingJackson2HttpMessageConverter() {
CustomMappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new CustomMappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
// 序列换成json时,将所有的long变成string 因为js中得数字类型不能包含所有的java long值
simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
jackson2HttpMessageConverter.setObjectMapper(objectMapper);
return jackson2HttpMessageConverter;
}
}
因为全局地对所有的long转string的粒度太粗了,我们需要对不同的接口进行区分,比如限定只对web前端的接口需要转换,但对于内部微服务之间的调用或者第三方接口等则不需要进行转换。CustomMappingJackson2HttpMessageConverter
的主要作用就是为了限定long转string的范围为web接口,即符合/web/xxxxx
风格的url(当然这个你需要根据自己产品的规范进行自定义)。
package io.prong.boot.framework.json;
import java.lang.reflect.Type;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 自定义的json转换器,匹配web api(以/web/开头的controller)中的接口方法的返回参数
*
* @author tangyz
*
*/
public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
private final static Logger logger = LoggerFactory.getLogger(CustomMappingJackson2HttpMessageConverter.class);
/**
* 判断该转换器是否能将请求内容转换成 Java 对象
*/
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
// 不需要反序列化
return false;
}
/**
* 判断该转换器是否能将请求内容转换成 Java 对象
*/
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
// 不需要反序列化
return false;
}
/**
* 判断该转换器是否可以将 Java 对象转换成返回内容.
* 匹配web api(形如/web/xxxx)中的接口方法的返回参数
*/
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
if (super.canWrite(clazz, mediaType)) {
ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (ra != null) { // web请求
HttpServletRequest request = ra.getRequest();
String uri = request.getRequestURI(); // 例如: "/web/frontApplicationPage"
logger.debug("Current uri is: {}", uri);
if (uri.startsWith("/web/")) {
return true;
}
}
}
return false;
}
}
我们的疑问来了,spring boot默认到底有多少个转换器?我们自定义的CustomMappingJackson2HttpMessageConverter
是覆盖了默认的MappingJackson2HttpMessageConverter
,还是两者并存?多个转换器之间的顺序是如何的?相互之间是否有影响?
下面我们来一一分析并回答。
查看spring的源码,首先我们找到了DelegatingWebMvcConfiguration
类,它的setConfigurers
方法将Spring容器中所有的WebMvcConfigurer
接口bean注入了方法的参数configurers
中。
package org.springframework.web.servlet.config.annotation;
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
/**
* 将Spring容器中所有的WebMvcConfigurer接口bean注入了参数configurers
*/
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
跟踪org.springframework.web.servlet.config.annotation.WebMvcConfigurerComposite
类的configureMessageConverters
方法,有以下WebMvcConfigurer
接口的9个代理(this.delegates
):
[0]io.prong.cloud.platform.config.SwaggerConfig
[1]org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration
[2]org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration
[3]org.springframework.cloud.netflix.metrics.MetricsInterceptorConfiguration$MetricsWebResourceConfiguration
[4]org.springframework.boot.actuate.endpoint.mvc.HeapdumpMvcEndpoint
[5]org.springframework.boot.actuate.endpoint.mvc.LogFileMvcEndpoint
[6]org.springframework.boot.actuate.endpoint.mvc.AuditEventsMvcEndpoint
[7]org.springframework.data.web.config.SpringDataWebConfiguration
[8]org.springframework.cloud.netflix.rx.RxJavaAutoConfiguration$RxJavaReturnValueHandlerConfig
当然,这个代理的数量是不确定的,跟你的工程以及所依赖组件里面包含的
WebMvcConfigurer
接口实现类的数量有关系。
目前这里面只有WebMvcAutoConfiguration
代理类覆盖了configureMessageConverters
方法并定义了spring boot默认的转换器,所以其他代理类的我们可以无视了。跟踪代码可以找到spring boot在WebMvcConfigurationSupport
类的addDefaultHttpMessageConverters
方法中对默认的转换器进行了定义。
跟踪org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration
的内部类WebMvcAutoConfigurationAdapter
类的configureMessageConverters(List<HttpMessageConverter<?>> converters)
方法,发现最终初始化的转换器顺序如下:
[0]org.springframework.http.converter.ByteArrayHttpMessageConverter
[1]org.springframework.http.converter.StringHttpMessageConverter // spring boot自定义的转换器
[2]org.springframework.http.converter.StringHttpMessageConverter
[3]org.springframework.http.converter.ResourceHttpMessageConverter
[4]org.springframework.http.converter.xml.SourceHttpMessageConverter
[5]org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
[6]io.prong.boot.framework.json.CustomMappingJackson2HttpMessageConverter // prong boot自定义的转换器
[7]org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
[8]org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter
那么我们定义的转换器是怎么加入进来的呢?
HttpMessageConvertersAutoConfiguration
类的构造函数,扫描spring容器并找到所有通过@bean方式定义的HttpMessageConverter
转换器:
package org.springframework.boot.autoconfigure.web;
public class HttpMessageConvertersAutoConfiguration {
static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper";
private final List<HttpMessageConverter<?>> converters;
public HttpMessageConvertersAutoConfiguration(
ObjectProvider<List<HttpMessageConverter<?>>> convertersProvider) {
// 找到容器里自定义的HttpMessageConverter实例
this.converters = convertersProvider.getIfAvailable();
}
这里面找到了2个:
[0]io.prong.boot.framework.json.CustomMappingJackson2HttpMessageConverter
[1]org.springframework.http.converter.StringHttpMessageConverter
接下来spring boot将自定义的转换器和默认的转换器进行合并:
package org.springframework.boot.autoconfigure.web;
public class HttpMessageConverters implements Iterable<HttpMessageConverter<?>> {
public HttpMessageConverters(boolean addDefaultConverters,
Collection<HttpMessageConverter<?>> converters) {
// 将自定义的转换器和默认的转换器进行合并
List<HttpMessageConverter<?>> combined = getCombinedConverters(converters,
addDefaultConverters ? getDefaultConverters()
: Collections.<HttpMessageConverter<?>>emptyList());
combined = postProcessConverters(combined);
this.converters = Collections.unmodifiableList(combined);
}
合并在方法getCombinedConverters
中进行,具体的算法大家可以看看源代码,我总结算法的主要核心如下:
1、比较自定义转换器类型是否为可以替换默认转换器的类型?
例如 CustomMappingJackson2HttpMessageConverter 是可以替换默认的 MappingJackson2HttpMessageConverter。
2、如果是,将自定义转换器放在默认转换器的前面。
因此,我们可以最终看到如上所述的,CustomMappingJackson2HttpMessageConverter
转换器的顺序排在了默认转换器MappingJackson2HttpMessageConverter
的前面。
注意,转换器是采用read、write分离的2条职责链的设计模式,一旦某个转换器的read/write可以处理请求,则退出职责链。
定义自己的Serializer
上面的MappingJackson2HttpMessageConverter
将所有的long
都转成了string
,对于有些例外的情况,例如前端antd
列表组件的总记录数为number
,java后端使用了pagehelper
分页组件,pagehelper
的Page
类返回的记录总数total
为long
型,如果转为string
给前端就会有问题,因此,我们通过自定义的Serializer来排除这种例外。
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class LongJsonSerializer extends JsonSerializer<Long> {
@Override
public void serialize(Long value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
if (value != null) {
jsonGenerator.writeNumber(value);
}
}
}
如何使用?
使用自定义的PageBean
类替换官方的PageInfo
,并在PageBean
类中使用:
@JsonSerialize(using = LongJsonSerializer.class)
private long total; // 总记录数
Recommend
-
48
作为一只前端狗,我们的使命就是在满足产品需求、实现交互设计的基础上,将最好的体验呈现给用户爸爸们。在保证性能的同时,我们通常会给页面加一些动态效果,以增强页面的表现力并提升页面的交互体验。故将前端实现动效的几种常用方式整理成此篇小结,以求温故而...
-
5
技术内参 | 数据分析,如何解决精度丢失的问题? 神策小秘书 标...
-
10
Long和List<Long>前端精度丢失解决办法锦集以及自定义JSON序列化方法因为JS解析整型的时候是有最大值的,Number.MAX_SAFE_INTEGER 常量表示在 JavaScript 中最大的安全整数(maxinum saf...
-
7
V2EX › 程序员 Spring Cloud Consul 注册丢失 jimmyismagic · 1 天前 · 357...
-
8
在 JavaScript 中浮点数运算时经常出现 0.1+0.2=0.30000000000000004 这样的问题,除此之外还有一个不容忽视的大数危机(大数处理精度丢失)问题。之前也分享过这个问题,我在做个梳理分享给大家,
-
6
关于Spring List 返回给前端后,顺序不对了 ...
-
3
雪花算法ID到前端之后精度丢失问题 Posted on 2021-08-18 下面我把异常的现象给大家...
-
6
如果你开发过涉及金额计算的 iOS app, 那么你很有可能经历过在使用浮点型数字时精度丢失的问题 让我们来...
-
5
Nicksxs's BlogWhat hurts more, the pain of hard work or the pain of regret?spring boot中的 http 接口返回 json 形式的小注意点...
-
3
javascript以64位双精度浮点数存储所有Number类型值,即计算机最多存储64位二进制数。 但是需要注意的是Number包含了我们常说的整形、浮点型,相比较于整形而言,会有一位存储小数点的偏移位,由于存储二进制时小数点的偏移量最大为52位,计算机存储的为二进制,而能...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK