前端長整型主鍵“失蹤”記
——一次
ArrayIndexOutOfBoundsException
的排查全過程
一、事故現場
最近在維護 SMS-OFFICE 后臺系統時,運維同事反饋:
點擊「短信詳情」或「郵箱賬號詳情」時,偶爾彈窗空白、日志報錯:
java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
抓了一條完整鏈路,發現請求 URL 是:
/secured/lookSmsMessage.html?contentId=7205759403792883606
而數據庫中明明存在這條記錄!為什么 SELECT … WHERE content_id = ?
一條都查不到?
二、線索一:前端 JSON 對象異常
打開瀏覽器 DevTools ? Network ? Preview,注意到 DataTables 接口返回:
{"contentId": 7205759403792883606,…
}
字段是 裸數字,但和數據庫里的大整數完全一致。那問題出在哪里?
三、線索二:JS Number 的“安全整數”
JavaScript 只有一種數值類型 Number
,實現為 IEEE-754 雙精度浮點。
最大安全整數 為
Number.MAX_SAFE_INTEGER === 9_007_199_254_740_991
任何大于此值的整數,低位全部失真。
主鍵 | 十進制 | 是否 > 9e15 |
---|---|---|
720 575 940 379 288 3606 | ≈ 7.2 × 101? | ? |
288 230 376 151 801 751 | ≈ 2.88 × 101? | ? |
在控制臺驗證一下:
JSON.stringify(7205759403792883606) // "7205759403792884000"
瀏覽器早在解析 JSON 時,就把它“改寫”成了一個近似值 ——
后端再跟這個錯誤 ID 去查數據庫,當然一條也沒有。
四、導致的連鎖反應
-
DataTables 渲染
列內contentId
被當成 number 存進rowData
,精度丟失。 -
按鈕拼接 URL
onclick="lookData(' + rdata.contentId + ')"
結果
rdata.contentId
已經變成錯誤值。 -
后端查詢為空 ?
list.get(0)
拋IndexOutOfBoundsException
.
五、最終定位:純前端精度問題
-
數據庫:MySQL / TiDB 的 BIGINT,范圍 ±9 × 101?,沒問題。
-
后端:Java
long
同樣能裝下。 -
真正掉鏈子 的是瀏覽器的
Number
精度。
六、修復方案
1. 讓主鍵永遠當「字符串」
- onclick="lookData(' + rdata.contentId + ')"
+ onclick="lookData(\'' + rdata.contentId + '\')"
-
只要在拼接時加一對 引號,JS 就會把它當作字符串傳遞。
-
后端 Spring MVC 可以自動把字符串轉
Long
;
如果你愿意,也可以把參數類型改成String
,然后Long.valueOf()
。
2. Mapper / ResultMap 調整
<result column="content_id" property="contentId" jdbcType="VARCHAR"/>
3. 防御式編碼
if (list.isEmpty()) {throw new NotFoundException("記錄不存在");
}
4. 全鏈路自檢腳本
// Chrome DevTools 快速檢測 big int
function hasUnsafeId(json, key='contentId'){return json.some(r => Math.abs(r[key]) > Number.MAX_SAFE_INTEGER);
}
七、最佳實踐小結
層 | 建議 |
---|---|
前端 | 所有主鍵字段一律用字符串;不要對大整數做數學運算。 |
后端 | 接口、DTO、Mapper 保持與前端一致的 String/Long;空列表先判空。 |
數據庫 | 如需水平分片可繼續用 AUTO_RANDOM ;只是傳輸層別當數值。 |
日志 | 記錄原始請求參數,便于比對是否被截斷。 |
八、一句話總結
當你在前端看到 18 位以上的主鍵時,第一反應:“加引號!”
避免 JS Number 精度地雷,你的分頁、詳情、批量操作都會更穩。