您现在的位置是:首页 >技术教程 >从MultipartFile上传文件名乱码开始详解编解码知识网站首页技术教程
从MultipartFile上传文件名乱码开始详解编解码知识
本章说
本篇文章开写的出发点是在解决“使用IDEA的HttpClient插件进行Multipart格式提交时,后端解析出的MultipartFile中的文件名中文乱码问题”时,对编解码知识的一些重新理解与认识。如果对前后端编解码流程,以及如何解决MultipartFile中文乱码有兴趣,不妨继续读下去
问题的产生
在使用IDEA自带的HttpClient插件测试后端上传文件接口时,我们编写了如下的http测试代码:
### 上传文件
POST {{media_host}}/media/upload/coursefile
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="filedata"; filename="测试图片1.png"
Content-Type: application/octet-stream;charset=utf-8
< E:/Users/zhuhj/tmp/测试图片1.png
后端接口方法定义如下:
@ApiOperation("上传图片")
@RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultDto upload(@RequestPart("filedata")MultipartFile filedata) throws IOException{
...
System.out.println(filedata.getOriginalFilename())
return ...;
}
经测试发现,中文“测试图片”四字变成了:“????”
分析问题
首先,我们知道乱码的本质是编码与解码的不匹配,第一个问题,编码在做什么?解码在做什么?
以一个简单的例子作为引入,小明规定0000表示A,0001表示B;小红规定0000表示C,0001表示D。因此,小明(客户端)将字符串“AB”处理为字节数组byte[] bytes = {0000, 0001};小红(服务端)接收到字节数组,并查找到对应的解码字典,得到字符串“CD”,乱码产生
上述过程可以通过以下的demo简单的发现:
String s = "测试图片";
byte[] isoBytes = s.getBytes(StandardCharsets.UTF_8);
System.out.println(Arrays.toString(isoBytes));
//output: [-26, -75, -117, -24, -81, -107, -27, -101, -66, -25, -119, -121]
System.out.println(new String(isoBytes, StandardCharsets.ISO_8859_1));
//output: 测试图片
那么再来分析我们的问题,在传递过程中,我们的乱码对应格式是:“测试图片”–>“????”,因此可断言是编码流使用了ISO_8859_1格式,导致中文字符转成字节数组时发生了截断(如果是解码流使用ISO_8859_1格式,乱码格式应如上述demo的输出所示)
那么在前后端传输过程中,可能发生编码流问题的位置有哪些:
- 前端将请求转为字节流时
- 后端将回复转为字节流时
SpringBoot默认已为我们处理好response的UTF-8编码,因此问题并非发生在这里。接下来应着重排查前端将请求转为字节流的过程,即HttpClient的问题
寻找解决方案
在多方尝试未果后,我们大胆推测:
HTTP协议并未给文件上传(即Content-Type为Multipart/form-data格式)提供修改charset为uft-8的功能。因此,使用Multipart/form-data自带的filename无法解决中文乱码问题,必须考虑以参数形式传入中文文件名
下面是我尝试过的各种解决方案:
最开始,我们怀疑是HttpClient插件的实现例中,默认采用了ISO-8859-1编码,因此,我们采取了重写HttpClient实现例,并手动指定MultiPart编码格式的方案1( × imes ×),重写实现例的代码如下,直接复制后放到一个新的java文件下即可执行:
public class FileUploadTest {
/**
* 这个例子展示了如何执行请求包含一个多部分编码的实体 模拟表单提交
*
* @throws IOException
*/
public static void main(String[] args) throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
try {
// TODO 要上传的文件的路径
String filePath = new String("E:/Users/zhuhj/tmp/测试图片1.png");
// TODO 把一个普通参数和文件上传给下面这个地址 是一个servlet
HttpPost httpPost = new HttpPost(
"http://localhost:63050/media/upload/coursefile");
//声明httpClient和builder
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
RequestConfig config = RequestConfig.custom().setConnectTimeout(1 * 1000).build();
httpClientBuilder.setDefaultRequestConfig(config);
httpClient = httpClientBuilder.build();
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
//配置待上传文件-->part
builder.setCharset(Charset.forName("UTF-8"));
builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);//设置浏览器兼容模式,兼容模式下,后端解析器会改变默认编码
ContentType contentType = ContentType.create("multipart/form-data",Charset.forName("UTF-8"));//重新设置UTF-8编码,默认编码是ISO-8859-1
File file = new File(filePath);
builder.addBinaryBody("filedata", new FileInputStream(file), contentType, "测试图片1.png");
HttpEntity fileEntity = builder.build();
httpPost.setEntity(fileEntity);
System.out.println("发起请求的页面地址 " + httpPost.getRequestLine());
// 发起请求 并返回请求的响应
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
System.out.println("----------------------------------------");
// 打印响应状态
System.out.println(response.getStatusLine());
// 获取响应对象
HttpEntity resEntity = response.getEntity();
if (resEntity != null) {
// 打印响应长度
System.out.println("Response content length: "
+ resEntity.getContentLength());
// 打印响应内容
System.out.println(EntityUtils.toString(resEntity,
Charset.forName("UTF-8")));
}
// 销毁
EntityUtils.consume(resEntity);
} finally {
response.close();
}
} finally {
httpClient.close();
}
}
}
该代码的maven依赖如下:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.12</version>
</dependency>
经测试,后端接收到的“测试图片”四字仍然是“????”
这与我们的分析不符合,我们进一步考虑,是否是SpringBoot提供的解析Multipart/form-data上传的MultiPartResolver
内部还有一系列使用ISO-8859-1的编码流过程?
经查验,SpringBoot提供了两个MultiPartResolver实现类,分别是:
- CommonsMultipartResolver
- StandardServletMultipartResolver
SpringBoot默认采用2,因为该方法相比于1,不需要额外引入依赖。进入2源代码,其并未提供修改默认编码的接口,根据SpringBoot对response的UTF-8编码处理,我们大胆猜测其应该也是使用的默认编码UTF-8
当然,以上只是猜测,因此我们可以排除SpringBoot对2的默认依赖,而重写并注入以1为实现类的bean,并通过1提供的编码修改接口修改编码为UTF-8
首先是排除SpringBoot对2的默认依赖,修改yaml文件:
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
写一个配置类,仅返回注入的bean:
@Configuration
public class MultiPartConfig {
@Bean(name = "multipartResolver")
public MultipartResolver multipartResolver() {
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
resolver.setDefaultEncoding("UTF-8");//设置默认编码
return resolver;
}
}
注意将该配置类扫描进SpringBoot启动类,开始方案2的测试( × imes ×)
方案2仍然没有改变“测试图片”–>“????”的局面,在排除了上述两个情况后,我们只能暂时推测:
HTTP协议并未给文件上传(即Content-Type为Multipart/form-data格式)提供修改charset为uft-8的功能。因此,使用Multipart/form-data自带的filename无法解决中文乱码问题,必须考虑以参数形式传入中文文件名