開發一個查詢功能時,遇到了一個ORM的問題:數據庫字段是 Blob 類型,里面實際存儲的是文本數據,Java 后端代碼中用字符串 String 類型去接收這個字段的數據時,報錯,提示沒有對應的setter方法,類型不匹配;換成 byte[] 字節數組類型去接收這個字段的數據,依然報錯,同樣是找不到setter方法,類型不匹配;最后只好將Java中對應的變量類型改為 java.sql.Blob 類型去接收對應的數據,不報錯了,但如何取獲取其中的文本數據呢?
使用的代碼如下:
private String getTextFromBlob(Blob blob) { int i = 1; byte btArr[] = new byte[0]; try { while (i < blob.length()) { byte[] bytes = blob.getBytes(i, 1024); btArr = ArrayUtils.addAll(btArr, bytes); i += 1024; } return new String(btArr, "GB2312"); } catch (Exception e) { logger.error(e.getMessage(), e); return null; } }
代碼邏輯並不復雜,但其實這地方有一個坑,需要注意一下。
最開始的時候寫的代碼並不是這樣,我在 while 循環里每次拿 1024 個字節的數據,然后使用 new String(bytes,"GB2312") 得到字符串,再用 StringBuilder 把每次循環得到的字符串拼接起來,最后 stringBuilder.toString() 返回完整的字符串內容。
寫完之后,測試,沒問題。但緊接着,我想到了一個問題:我每次拿1024個字節,會不會正好把組成一個漢字的兩個字節拆分開呢?
答案是肯定的。雖然 GB2312 編碼字符集固定使用 2 個字節來存儲漢字,但是 GB2312字符集在存儲 ASCII 字符的時候,用的是 1 個字節來存儲。也就是說,對於英文字母、數字、英文標點,GB2312 用一個字節存儲;對於中文,則使用兩個字節存儲。這樣的話就沒法保證每次拿1024個字節不會把某個漢字的兩個字節拆分成兩段。
因為數據庫中的文字都比較短,沒有超過一百個字的,程序每次拿1024個字節就把所有的內容都拿完了,所以在測試中返回的文本都是正常的。改了一下代碼,把 1024 改成 10,每次拿10個字節,果然出現了亂碼問題,文本中的部分中文出現了亂碼,而其他部分的中文是正常的。
最后改成了上面的代碼,每次依然拿固定長度的字節,然后把結果都放到一個 byte[] 字節數組里,等拿完所有的字節之后,使用 new String(bytes,"GB2312") 得到字符串,這樣就避免了上面的問題。實際測試之后(每次拿10個字節),返回的文本正常,沒有亂碼。
當然這個代碼並不完美,代碼里使用 apache collections 包里的 ArrayUtils.addAll(byte[] b,byte[] c) 方法來把兩個字節數據拼到一塊,其內部的實現方法就是創建一個大數組,然后把兩個數據的內容依次放進去,這樣的話每次都要開辟一個新的內存空間,效率並不高,如果數據量大的話,會有很大的性能開銷。
一個比較好的解決方案就是:自己定義一個大數組,每次循環把取到的內容放到這個大數組對應的位置上,避免每次都要 new 一個數組出來,性能更好。缺點就是代碼邏輯會復雜一些。
總結:
在對字節流進行讀取、拆分的時候,需要注意會不會把表示一個字符的幾個字節給誤拆分了,這樣最后得到的內容會有部分亂碼。像常見的GB2312、UTF-8、UTF-16等都是變長的方式進行字節存儲,不能進行拆分;而像 UCS-2 這樣的字符集,固定使用兩個字節存儲,按偶數進行拆分就沒問題。