跳到主要內容

搜尋酷無極

选择搜索范围,输入关键词后按回车。

語言 / Language

長得太長也是錯?——後端Long型ID精度遺失的奇妙修復之旅

引言

在前後端分離的時代,我們的生活充滿了無數的機遇與挑戰——包括那些突然冒出來的讓人抓狂的 Bug。今天我們要聊的,就是一個讓無數開發者哭笑不得的經典問題:後端 Long 類型 ID 過長導致前端精度遺失。說到這個問題,那可真是「萬惡之源」啊,誰讓 JavaScript 只能安全地處理 Number.MAX_SAFE_INTEGER(也就是 9007199254740991)以內的數值呢?

如果你曾經為了這個問題而冥思苦想,別擔心,你不是一個人。今天,我們不僅要用幽默的方式來剖析這個「世紀難題」,還要帶你從根源上解決它,讓你的程式碼不再「失精」。

問題背景:為什麼 Long 這麼長?

首先,我們得從 Long 型別說起。Long,就是 Java 中的 64 位元整數型別,對於喜歡處理大數據、大數字的 Java 來說,這個型別簡直就是福音。然而,前端世界卻有點「孤陋寡聞」,它只懂得處理 53 位元以內的整數。是的,你沒聽錯,在這點上 JavaScript 就像是一個「慢半拍」的老學究,面對更大的數字時,它就開始擺弄小數點和指數,最後吐出一個讓你感到絕望的數字。於是乎,精度遺失的問題就像幽靈一樣,開始在你的專案中遊蕩。

第一次相遇:精度遺失的那些事

故事得從某一天的 Bug 回報開始:「開發哥哥,你看這個 ID 怎麼變了樣?這不是我資料庫裡的那個 ID 啊!」你皺著眉頭一看,是的,9223372036854775807 變成了 9223372036854776000,哎呀,這多出來的數字簡直像是魔術一樣。明明後端給的是對的呀!你一邊抓頭一邊心想:「這肯定是前端的鍋!」

前端的鍋還是後端的鍋?——追根溯源

其實吧,這個問題甩鍋給前端也不是完全沒道理。讓我們來看看 JavaScript 在處理數字時的「弱項」。JavaScript 的 Number 類型是基於 IEEE 754 標準的雙精度浮點數格式,只能安全地表示 53 位二進位數字,也就是 Number.MAX_SAFE_INTEGER 的值——9007199254740991。

換句話說,超過這個範圍的整數,JavaScript 就會開始「精度打折」,它的「腦容量」突然就不夠用了。於是,你的 Long 類型 ID 就變成了它眼中的一堆沒那麼「性感」的數值。正因為如此,前端處理這些長得離譜的 ID 時,不得不「犧牲」一下,結果就是你那原本忠實的 ID 被截斷,變成了一個「有趣」的新數字。

從震驚到冷靜:尋找解決方案

既然問題的根源已經找到,那就輪到我們這些開發者來大顯身手了。接下來,我將帶你深入了解幾種解決方案,並告訴你每種方案的優缺點,畢竟條條大路通羅馬。

1. 直接轉成字串:簡單粗暴卻高效

面對這種問題,我們最先想到的肯定是最簡單粗暴的方法:直接把 Long 類型的資料轉換成字串不就好了嘛!既然 JavaScript 是浮點數腦殘粉,那我們乾脆把問題丟回去,告訴它:「你只需要當這是個字串,別擔心它有多長!」

如何做到這一點呢?其實很簡單,使用 Jackson 提供的 ToStringSerializer,我們可以輕鬆地把 Long 轉成字串。

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

public class UserDto {
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
    // 其他字段
}


這樣一來,當後端回傳 UserDto 的時候,Long 類型的 id 會被序列化為字串,前端接收到的也是字串,精度問題迎刃而解。簡直就像在 JavaScript 的腦袋上貼了一張便條:「這個是字串喔,不用你操心!」

2. 前端使用 BigInt:讓大數也能精確運算

當然,簡單粗暴的方法並不總是適合所有情境。想像一下,如果前端需要對這個 ID 進行某種數學運算,直接轉成字串可就不太好了。那麼,前端該怎麼處理這些「超長」的 ID 呢?

幸運的是,JavaScript 也不是一無是處。引入了 BigInt 之後,JavaScript 終於不再是那個只會擺弄小數點的呆子了。BigInt 是一種新的原始資料型別,專門用來處理任意精度的整數。你可以這樣做:

const id = BigInt("9223372036854775807");
console.log(id + 1n); // 输出:9223372036854775808n

