SpringBoot中使用Jackson導致Long型數據精度丟失問題


數據庫中有一個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
2
3
9007199254740992     >> 10000000000000...000 // 共計 53 個 0
9007199254740992 + 1 >> 10000000000000...001 // 中間 52 個 0
9007199254740992 + 2 >> 10000000000000...010 // 中間 51 個 0

實際上值卻是:

1
2
3
4
9007199254740992 + 1 // 丟失
9007199254740992 + 2 // 未丟失
9007199254740992 + 3 // 丟失
9007199254740992 + 4 // 未丟失

2.解決方法

解決辦法就是讓Javascript把數字當成字符串進行處理。對Javascript來說,不進行運算,數字和字符串處理起來沒有什么區別。當然如果需要進行運算,只能采用其他方法,例如使用JavaScript的一些開源庫bignumber之類的處理了。Java進行JSON處理的時候是能夠正確處理long型的,只需要將數字轉化成字符串就可以了。例如:

1
2
3
4
5
{
...
"bankcardHash": 1475797674679549851,
...
}

變為:

1
2
3
4
5
{
...
"bankcardHash": "1475797674679549851",
...
}

這樣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
2
3
4
spring:
jackson:
generator:
write_numbers_as_strings: true

這種方式的優點是使用方便,不需要調整代碼;缺點是顆粒度太大,所有的數字都被轉成字符串輸出了,包括按照timestamp格式輸出的時間也是如此。

2.2 注解

另一個方式是使用注解JsonSerialize

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

指定了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
2
3
4
5
6
7
8
9
10
11
@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來進行序列化。問題終於完美解決。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM