您现在的位置是:首页 >技术教程 >从MultipartFile上传文件名乱码开始详解编解码知识网站首页技术教程

从MultipartFile上传文件名乱码开始详解编解码知识

阿银的万事屋 2023-06-22 00:00:03
简介从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实现类,分别是:

  1. CommonsMultipartResolver
  2. 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无法解决中文乱码问题,必须考虑以参数形式传入中文文件名

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。