這樣,你就可以在前端精確地處理 Long 類型的資料,避免精度遺失的問題。當然,這裡有一個小小的提醒:BigInt 並不是所有瀏覽器都支援的,所以你得確保你的應用環境能夠相容。

3. 自訂序列化:複雜問題簡單化

雖然上面的方法已經可以解決大多數問題,但有時候我們會遇到一些需要更細緻控制的情境。這時候,Jackson 的自訂序列化器就派上用場了。

我們可以編寫一個自訂的序列化器,根據需求靈活控制 Long 類型欄位的序列化過程。比如,我們可以在序列化時決定是否將 Long 轉換為字串,或者針對特定條件進行不同處理:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;

public class CustomLongSerializer extends JsonSerializer<Long> {

    @Override
    public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value != null) {
            gen.writeString(value.toString());
        }
    }
}


然後在需要的地方套用這個自訂序列化器:

import com.fasterxml.jackson.databind.annotation.JsonSerialize;

public class UserDto {
    @JsonSerialize(using = CustomLongSerializer.class)
    private Long id;
    // 其他字段
}


這樣,你就能掌控整個序列化過程,確保每一個 Long 都能按照你希望的方式被處理。當然,這種方法雖然靈活,但稍微複雜了一些,需要多寫幾行程式碼,也可能增加維護成本。

4. 全域處理:省時省力的方案

如果你希望以全域方式解決這個問題,省去在每個欄位上設定註解的麻煩,可以考慮全域設定 ObjectMapper。透過在 Spring Boot 中設定全域的 ObjectMapper,你可以讓所有的 Long 類型欄位都自動轉換為字串。

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.addSerializer(Long.class, ToStringSerializer.instance);
        module.addSerializer(Long.TYPE, ToStringSerializer.instance);
        mapper.registerModule(module);
        return mapper;
    }
}


一勞永逸,不用擔心忘記為某個欄位設定序列化器,整個專案都能享受到精度不遺失的快感。當然,使用全域設定的同時要注意,可能會影響到某些你不希望被轉換的 Long 欄位,因此需要審慎考慮。

實際案例:一波三折的 Bug 解決之路

為了讓大家更好地理解這些解決方案的實際效果,我來分享一個真實專案中的故事。這個專案涉及使用者 ID 的管理,由於使用者數量很大,後端採用了 Long 型別的唯一識別碼。專案上線沒幾天,就出現了使用者回報:「我的 ID 怎麼變了樣?」。

第一步:追查問題根源

我們首先檢查了前端程式碼,發現 JavaScript 的 Number 類型確實無法準確表示這個長達 19 位的數字,於是導致了精度遺失。接著,我們查看了後端程式碼,發現雖然 Long 類型的資料在後端是正確的,但在透過 REST API 回傳給前端時,數字的精度遺失了。

第二步:選擇合適的解決方案

為了快速解決問題,我們決定首先採用 @JsonSerialize(using = ToStringSerializer.class) 這個簡單有效的辦法。透過這個辦法,我們成功地避免了前端接收精度遺失的數字。

然而,問題並沒有完全解決。在後續的需求中,前端需要對 ID 進行某些運算,比如對使用者的 ID 進行排序。這時候,字串就顯得有些力不從心了。

第三步:使用 BigInt 解決前端運算問題

於是,我們決定在前端引入 BigInt。透過使用 BigInt,前端不僅能夠精確地儲存這些超長的 ID,還能進行必要的數學運算。經過測試,這種方法在各大主流瀏覽器上表現良好,唯一的缺點就是對一些舊版本瀏覽器的支援不太友善。

第四步:最終的全域設定

為了避免今後類似問題再次發生,我們決定將 ToStringSerializer 設定為全域生效。這讓所有的 Long 類型資料在序列化時都自動轉換為字串,既保證了前端的資料準確性,又減少了程式碼的重複設定。

結語:精度遺失的終結者

從這個案例中可以看到,雖然 Long 型別的精度遺失問題看似簡單,但在實際專案中可能帶來諸多隱患。透過多種解決方案的比較和嘗試,我們最終找到了適合自己專案的最佳方案。

希望這篇幽默而又詳盡的部落格文章,能讓你在處理 Long 型別精度遺失問題時,少走彎路。如果你也有類似的經歷,歡迎在留言區分享你的故事,也許你的經驗能為其他開發者帶來更多啟發。

最後,記住,程式碼如人生,偶爾的「遺失」並不可怕,關鍵是找到合適的「序列化器」讓它回歸正軌。祝願大家的程式碼再也不會「失精」,保持精準,一路通暢!


評論區

傳送中

選擇喜歡的樣式

所選樣式會套用至全站。