HTTP上传文件解析
浏览器上传完整报文如下
POST http://localhost:8080/fileUpload/ HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Referer: http://localhost:8080/ Content-Type: multipart/form-data; boundary=---------------------------114782935826962 Content-Length: 937402 Connection: keep-alive Upgrade-Insecure-Requests: 1 DNT: 1 -----------------------------114782935826962 Content-Disposition: form-data; name="text1" text default -----------------------------114782935826962 Content-Disposition: form-data; name="text2" aωb -----------------------------114782935826962 Content-Disposition: form-data; name="file1"; filename="timg.jpg" Content-Type: image/jpeg ... contents of file goes here ... ----------------114782935826962 Content-Disposition: form-data; name="file2"; filename="timg.jpg" Content-Type: image/jpeg ... contents of file goes here ... ----------------114782935826962--
从这里可以看出
1. 文件上传时使用的Content-Type为 multipart/form-data
根据rfc1867:
The media-type multipart/form-data follows the rules of all multipart
MIME data streams as outlined in RFC 1521. It is intended for use in
returning the data that comes about from filling out a form.
这个类型允许传递所有MIME(Multipurpose Internet Mail Extensions多用途互联网邮件扩展类型)数据流。
2. 其通过Content-Type中的boundary=---------------------------114782935826962来分割表单中的不同参数。
boundary是一个不会在文件流中出现的字符串,用以分割不同参数。
浏览器发送后,那么后台是怎么处理的,这里以springMVC为例子。
后台的SpringMvc接到后的处理方式如下:
1. SpringMvc所有的请求都由DispatcherServlet进行分发,其具体代码截取如下:
org.springframework.web.servlet.DispatcherServlet.doDispatch(HttpServletRequest, HttpServletResponse)
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { try{ try{ ... processedRequest = checkMultipart(request);//检查并处理multipart的data,若存在file,则转换为StandardMultipartHttpServletRequest返回请求 multipartRequestParsed = (processedRequest != request);//标记是否有multipart处理过,若有,在最后要进行清理文件 ... } catch (Exception ex) { dispatchException = ex; } } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { ... } else { // Clean up any resources used by a multipart request. 清理消息 if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } }
接下来看看checkMultipart
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {//通过contentType中是否含有multipart/来判断是否有文件 if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) { if (request.getDispatcherType().equals(DispatcherType.REQUEST)) { logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter"); } } else if (hasMultipartException(request) ) { logger.debug("Multipart resolution previously failed for current request - " + "skipping re-resolution for undisturbed error rendering"); } else { try { return this.multipartResolver.resolveMultipart(request);//将request中的数据流转成StandardMultipartHttpServletRequest } catch (MultipartException ex) { if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) { logger.debug("Multipart resolution failed for error dispatch", ex); // Keep processing error dispatch with regular request handle below } else { throw ex; } } } } // If not returned before: return original request. return request; }
org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(HttpServletRequest)
@Override public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { return new StandardMultipartHttpServletRequest(request, this.resolveLazily);//里面执行了parseRequest() }
org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(HttpServletRequest)具体逻辑如下,其中从每部分中获取Content-Disposition,包含文件名,和元素名称,如果发现对象是文件的话(文件名不为空)则将文件放到request对象中去并返回。
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) { String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);//从每部分中获取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); } }
接下来就是正常的处理逻辑,去执行springMvc对应的方法,从头中已经能拿到对应的file。
在请求完成后,会进行文件资源的清理。
org.springframework.web.multipart.support.StandardServletMultipartResolver.cleanupMultipart(MultipartHttpServletRequest)
@Override public void cleanupMultipart(MultipartHttpServletRequest request) { if (!(request instanceof AbstractMultipartHttpServletRequest) || ((AbstractMultipartHttpServletRequest) request).isResolved()) { // To be on the safe side: explicitly delete the parts, // but only actual file parts (for Resin compatibility) try { for (Part part : request.getParts()) { if (request.getFile(part.getName()) != null) { part.delete(); } } } catch (Throwable ex) { LogFactory.getLog(getClass()).warn("Failed to perform cleanup of multipart items", ex); } } }
参考:
1. https://tools.ietf.org/html/rfc1867 文件上传的rfc规范
任凭弱水三千,我只取一瓢饮