圖 by:石頭@阿里巴巴飛天園區web
關於做者:程序猿石頭(ID: tangleithu),現任阿里巴巴技術專家,清華學渣,前大疆後端 Leader。歡迎關注,交流和指導!回覆 「0」 送阿里技術大禮包。
背景
石頭同窗是某大公司高級開發工程師,某日收到很多錯誤告警信息,因而便去開始排查。面試
跟蹤日誌發現是某個服務拋出的異常信息,奇怪的是這個服務上線也有一段時間了。以前不多看到相似的錯誤信息,最近偶爾多了起來。算法
後來才定位到是由於服務調用了某外部接口,發現對方對參數長度作了限制,若是輸入參數超過 1000 bytes,就直接拋異常,代碼相似以下:後端
/**
* @param status
* @param result, the size should less than 1000 bytes
* @throws Exception
*/
public XXResult(boolean status, String result) {
if (result != null && result.getBytes().length > 1000) {
throw new RuntimeException("result size more than 1000 bytes!");
}
......
}
心想,這還不簡單,我們的 result
也不是什麼關鍵性的東西,你有限制,我直接 trim 一下不就好了?數組
解決方案
因而三下五除二,給搞了個 trim
方法,支持傳不一樣參數按需 trim,代碼以下:微信
/**
* 將給定的字符串 trim 到指定大小
* @param input
* @param trimTo 須要 trim 的字節長度
* @return trim 後的 String
*/
public static String trimAsByte(String input, int trimTo) {
if (Objects.isNull(input)) {
return null;
}
byte[] bytes = input.getBytes();
if (bytes.length > trimTo) {
byte [] subArray = Arrays.copyOfRange(bytes, 0, trimTo);
return new String(subArray);
}
return input;
}
再在須要調用外部服務的地方,先調用這個 trimAsByte
方法,一頓操做連忙上線,一切完美~app
災難現場
一切完美,石頭哥也是這樣認爲的。而後幸福老是短暫的。less
通過一段時間後(前面也提到,業務場景確實是偶發的),相同的錯誤仍然發生了。
簡直不敢相信,都 trim 了爲啥還會超出?你也幫忙想一想,是哪裏的問題?
看看上面的例子(爲了方便展現,簡單修改文首代碼了下),
trimAsByte("WeChat:tangleithu", 8)
輸入字符串 WeChat:tangleithu
太長了,只 trim 到剩下 8 個字節,對應的字節數組是從 [87,101,67,104,97,116,58,116,97,110,103,108,101,105,116,104,117]
變爲了 [87,101,67,104,97,116,58,116]
,字符串變成了 WeChat:t
,結果正確。
其實在寫這個方法的時候仍是太草率了,本應該很容易想到中文的狀況的,咱們來試試:
trimAsByte("程序猿石頭", 8)
看上述截圖,悲劇了,輸入程序猿石頭
,3 個字節一個漢字,一共 15 個字節 [-25,-88,-117,-27,-70,-113,-25,-116,-65,-25,-97,-77,-27,-92,-76]
,trim 到 8 位,剩下前 8 位 [-25,-88,-117,-27,-70,-113,-25,-116]
也正確。再 new String
,又變成3 個 「中文」 了,雖然第 3 個「中文」,咱也不認識,咱也不敢問到底讀啥,總之再轉換成字節數組,長度多了 1 個,變成 9 了。
問題算是定位到了。
不由要問,爲何?
來看看這個 String 的構造函數,看看上面註釋才發現,其實咱們忽略了一個很重要的概念,就是編碼方式。
/**
* Constructs a new {@code String} by decoding the specified array of bytes
* using the platform's default charset. The length of the new {@code
* String} is a function of the charset, and hence may not be equal to the
* length of the byte array.
*
* <p> The behavior of this constructor when the given bytes are not valid
* in the default charset is unspecified. The {@link
* java.nio.charset.CharsetDecoder} class should be used when more control
* over the decoding process is required.
*
* @param bytes
* The bytes to be decoded into characters
*
* @since JDK1.1
*/
public String(byte bytes[]) {
//this(bytes, 0, bytes.length);
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
}
當咱們用默認的構造函數 new String 的時候,只是用了系統默認的編碼(本文是「UTF-8」)去嘗試解碼,構造出字符串。
因此,當咱們在用字節數組(字節流)來表達具體的語義的時候,必定要約定好以什麼方式進行編碼,本文不具體闡述編碼問題了。下面用一個例子來解釋上文的現象:
[-25,-88,-117,-27,-70,-113,-25,-116,-65,-25,-97,-77,-27,-92,-76]
仍然用這串字節數組來實驗,這串字節數組,若是用 「UTF-8」 編碼去解釋,那麼其想表達的語義就是中文「程序猿石頭」,從上文標註的 1,2,3 中能夠看出來,沒有寫即用了系統中的默認編碼「UTF-8」。
假設按照 「GBK」 來解釋(標註 4),就是表達的 「紼嬪簭鐚跨煶澶�」,注意看下其中的 �
是否是似曾相識;
注意標註 5,經過 GBK 解釋構造字符串後,再經過默認的 「UTF-8」 獲取字節數組,長度就變成 24 了,而後還經過 「GBK」 編碼獲得的字節數組長度爲 15(標註 6),再試圖構造字符串(標註 7),其中「程序猿石頭」的「頭」字,已經沒了。說明這個轉換過程當中,其實信息已經被丟了。
上面的 �
實際上是 UNICODE 編碼方式中的一個特殊的字符,也就是 0xFFFD(65535),實際上是一個佔位符(REPLACEMENT CHARACTER),用來表達未知的、沒辦法表達的東東。上文中在進行編碼轉換過程當中,出現了這個玩意,其實也就是沒辦法準確表達含義,會被替換成這個東西,所以信息也就丟失了。你能夠試試前面的例子,好比把前 8 個字節中的最後一兩個字節隨便改改,都是同樣的。
總結
總結一下,其實原本是一個很簡單的問題,卻通過幾回修改才最終解決,說明對 「基礎」 掌握得仍是不夠,一個重要的點是,在處理二進制數據的時候,必定要聯想到 「編碼」 方式。
另外,提醒咱們,看似簡單的問題,咱們每每容易忽略。好比若是單純看到文中提到的這個trim
方法,其實很容易寫個單元測試就能儘早發現有問題;
越是基礎的方法,咱們越應該考慮其代碼的健壯性,在以前的 從一道面試題談談一線大廠碼農應該具有的基本能力 中,我也談到了寫單元測試、測試用例的重要性。
後記
以爲本號分享的文章有價值,記得添加星標哦。周更很累,不要白 piao,須要來點正反饋,安排個 「一鍵三連」(點贊、在看、分享)如何?😝 這將是我持續輸出優質文章的最強動力。

程序猿石頭
程序猿石頭(ID: tangleithu),現任阿里巴巴技術專家,清華學渣,前大疆後端 Leader。用不一樣的視角分享高質量技術文章,以每篇文章都讓人有收穫爲目的,歡迎關注,交流和指導!掃碼回覆關鍵字 「1024」 獲取程序員大廠面試指南。
本文分享自微信公衆號 - 程序猿石頭(tangleithu)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。