數據庫中有一個bigint類型數據,對應java后台類型為Long型,在某個查詢頁面中碰到了問題:頁面上顯示的數據和數據庫中的數據不一致。例如數據庫中存儲的是:1475797674679549851,顯示出來卻成了1475797674679550000,后面幾位全變成了0,精度丟失了。
1. 原因
這是因為Javascript中數字的精度是有限的,bigint類型的的數字超出了Javascript的處理范圍。JS 遵循 IEEE 754 規范,采用雙精度存儲(double precision),占用 64 bit。其結構如圖:
各位的含義如下:
- 1位(s) 用來表示符號位
- 11位(e) 用來表示指數
- 52位(f) 表示尾數
尾數位最大是 52 位,因此 JS 中能精准表示的最大整數是 Math.pow(2, 53),十進制即 9007199254740992。而Bigint類型的有效位數是63位(扣除一位符號位),其最大值為:Math.pow(2,63)。任何大於 9007199254740992 的就可能會丟失精度:
1 |
9007199254740992 >> 10000000000000...000 // 共計 53 個 0 |
實際上值卻是:
1 |
9007199254740992 + 1 // 丟失 |
2.解決方法
解決辦法就是讓Javascript把數字當成字符串進行處理。對Javascript來說,不進行運算,數字和字符串處理起來沒有什么區別。當然如果需要進行運算,只能采用其他方法,例如使用JavaScript的一些開源庫bignumber之類的處理了。Java進行JSON處理的時候是能夠正確處理long型的,只需要將數字轉化成字符串就可以了。例如:
1 |
{ |
變為:
1 |
{ |
這樣Javascript就可以按照字符串方式處理,不存在數字精度丟失了。在Springboot中處理方法基本上有以下幾種:
2.1 配置參數write_numbers_as_strings
Jackson有個配置參數WRITE_NUMBERS_AS_STRINGS,可以強制將所有數字全部轉成字符串輸出。其功能介紹為:Feature that forces all Java numbers to be written as JSON strings.。使用方法很簡單,只需要配置參數即可:
1 |
spring: |
這種方式的優點是使用方便,不需要調整代碼;缺點是顆粒度太大,所有的數字都被轉成字符串輸出了,包括按照timestamp格式輸出的時間也是如此。
2.2 注解
另一個方式是使用注解JsonSerialize:
1 |
|
指定了ToStringSerializer進行序列化,將數字編碼成字符串格式。這種方式的優點是顆粒度可以很精細;缺點同樣是太精細,如果需要調整的字段比較多會比較麻煩。
實現方法:
在dto所在項目中,新建一個helper包(名字自定義,也可以放現有包里)。PS:為什么要建到dto項目中?因為,這個包最后可能會給其他組使用,這樣以來,所有的處理規則邏輯都是統一的,方便對接。
在包里添加類LongJsonSerializer,代碼如下:
/**
* Long 類型字段序列化時轉為字符串,避免js丟失精度
*
*/
public class LongJsonSerializer extends JsonSerializer<Long> {
@Override
public void serialize(Long value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
String text = (value == null ? null : String.valueOf(value));
if (text != null) {
jsonGenerator.writeString(text);
}
}
}
然后在包里再添加類LongJsonDeserializer,代碼如下:
/**
* 將字符串轉為Long
*
*/
public class LongJsonDeserializer extends JsonDeserializer<Long> {
private static final Logger logger = LoggerFactory.getLogger(LongJsonDeserializer.class);
@Override
public Long deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
String value = jsonParser.getText();
try {
return value == null ? null : Long.parseLong(value);
} catch (NumberFormatException e) {
logger.error("解析長整形錯誤", e);
return null;
}
}
}
好了,接下來是使用這兩個類。在需要處理的id字段上,加上注解,比如如下代碼:
/** * id */ @JsonSerialize(using = LongJsonSerializer.class) @JsonDeserialize(using = LongJsonDeserializer.class) private Long id;
2.3 自定義ObjectMapper
最后想到可以單獨根據類型進行設置,只對Long型數據進行處理,轉換成字符串,而對其他類型的數字不做處理。Jackson提供了這種支持。方法是對ObjectMapper進行定制。根據SpringBoot的官方幫助(https://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html#howto-customize-the-jackson-objectmapper),找到一種相對簡單的方法,只對ObjectMapper進行定制,而不是完全從頭定制,方法如下:
1 |
|
通過定義Jackson2ObjectMapperBuilderCustomizer,對Jackson2ObjectMapperBuilder對象進行定制,對Long型數據進行了定制,使用ToStringSerializer來進行序列化。問題終於完美解決。

