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

8731 Views

圣旨(金科玉律)

前端人员接旨:

1. 提交方式必须是:post

2. 文件上传的表单项必须是:

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再从服务器获取文件,根据业务需求再对文件中的数据进行处理。

二. 文件是怎么被上传的

前端

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



金科玉律:

1. 提交方式必须是:post

2. 文件上传的表单项必须是:

3. 表单类型必须是:enctype="multipart/form-data"

创建两个文件

文件名:file1.xlsx 文件内存储的内容:hello

文件名:file2.xlsx 文件内存储的内容:world

选择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,用来解析文件数据。

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

public boolean isMultipart(HttpServletRequest request) {

return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");

}

如果是文件上传的请求,MultipartResolver的resolveMultipart()方法会把文件解析的工作交给StandardMultipartHttpServletRequest对象。

public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {

return new StandardMultipartHttpServletRequest(request, this.resolveLazily);

}

StandardMultipartHttpServletRequest对象对数据进行解析。解析后得到的是一组MultipartFile,每个MultipartFile对应文件上传数据中的一个part,并且存储于服务器的/tmp临时目录或自己指定的目录。

private void parseRequest(HttpServletRequest request) {

try {

Collection parts = request.getParts();

this.multipartParameterNames = new LinkedHashSet<>(parts.size());

MultiValueMap 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 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 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;

}

三. 上传文件遇到过的坑

文件在服务器的存储目录找不到了,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();

}

}

文件模版的下载链接失效了??? 曾在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個祕密!
黎姿:镜头内外,不止于美