文件上传、解析及存储(前端+后端)

圣旨(金科玉律)

前端人员接旨:
      1. 提交方式必须是:post
      2. 文件上传的表单项必须是:<input type="file"/>
      3. 表单类型必须是:enctype="multipart/form-data"
后端人员接旨:
	  1. 文件上传接口如果走auth网关,则必须配auth网关白名单
 	  2. 最好指定文件在服务器的存储路径
	  3. 文件上传超时可考虑异步上传
	  4. 若涉及到文件存储,可考虑使用OSS(注意:文件的存储时长,存储的目录);可考虑将下载链接落库

一. 简介

早在1995年之前,HTTP的POST请求是不支持上传文件的,但随着《RFC 1867 -Form-based File Upload in HTML》的问世,开启了文件上传的新篇章,MIME(Multipurpose Internet Mail Extensions)新增了multipart/form-data类型,Content-Type HTTP Header就可以选择此类型,即通过“Content-Type: multipart/form-data”来支持上传文件,Spring MVC会通过Content-Type的MIME类型来判断是否为文件上传请求,再对文件的数据进行解析,解析好的文件会被存储到服务器,Spring Boot再从服务器获取文件,根据业务需求再对文件中的数据进行处理。

二. 文件是怎么被上传的

在这里插入图片描述
前端

  1. 同时可提交两个文件的HTML表单

    <form action="http://www.baidu.com/" method="post" enctype="multipart/form-data">
        <input type="file" name="file1" value="请选择文件1"/><br/>
        <input type="file" name="file2" value="请选择文件2"/><br/>
        <input type="submit"/>
    </form>
    
    金科玉律:
    1. 提交方式必须是:post
    2. 文件上传的表单项必须是:<input type="file"/>
    3. 表单类型必须是:enctype="multipart/form-data"
    
  2. 创建两个文件

    文件名:file1.xlsx     文件内存储的内容:hello
    文件名:file2.xlsx     文件内存储的内容:world
    
  3. 选择file1.xlsx和file2.xlsx两个文件并提交

    给浏览器传递如下内容:
    POST http://www.baidu.com/ HTTP/1.1
    Host: www.baidu.com
    Content-Length: 495
    Content-Type: multipart/form-data; boundary=---------------------------7db2d1bcc50e6e
    
    -----------------------------7db2d1bcc50e6e
    Content-Disposition: form-data; name="file1"; filename="D:\file1.xlsx"
    Content-Type: text/plain
    
    hello
    -----------------------------7db2d1bcc50e6e
    Content-Disposition: form-data; name="file2"; filename="D:\file2.xlsx"
    Content-Type: text/plain
    
    world
    -----------------------------7db2d1bcc50e6e--
    
    解释:
    1. HTTP头:第一个空行之前,即第2~5行为HTTP头
       1.1 Content-Length:消息实体的传输长度(不是消息实体的长度,如:zip压缩的文件,消息实体的长度是压缩
           前的长度,消息实体的传输长度为压缩后的长度)。
       1.2 Content-Type:上传的附件
       1.3 boundary:分隔符
    2. Body:第一个空行之后,即第7~17行为Body
       2.1 -----------------------------7db2d1bcc50e6e:每个文件以分隔符开始和结束
       2.2 -----------------------------7db2d1bcc50e6e--:结束符=分隔符--,表示最后一个文件
       2.3 Content-Disposition:附件的基本信息
    

Spring MVC

服务启动后,Spring MVC的Servlet容器会初始化DispatcherServlet,而DispatcherServlet初始化的同时,又会初始化MultipartResolver,用来解析文件数据。

  1. MultipartResolver的isMultipart()方法会判断Content-Type的MIME的前缀是否为“multipart/”,是则为文件上传请求。

    public boolean isMultipart(HttpServletRequest request) {
    	return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
    }
    
  2. 如果是文件上传的请求,MultipartResolver的resolveMultipart()方法会把文件解析的工作交给StandardMultipartHttpServletRequest对象。

    public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
    	return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
    }
    
  3. StandardMultipartHttpServletRequest对象对数据进行解析。解析后得到的是一组MultipartFile,每个MultipartFile对应文件上传数据中的一个part,并且存储于服务器的/tmp临时目录或自己指定的目录。

    private void parseRequest(HttpServletRequest request) {
         try {
             Collection<Part> parts = request.getParts();
             this.multipartParameterNames = new LinkedHashSet<>(parts.size());
             MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
             for (Part part : parts) {
                 // 获取part的Content-Disposition信息
                 String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
                 ContentDisposition disposition = ContentDisposition.parse(headerValue);
                 // 获取文件名
                 String filename = disposition.getFilename();
                 if (filename != null) {
                     if (filename.startsWith("=?") && filename.endsWith("?=")) {
                         filename = MimeDelegate.decode(filename);
                     }
                     files.add(part.getName(), new StandardMultipartFile(part, filename));
                 }
                 else {
                     this.multipartParameterNames.add(part.getName());
                 }
             }
             setMultipartFiles(files);
         } catch (Throwable ex) {
             handleParseFailure(ex);
         }
    }
    

服务器

解析好的数据存储到服务器的临时目录或自己指定的目录。

Spring Boot

控制器处理单个文件代码如下:

@RestController
@RequestMapping("/upload")
public class UploadController {
    @PostMapping("/file")
    public void file(@RequestParam(value = "file") MultipartFile multipartFile) throws IOException {
        ExcelReadModel<FileCmd> excelResult = ExcelUtils.read(multipartFile.getInputStream(), FileCmd.class);
        // 获取文件名。如得到的文件名为:fiel1.xlsx
        String fileName = multipartFile.getOriginalFilename();
        ........
    }
}

