最近为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; // 总记录数