前言

在Java项目中Long是很常用的类型,通常用来做ID字段,近期的分布式雪花ID也是可以使用Long类型的,今天来看一下Long类型在项目中的坑。
最近在项目中偶然看到所有Long类型的字段返回前端之后都变成了字符串,导致前端需要进行一次转换才能使用。当时很不解,后来找到了项目中之前架构师写的转换代码,所有的Long类型和BigInteger都转换成String返回给前端,当时就蒙了,满脑子都是问号??????为啥?

为什么要转换?

这个要聊到Long类型的最大承受数值,众所周知long 数据类型是 64 位、有符号的以二进制补码表示的整数 ,取值范围是263-2^{63}~26312^{63}-1
我们可以使用以下代码查看Long类型的最大和最小值

1
2
System.out.println(Long.MIN_VALUE); // -9223372036854775808  最小数值
System.out.println(Long.MAX_VALUE); // 9223372036854775807 最大数值

现在我们需要了解以下前端数字类型能承受的最大数值了。
在我们小学二年级学到过,JavaScript中的数字都是Number类型,所有数字都是采用IEEE 754 标准定义的双精度64位格式存储,即使整数也是如此这就是说,JavaScript 语言的底层根本没有整数,所有数字都是小数(64位浮点数)。其结构如图:
JavaScript Number结构图
各位的含义如下:

  • 1位(s) 用来表示符号位,0表示正数,1表示负数
  • 11位(e) 用来表示指数部分
  • 52位(f) 表示小数部分(即有效数字)

双精度浮点数(double)并不是能够精确表示范围内的所有数, 虽然双精度浮点型的范围看上去很大: 2.23×103082.23\times10^{-308} ~ 1.79×103081.79\times10^{308}。 可以表示的最大整数可以很大,但能够精确表示、使用算数运算的并没有这么大。因为小数部分最大是 52位,因此 JavaScript 中能精准表示的最大整数是 25312^{53}-1,十进制为 9007199254740991。所以JavaScript中存在一个概念安全整数取值范围为 253-2^{53} ~ 2532^{53} 。而超过这个范围,会有两个或更多整数的双精度表示是相同的;即超过这个范围,有的整数是无法精确表示的,只能大约(round)到与它相近的浮点数(说到底就是科学计数法)表示,这种情况下叫做不安全整数,例如:

1
2
3
4
5
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类型的数值范围为263-2^{63}~26312^{63}-1
  • JavaScript中的Number类型安全整数范围为 253-2^{53} ~ 2532^{53} ,超过这个范围数字将可能会丢失。
  • 如果不做处理,当Long类型数字超过Number的安全整数范围后,数字可能会变化,导致跟原数据不一样。

有什么解决方法呢?

注解

1
2
@JsonSerialize(using=ToStringSerializer.class)
private Long bankcardHash;

使用HttpMessageConverter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* WEB 初始化相关配置
*/
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
ObjectMapper objectMapper = builder.build();
SimpleModule simpleModule = new SimpleModule();
// Long转换为String传输
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
// 忽略 transient 修饰的属性
objectMapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 设置为中国上海时区
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
converters.add(new MappingJackson2HttpMessageConverter(objectMapper));
converters.add(new StringHttpMessageConverter(Charsets.UTF_8));
converters.add(new ByteArrayHttpMessageConverter());
converters.add(new ResourceHttpMessageConverter());
converters.add(new ResourceRegionHttpMessageConverter());
super.configureMessageConverters(converters);
}

// 资源目录
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截器
}

//支持跨域
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedHeaders("*")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3600);
}
}

总结

因为Java中Long能承受的整数范围比JavaScript Number类型的安全整数范围大,所以可能会导致数字不准确。