处理多个文件也是类似的,修改形参为List即可,如:@ModelAttribute List fileList。虽然我们知道通过@RequestParam(value = “file”)可以获取文件的数据,但其中又是如何保证获取到的数据是所需的数据呢?其实Spring MVC通过RequestParamMethodArgumentResolver对@RequestParam中的参数进行了精准的解析,从而保证了“所供即所需”。

    @Nullable
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
        HttpServletRequest servletRequest = (HttpServletRequest)request.getNativeRequest(HttpServletRequest.class);
        Object arg;
        if (servletRequest != null) {
            arg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
            if (arg != MultipartResolutionDelegate.UNRESOLVABLE) {
                return arg;
            }
        }

        arg = null;
        MultipartRequest multipartRequest = (MultipartRequest)request.getNativeRequest(MultipartRequest.class);
        if (multipartRequest != null) {
            List<MultipartFile> files = multipartRequest.getFiles(name);
            if (!files.isEmpty()) {
                arg = files.size() == 1 ? files.get(0) : files;
            }
        }

        if (arg == null) {
            String[] paramValues = request.getParameterValues(name);
            if (paramValues != null) {
                arg = paramValues.length == 1 ? paramValues[0] : paramValues;
            }
        }

        return arg;
    }

三. 上传文件遇到过的坑

  1. 文件在服务器的存储目录找不到了,500报错???
    在这里插入图片描述
    那是一个睡意沉沉的清晨,准备上传一个包含了几个亿RMB的机密文件时,却抛了个500,顿时清醒了许多,急忙打开那该死的服务器(映入眼帘的):

    10.88.21.210 2020-12-03 20:03:09:480 ERROR [] c.s.d.f.i.a.WebErrorInterceptor 45  Failed to parse multipart servlet request; nested exception is java.lang.RuntimeException: java.nio.file.NoSuchFileException: /tmp
    org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.lang.RuntimeException: java.nio.file.NoSuchFileException: /tmp
            at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.handleParseFailure(StandardMultipartHttpServletRequest.java:124)
            at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:115)
            .......
    Caused by: java.lang.RuntimeException: java.nio.file.NoSuchFileException: /tmp
            at io.undertow.server.handlers.form.MultiPartParserDefinition$MultiPartUploadHandler.beginPart(MultiPartParserDefinition.java:261)
            at io.undertow.util.MultipartParser$ParseState.headerName(MultipartParser.java:208)
            .......
    

    一看,傻眼了,事情不太妙,这是神马情况?怎么就找不到这个/tmp目录了呢?便请教了百度大哥,他告诉我原因:Spring Boot在启动时,会在操作系统生成一个名为/tmp的临时目录,如果超过10天未使用该临时目录,则系统会自动执行systemd-tmpfiles-clean.service服务来清除该临时目录。当上传文件的请求到来时,Spring Boot会痴痴的去寻觅/tmp,找不到了也不会再去新建一个目录,这就是爱吧!我被感动了,我决心成人之美。给他想了以下解决方案:
    a. 在服务器新建一个名为/tmp的临时目录(不可取:强“建”的目录不久,Spring Boot 超过10天不搭理/tmp,爱依旧会消失的)
    b. 重启服务,会再此自动生产临时目录(不可取:今生不能再做情人,来世再做夫妻,奈何命运弄Spring Boot,依旧会重蹈覆辙)
    c. Spring Boot官方回应,Spring Boot 2.1.4修复了该问题,我没有亲自尝试过(我信你个鬼,你个糟老头子坏得狠),但Spring Boot 2.2.6没有修复,可能只是2.1.4版本修复了。
    在这里插入图片描述
    d. 在yml文件配置一个目录,指定文件存储的目录,但必须保证这个目录存在,且具有读写的权限。

    spring:
      servlet:
        multipart:
          location: /uploadFile
    

    e. 优化方案d,写一个配置类,即使yml文件中配置的目录不存在,也可以自动创建目录。

    @Slf4j
    @Configuration
    public class UndertowConfig {
    
        @Value("${spring.servlet.multipart.location}")
        private String filePath;
    
        @Bean
        public MultipartConfigElement multipartConfigElement(){
    
            MultipartConfigFactory factory = new MultipartConfigFactory();
    
            if (!StringUtil.isNullOrEmpty(filePath)) {
                File file = new File(filePath);
                log.info("文件存储路径:{}", file);
                if (!file.exists()) {
                    boolean dirCreated = file.mkdir();
                    if(! dirCreated){
                        throw new RuntimeException("create file " + file.getAbsolutePath() + " failed!");
                    }
                }
    
                // 需要写和执行的权限
                if(! (file.canWrite() && file.canExecute())){
                    throw new RuntimeException(file.getAbsolutePath() + " Permission denied!");
                }
            }
    
            factory.setLocation(filePath);
            return factory.createMultipartConfig();
        }
    
    }
    
  2. 文件模版的下载链接失效了???
    曾在OSS界面将文件模版进行上传,但一段时间后发现链接URL失效了,导致无法下载模版。打开OSS发现,每个URL的有效时间最长是32400秒,每间隔32400秒就会生成一个新的URL,旧的URL就会失效。
    **加粗样式**
    为了实现URL长期不失效,可通过OSSClient的generatePresignedUrl(String bucketName, String key, Date expiration)进行上传,Date expiration参数可设置URL的有效时长,如10年,20年。

参考:
http://blog.zhaojie.me/2011/03/html-form-file-uploading-programming.html
http://www.faqs.org/rfcs/rfc1867.html
https://github.com/spring-projects/spring-boot/issues/9616
https://www.codenong.com/cs105778648/

  • 10
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值