项目上线一周后,正准备看新闻的我突然接到了一个任务。线上突然出现了一条乱码的数据,需要解决这个bug。于是我放下了手中的保温杯,开始解决这个bug。经过一番折腾,发现是有一个同事在处理IO流上写得有点问题,导致了乱码的产生。
一、问题的发现与分析
(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
}
三、小结
在将字节流内容转换成字符串时,特别要注意这种读取到半个中文的问题。