一、問題的發現與分析
(1)發現
針對這個亂碼問題,我腦海中閃過了3種會導致亂碼產生的情景。
- [1] 數據庫表里面字符集設置錯誤
- [2] 由於未加編碼過濾器導致SpringMVC接收參數時造成的亂碼
- [3] 代碼中涉及byte數組轉換String時出現了問題
經過一序列的排查,發現不存在
[1] [2]
的問題,應該是
[3]
這種場景出現了問題。
經過仔細閱讀代碼,發現了一個InputStream流轉成String字符串的代碼有bug,會導致出現亂碼。代碼如下圖
防止圖片失效,代碼也貼上
/**
* 將流中的內容轉換為字符串,主要用於提取request請求的中requestBody
* @param in
* @param encoding
* @return
*/
public static String streamToString(InputStream in, String encoding){
// 將流轉換為字符串
try {
StringBuffer sb = new StringBuffer();
byte[] b = new byte[1024];
for (int n; (n = in.read(b)) != -1;) {
sb.append(new String(b, 0, n, encoding));
}
return sb.toString();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("提取 requestBody 異常", e);
}
}
20
1
/**
2
* 將流中的內容轉換為字符串,主要用於提取request請求的中requestBody
3
* @param in
4
* @param encoding
5
* @return
6
*/
7
public static String streamToString(InputStream in, String encoding){
8
// 將流轉換為字符串
9
try {
10
StringBuffer sb = new StringBuffer();
11
byte[] b = new byte[1024];
12
for (int n; (n = in.read(b)) != -1;) {
13
sb.append(new String(b, 0, n, encoding));
14
}
15
return sb.toString();
16
} catch (IOException e) {
17
e.printStackTrace();
18
throw new RuntimeException("提取 requestBody 異常", e);
19
}
20
}
(2)分析
這段代碼是一個字節流讀取內容,然后轉換成String的過程。仔細觀察他這段代碼,發現將流的內容讀取進來是采用小數組的方式。小數組讀取的方式本身沒什么問題,但是下面的這個new String這個代碼就有大問題了。java中utf-8編碼的中文是占3個字節。如果剛好有一個中文"我"字處於流中的位置為第1023-1025字節,那么采用小數組方式第一次讀取時只讀到了這個"我"字的2/3,把這2/3轉成String時就產生了亂碼。
因此,根本原因是用小數組方式會出現讀到半個中文,然后把這個半個中文轉成String就會亂碼。要解決這個問題,只需要將所有數據都讀進來,最后再轉換成String即可。
二、問題的解決
經過上面的分析,我們知道如果要保證不出現亂碼則必須將流數據全部讀取完畢再轉換成String。為了實現這個功能,那這個byte小數組怎么合並呢?一次性全部讀進來感覺也不是很好的方案。這時候輪到內存輸出流ByteArrayOutputStream登場了。具體的直接看下面代碼
/**
* 將流中的內容轉換為字符串,主要用於提取request請求的中requestBody
* @param in
* @param encoding
* @return
*/
public static String streamToString(InputStream in, String encoding){
// 將流轉換為字符串
ByteArrayOutputStream bos = null;
try {
// 1.創建內存輸出流,將讀到的數據寫到內存輸出流中
bos = new ByteArrayOutputStream();
// 2.創建字節數組
byte[] arr = new byte[1024];
int len;
while(-1 != (len = in.read(arr))) {
bos.write(arr, 0, len);
}
// 3.將內存輸出流的數據全部轉換為字符串
return bos.toString(encoding);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("提取 requestBody 異常", e);
} finally {
if(null != bos) {
try {
// 其實這個內存輸出流可關可不關,因為它的close方法里面沒做任何操作。
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1
/**
2
* 將流中的內容轉換為字符串,主要用於提取request請求的中requestBody
3
* @param in
4
* @param encoding
5
* @return
6
*/
7
public static String streamToString(InputStream in, String encoding){
8
// 將流轉換為字符串
9
ByteArrayOutputStream bos = null;
10
try {
11
// 1.創建內存輸出流,將讀到的數據寫到內存輸出流中
12
bos = new ByteArrayOutputStream();
13
// 2.創建字節數組
14
byte[] arr = new byte[1024];
15
int len;
16
while(-1 != (len = in.read(arr))) {
17
bos.write(arr, 0, len);
18
}
19
// 3.將內存輸出流的數據全部轉換為字符串
20
return bos.toString(encoding);
21
} catch (IOException e) {
22
e.printStackTrace();
23
throw new RuntimeException("提取 requestBody 異常", e);
24
} finally {
25
if(null != bos) {
26
try {
27
// 其實這個內存輸出流可關可不關,因為它的close方法里面沒做任何操作。
28
bos.close();
29
} catch (IOException e) {
30
e.printStackTrace();
31
}
32
}
33
}
34
}
三、小結
在將字節流內容轉換成字符串時,特別要注意這種讀取到半個中文的問題。
