• Spring MVC文件请求处理详解:MultipartResolver


    org.springframework.web.multipart.MultipartResolver是Spring-Web针对RFC1867实现的多文件上传解决策略。

    1 使用场景#

    前端上传文件时,无论是使用比较传统的表单,还是使用FormData对象,其本质都是发送一个multipart/form-data请求。
    例如,前端模拟上传代码如下:

    1. var formdata = new FormData();
    2. formdata.append("key1", "value1");
    3. formdata.append("key2", "value2");
    4. formdata.append("file1", fileInput.files[0], "/d:/Downloads/rfc1867.pdf");
    5. formdata.append("file2", fileInput.files[0], "/d:/Downloads/rfc1314.pdf");
    6. var requestOptions = {
    7. method: 'POST',
    8. body: formdata,
    9. redirect: 'follow'
    10. };
    11. fetch("http://localhost:10001/file/upload", requestOptions)
    12. .then(response => response.text())
    13. .then(result => console.log(result))
    14. .catch(error => console.log('error', error));

    实际会发送如下HTTP请求:

    1. POST /file/upload HTTP/1.1
    2. Host: localhost:10001
    3. Content-Length: 536
    4. Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
    5. ----WebKitFormBoundary7MA4YWxkTrZu0gW
    6. Content-Disposition: form-data; name="key1"
    7. value1
    8. ----WebKitFormBoundary7MA4YWxkTrZu0gW
    9. Content-Disposition: form-data; name="key2"
    10. value2
    11. ----WebKitFormBoundary7MA4YWxkTrZu0gW
    12. Content-Disposition: form-data; name="file1"; filename="/d:/Downloads/rfc1867.pdf"
    13. Content-Type: application/pdf
    14. (data)
    15. ----WebKitFormBoundary7MA4YWxkTrZu0gW
    16. Content-Disposition: form-data; name="file2"; filename="/d:/Downloads/rfc1314.pdf"
    17. Content-Type: application/pdf
    18. (data)
    19. ----WebKitFormBoundary7MA4YWxkTrZu0gW

    在后端可以通过MultipartHttpServletRequest接收文件:

    1. @RestController
    2. @RequestMapping("file")
    3. public class FileUploadController {
    4. @RequestMapping("/upload")
    5. public String upload(MultipartHttpServletRequest request) {
    6. // 获取非文件参数
    7. String value1 = request.getParameter("key1");
    8. System.out.println(value1); // value1
    9. String value2 = request.getParameter("key2");
    10. System.out.println(value2); // value2
    11. // 获取文件
    12. MultipartFile file1 = request.getFile("file1");
    13. System.out.println(file1 != null ? file1.getOriginalFilename() : "null"); // rfc1867.pdf
    14. MultipartFile file2 = request.getFile("file2");
    15. System.out.println(file2 != null ? file2.getOriginalFilename() : "null"); // rfc1314.pdf
    16. return "Hello MultipartResolver!";
    17. }
    18. }

    2 MultipartResolver接口#

    2.1 MultipartResolver的功能#

    org.springframework.web.multipart.MultipartResolver是Spring-Web根据RFC1867规范实现的多文件上传的策略接口。
    同时,MultipartResolver是Spring对文件上传处理流程在接口层次的抽象。
    也就是说,当涉及到文件上传时,Spring都会使用MultipartResolver接口进行处理,而不涉及具体实现类。
    MultipartResolver接口源码如下:

    1. public interface MultipartResolver {
    2. /**
    3. * 判断当前HttpServletRequest请求是否是文件请求
    4. */
    5. boolean isMultipart(HttpServletRequest request);
    6. /**
    7. * 将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象
    8. */
    9. MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
    10. /**
    11. * 清除文件上传产生的临时资源(如服务器本地临时文件)
    12. */
    13. void cleanupMultipart(MultipartHttpServletRequest request);
    14. }

    2.2 在DispatcherServlet中的使用#

    DispatcherServlet中持有MultipartResolver成员变量:

    1. public class DispatcherServlet extends FrameworkServlet {
    2. /** Well-known name for the MultipartResolver object in the bean factory for this namespace. */
    3. public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";
    4. /** MultipartResolver used by this servlet. */
    5. @Nullable
    6. private MultipartResolver multipartResolver;
    7. }

    DispatcherServlet在初始化时,会从Spring容器中获取名为multipartResolver的对象(该对象是MultipartResolver实现类),作为文件上传解析器:

    1. /**
    2. * Initialize the MultipartResolver used by this class. *

      If no bean is defined with the given name in the BeanFactory for this namespace,

    3. * no multipart handling is provided. */
    4. private void initMultipartResolver(ApplicationContext context) {
    5. try {
    6. this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
    7. if (logger.isTraceEnabled()) {
    8. logger.trace("Detected " + this.multipartResolver);
    9. }
    10. else if (logger.isDebugEnabled()) {
    11. logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());
    12. }
    13. }
    14. catch (NoSuchBeanDefinitionException ex) {
    15. // Default is no multipart resolver.
    16. this.multipartResolver = null;
    17. if (logger.isTraceEnabled()) {
    18. logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared");
    19. }
    20. }
    21. }

    需要注意的是,如果Spring容器中不存在名为multipartResolver的对象,DispatcherServlet并不会额外指定默认的文件解析器。此时,DispatcherServlet不会对文件上传请求进行处理。也就是说,尽管当前请求是文件请求,也不会被处理成MultipartHttpServletRequest,如果我们在控制层进行强制类型转换,会抛异常。

    DispatcherServlet在处理业务时,会按照顺序分别调用这些方法进行文件上传处理,相关核心源码如下:

    1. protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    2. HttpServletRequest processedRequest = request;
    3. boolean multipartRequestParsed = false;
    4. try {
    5. // 判断&封装文件请求
    6. processedRequest = checkMultipart(request);
    7. multipartRequestParsed = (processedRequest != request);
    8. // 请求处理……
    9. }
    10. finally {
    11. // 清除文件上传产生的临时资源
    12. if (multipartRequestParsed) {
    13. cleanupMultipart(processedRequest);
    14. }
    15. }
    16. }

    checkMultipart()方法中,会进行判断、封装文件请求:

    1. /**
    2. * Convert the request into a multipart request, and make multipart resolver available. *

      If no multipart resolver is set, simply use the existing request.

    3. * @param request current HTTP request
    4. * @return the processed request (multipart wrapper if necessary) * @see MultipartResolver#resolveMultipart
    5. */
    6. protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
    7. if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
    8. if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
    9. if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
    10. logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
    11. }
    12. }
    13. else if (hasMultipartException(request)) {
    14. logger.debug("Multipart resolution previously failed for current request - " +
    15. "skipping re-resolution for undisturbed error rendering");
    16. }
    17. else {
    18. try {
    19. return this.multipartResolver.resolveMultipart(request);
    20. }
    21. catch (MultipartException ex) {
    22. if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
    23. logger.debug("Multipart resolution failed for error dispatch", ex);
    24. // Keep processing error dispatch with regular request handle below
    25. }
    26. else {
    27. throw ex;
    28. }
    29. }
    30. }
    31. }
    32. // If not returned before: return original request.
    33. return request;
    34. }

    总的来说,DispatcherServlet处理文件请求会经过以下步骤:

    1. 判断当前HttpServletRequest请求是否是文件请求
      1. 是:将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象
      2. 不是:不处理
    2. DispatcherServlet对原始HttpServletRequestMultipartHttpServletRequest对象进行业务处理
    3. 业务处理完成,清除文件上传产生的临时资源

    2.3 MultipartResolver实现类&配置方式#

    Spring提供了两个MultipartResolver实现类:

    • org.springframework.web.multipart.support.StandardServletMultipartResolver:根据Servlet 3.0+ Part Api实现
    • org.springframework.web.multipart.commons.CommonsMultipartResolver:根据Apache Commons FileUpload实现

    在Spring Boot 2.0+中,默认会在org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration中创建StandardServletMultipartResolver作为默认文件解析器:

    1. @AutoConfiguration
    2. @ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
    3. @ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
    4. @ConditionalOnWebApplication(type = Type.SERVLET)
    5. @EnableConfigurationProperties(MultipartProperties.class)
    6. public class MultipartAutoConfiguration {
    7. private final MultipartProperties multipartProperties;
    8. public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
    9. this.multipartProperties = multipartProperties;
    10. }
    11. @Bean
    12. @ConditionalOnMissingBean({ MultipartConfigElement.class, CommonsMultipartResolver.class })
    13. public MultipartConfigElement multipartConfigElement() {
    14. return this.multipartProperties.createMultipartConfig();
    15. }
    16. @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
    17. @ConditionalOnMissingBean(MultipartResolver.class)
    18. public StandardServletMultipartResolver multipartResolver() {
    19. StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
    20. multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
    21. return multipartResolver;
    22. }
    23. }

    当需要指定其他文件解析器时,只需要引入相关依赖,然后配置一个名为multipartResolverbean对象:

    1. @Bean
    2. public MultipartResolver multipartResolver() {
    3. MultipartResolver multipartResolver = ...;
    4. return multipartResolver;
    5. }

    接下来,我们分别详细介绍两种实现类的使用和原理。

    3 StandardServletMultipartResolver解析器#

    3.1 StandardServletMultipartResolver#isMultipart#

    StandardServletMultipartResolver解析器的通过判断请求的Content-Type来判断是否是文件请求:

    1. public boolean isMultipart(HttpServletRequest request) {
    2. return StringUtils.startsWithIgnoreCase(request.getContentType(),
    3. (this.strictServletCompliance ? "multipart/form-data" : "multipart/"));
    4. }

    其中,strictServletComplianceStandardServletMultipartResolver的成员变量,默认false,表示是否严格遵守Servlet 3.0规范。简单来说就是对Content-Type校验的严格程度。如果strictServletCompliancefalse,请求头以multipart/开头就满足文件请求条件;如果strictServletCompliancetrue,则需要请求头以multipart/form-data开头。

    3.2 StandardServletMultipartResolver#resolveMultipart#

    StandardServletMultipartResolver在解析文件请求时,会将原始请求封装成StandardMultipartHttpServletRequest对象:

    1. public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
    2. return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
    3. }

    需要注意的是,这里传入this.resolveLazily成员变量,表示是否延迟解析。我们可以来看对应构造函数源码:

    1. public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
    2. throws MultipartException {
    3. super(request);
    4. if (!lazyParsing) {
    5. parseRequest(request);
    6. }
    7. }

    如果需要修改resolveLazily成员变量的值,需要在初始化StandardServletMultipartResolver时指定值。
    在Spring Boot 2.0+中,默认会在org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration中创建StandardServletMultipartResolver作为默认文件解析器,此时会从MultipartProperties中读取resolveLazily值。因此,如果是使用Spring Boot 2.0+默认配置的文件解析器,可以在properties.yml文件中指定resolveLazily值:

    spring.servlet.multipart.resolve-lazily=true
    

    如果是使用自定义配置的方式配置StandardServletMultipartResolver,则可以在初始化的手动赋值:

    1. @Bean
    2. public MultipartResolver multipartResolver() {
    3. StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
    4. multipartResolver.setResolveLazily(true);
    5. return multipartResolver;
    6. }

    3.3 StandardMultipartHttpServletRequest#parseRequest#

    resolveLazilytrue时,会马上调用parseRequest()方法会对请求进行实际解析,该方法会完成两件事情:

    1. 使用Servlet 3.0的Part API,获取Part集合
    2. 解析Part对象,封装表单参数和表单文件
    1. private void parseRequest(HttpServletRequest request) {
    2. try {
    3. Collection<Part> parts = request.getParts();
    4. this.multipartParameterNames = new LinkedHashSet<>(parts.size());
    5. MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
    6. for (Part part : parts) {
    7. String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
    8. ContentDisposition disposition = ContentDisposition.parse(headerValue);
    9. String filename = disposition.getFilename();
    10. if (filename != null) {
    11. if (filename.startsWith("=?") && filename.endsWith("?=")) {
    12. filename = MimeDelegate.decode(filename);
    13. }
    14. files.add(part.getName(), new StandardMultipartFile(part, filename));
    15. }
    16. else {
    17. this.multipartParameterNames.add(part.getName());
    18. }
    19. }
    20. setMultipartFiles(files);
    21. }
    22. catch (Throwable ex) {
    23. handleParseFailure(ex);
    24. }
    25. }

    经过parseRequest()方法处理,我们在业务处理时,直接调用StandardMultipartHttpServletRequest接口的getXxx()方法就可以获取表单参数或表单文件信息。

    resolveLazilyfalse时,在MultipartResolver#resolveMultipart()阶段并不会进行文件请求解析。也就是说,此时StandardMultipartHttpServletRequest对象的成员变量都是空值。那么,resolveLazilyfalse时文件请求解析是在什么时候完成的呢?
    实际上,在调用StandardMultipartHttpServletRequest接口的getXxx()方法时,内部会判断是否已经完成文件请求解析。如果未解析,就会调用partRequest()方法进行解析,例如:

    1. @Override
    2. public Enumeration<String> getParameterNames() {
    3. if (this.multipartParameterNames == null) {
    4. initializeMultipart(); // parseRequest(getRequest());
    5. }
    6. // 业务处理……
    7. }

    3.4 HttpServletRequest#getParts#

    根据StandardMultipartHttpServletRequest#parseRequest源码可以发现,StandardServletMultipartResolver解析文件请求依靠的是HttpServletRequest#getParts方法。
    这是StandardServletMultipartResolver是根据标准Servlet 3.0实现的核心体现。
    在Servlet 3.0中定义了javax.servlet.http.Part,用来表示multipart/form-data请求体中的表单数据或文件:

    1. public interface Part {
    2. public InputStream getInputStream() throws IOException;
    3. public String getContentType();
    4. public String getName();
    5. public String getSubmittedFileName();
    6. public long getSize();
    7. public void write(String fileName) throws IOException;
    8. public void delete() throws IOException;
    9. public String getHeader(String name);
    10. public Collection getHeaders(String name);
    11. public Collection getHeaderNames();
    12. }

    javax.servlet.http.HttpServletRequest,提供了获取multipart/form-data请求体各个part的方法:

    1. public interface HttpServletRequest extends ServletRequest {
    2. /**
    3. * Return a collection of all uploaded Parts.
    4. *
    5. * @return A collection of all uploaded Parts.
    6. * @throws IOException
    7. * if an I/O error occurs
    8. * @throws IllegalStateException
    9. * if size limits are exceeded or no multipart configuration is
    10. * provided
    11. * @throws ServletException
    12. * if the request is not multipart/form-data
    13. * @since Servlet 3.0
    14. */
    15. public Collection getParts() throws IOException, ServletException;
    16. /**
    17. * Gets the named Part or null if the Part does not exist. Triggers upload
    18. * of all Parts.
    19. *
    20. * @param name The name of the Part to obtain
    21. *
    22. * @return The named Part or null if the Part does not exist
    23. * @throws IOException
    24. * if an I/O error occurs
    25. * @throws IllegalStateException
    26. * if size limits are exceeded
    27. * @throws ServletException
    28. * if the request is not multipart/form-data
    29. * @since Servlet 3.0
    30. */
    31. public Part getPart(String name) throws IOException, ServletException;
    32. }

    所有实现标准Servlet 3.0规范的Web服务器,都必须实现getPart()/getParts()方法。也就是说,这些Web服务器在解析请求时,会将multipart/form-data请求体中的表单数据或文件解析成Part对象集合。通过HttpServletRequestgetPart()/getParts()方法,可以获取这些Part对象,进而获取multipart/form-data请求体中的表单数据或文件。
    每个Web服务器对Servlet 3.0规范都有自己的实现方式。对于Spring Boot来说,通常使用的是Tomcat/Undertow/Jetty内嵌Web服务器。通常只需要了解这三种服务器的实现方式即可。

    3.4.1 Tomcat实现#

    Tomcat是Spring Boot默认使用的内嵌Web服务器,只需要引入如下依赖:

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-web</artifactId>
    4. </dependency>

    会默认引入Tomcat依赖:

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-tomcat</artifactId>
    4. </dependency>

    Tomcat解析文件请求的核心在于org.apache.catalina.connector.Request#parseParts方法,核心代码如下:

    1. // 1、创建ServletFileUpload文件上传对象
    2. DiskFileItemFactory factory = new DiskFileItemFactory();
    3. try {
    4. factory.setRepository(location.getCanonicalFile());
    5. } catch (IOException ioe) {
    6. parameters.setParseFailedReason(FailReason.IO_ERROR);
    7. partsParseException = ioe;
    8. return;
    9. }
    10. factory.setSizeThreshold(mce.getFileSizeThreshold());
    11. ServletFileUpload upload = new ServletFileUpload();
    12. upload.setFileItemFactory(factory);
    13. upload.setFileSizeMax(mce.getMaxFileSize());
    14. upload.setSizeMax(mce.getMaxRequestSize());
    15. this.parts = new ArrayList<>();
    16. try {
    17. // 2、解析文件请求
    18. List<FileItem> items =
    19. upload.parseRequest(new ServletRequestContext(this));
    20. // 3、封装Part对象
    21. for (FileItem item : items) {
    22. ApplicationPart part = new ApplicationPart(item, location);
    23. this.parts.add(part);
    24. }
    25. }
    26. success = true;
    27. }

    核心步骤如下:

    1. 创建ServletFileUpload文件上传对象
    2. 解析文件请求
    3. 封装Part对象

    org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest会进行实际解析文件请求:

    1. public List parseRequest(final RequestContext ctx) throws FileUploadException {
    2. final List items = new ArrayList<>();
    3. boolean successful = false;
    4. try {
    5. final FileItemIterator iter = getItemIterator(ctx);
    6. final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
    7. "No FileItemFactory has been set.");
    8. final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
    9. while (iter.hasNext()) {
    10. final FileItemStream item = iter.next();
    11. // Don't use getName() here to prevent an InvalidFileNameException.
    12. final String fileName = item.getName();
    13. final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
    14. item.isFormField(), fileName);
    15. items.add(fileItem);
    16. try {
    17. Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
    18. } catch (final FileUploadIOException e) {
    19. throw (FileUploadException) e.getCause();
    20. } catch (final IOException e) {
    21. throw new IOFileUploadException(String.format("Processing of %s request failed. %s",
    22. MULTIPART_FORM_DATA, e.getMessage()), e);
    23. }
    24. final FileItemHeaders fih = item.getHeaders();
    25. fileItem.setHeaders(fih);
    26. }
    27. successful = true;
    28. return items;
    29. }
    30. }

    简单来说,Tomcat会使用java.io.InputStreamjava.io.OutputStream(传统IO流)将multipart请求中的表单参数和文件保存到服务器本地临时文件,然后将本地临时文件信息封装成Part对象返回。
    也就是说,我们在业务中获取到的文件实际上都来自服务器本地临时文件。

    3.4.2 Undertow实现#

    为了使用Undertow服务器,需要引入如下依赖:

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-web</artifactId>
    4. <exclusions>
    5. <exclusion>
    6. <groupId>org.springframework.boot</groupId>
    7. <artifactId>spring-boot-starter-tomcat</artifactId>
    8. </exclusion>
    9. </exclusions>
    10. </dependency>
    11. <dependency>
    12. <groupId>org.springframework.boot</groupId>
    13. <artifactId>spring-boot-starter-undertow</artifactId>
    14. </dependency>

    Undertow解析文件请求的核心在于io.undertow.servlet.spec.HttpServletRequestImpl#loadParts方法,核心代码如下

    1. final List<Part> parts = new ArrayList<>();
    2. String mimeType = exchange.getRequestHeaders().getFirst(Headers.CONTENT_TYPE);
    3. if (mimeType != null && mimeType.startsWith(MultiPartParserDefinition.MULTIPART_FORM_DATA)) {
    4. // 1、解析文件请求,封装FormData对象
    5. FormData formData = parseFormData();
    6. // 2、封装Part对象
    7. if(formData != null) {
    8. for (final String namedPart : formData) {
    9. for (FormData.FormValue part : formData.get(namedPart)) {
    10. parts.add(new PartImpl(namedPart,
    11. part,
    12. requestContext.getOriginalServletPathMatch().getServletChain().getManagedServlet().getMultipartConfig(),
    13. servletContext, this));
    14. }
    15. }
    16. }
    17. } else {
    18. throw UndertowServletMessages.MESSAGES.notAMultiPartRequest();
    19. }
    20. this.parts = parts;

    核心步骤如下:

    1. 解析文件请求,封装FormData对象
    2. 封装Part对象

    io.undertow.servlet.spec.HttpServletRequestImpl#parseFormData方法会进行实际解析文件请求,核心代码如下:

    1. final FormDataParser parser = originalServlet.getFormParserFactory().createParser(exchange)
    2. try {
    3. return parsedFormData = parser.parseBlocking();
    4. }

    io.undertow.server.handlers.form.MultiPartParserDefinition.MultiPartUploadHandler#parseBlocking核心代码如下:

    1. InputStream inputStream = exchange.getInputStream();
    2. try (PooledByteBuffer pooled = exchange.getConnection().getByteBufferPool().getArrayBackedPool().allocate()){
    3. ByteBuffer buf = pooled.getBuffer();
    4. while (true) {
    5. buf.clear();
    6. int c = inputStream.read(buf.array(), buf.arrayOffset(), buf.remaining());
    7. if (c == -1) {
    8. if (parser.isComplete()) {
    9. break;
    10. } else {
    11. throw UndertowMessages.MESSAGES.connectionTerminatedReadingMultiPartData();
    12. }
    13. } else if (c != 0) {
    14. buf.limit(c);
    15. parser.parse(buf);
    16. }
    17. }
    18. exchange.putAttachment(FORM_DATA, data);
    19. }
    20. return exchange.getAttachment(FORM_DATA);

    在这个过程中,Undertow会使用java.io.InputStreamjava.io.OutputStream(传统IO流),结合java.nio.ByteBuffermultipart请求中的表单参数和文件保存到服务器本地临时文件,然后将本地临时文件信息封装成Part对象返回(具体细节可以继续深入阅读相关源码)。
    也就是说,我们在业务中获取到的文件实际上都来自服务器本地临时文件。

    3.4.2 Jetty实现#

    为了使用Jetty服务器,需要引入如下依赖:

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-web</artifactId>
    4. <exclusions>
    5. <exclusion>
    6. <groupId>org.springframework.boot</groupId>
    7. <artifactId>spring-boot-starter-tomcat</artifactId>
    8. </exclusion>
    9. </exclusions>
    10. </dependency>
    11. <dependency>
    12. <groupId>org.springframework.boot</groupId>
    13. <artifactId>spring-boot-starter-jetty</artifactId>
    14. </dependency>

    Jetty解析文件请求的核心在于org.eclipse.jetty.server.Request#getParts方法,核心代码如下

    1. MultipartConfigElement config = (MultipartConfigElement)this.getAttribute("org.eclipse.jetty.multipartConfig");
    2. this._multiParts = this.newMultiParts(config);
    3. // 省略……
    4. return this._multiParts.getParts();

    org.eclipse.jetty.server.Request#newMultiParts会创建文件解析器:

    1. private MultiParts newMultiParts(MultipartConfigElement config) throws IOException {
    2. MultiPartFormDataCompliance compliance = this.getHttpChannel().getHttpConfiguration().getMultipartFormDataCompliance();
    3. switch(compliance) {
    4. case RFC7578:
    5. return new MultiPartsHttpParser(this.getInputStream(), this.getContentType(), config, this._context != null ? (File)this._context.getAttribute("javax.servlet.context.tempdir") : null, this);
    6. case LEGACY:
    7. default:
    8. return new MultiPartsUtilParser(this.getInputStream(), this.getContentType(), config, this._context != null ? (File)this._context.getAttribute("javax.servlet.context.tempdir") : null, this);
    9. }
    10. }

    org.eclipse.jetty.server.MultiParts.MultiPartsHttpParser#getPartsorg.eclipse.jetty.server.MultiParts.MultiPartsUtilParser#getParts则会进行文件请求解析:

    1. public Collection getParts() throws IOException {
    2. Collection parts = this._httpParser.getParts();
    3. this.setNonComplianceViolationsOnRequest();
    4. return parts;
    5. }
    6. public Collection getParts() throws IOException {
    7. Collection parts = this._utilParser.getParts();
    8. this.setNonComplianceViolationsOnRequest();
    9. return parts;
    10. }

    在这个过程中,Jetty会使用java.io.InputStreamjava.io.OutputStream(传统IO流),结合java.nio.ByteBuffermultipart请求中的表单参数和文件保存到服务器本地临时文件,然后将本地临时文件信息封装成Part对象返回。
    也就是说,我们在业务中获取到的文件实际上都来自服务器本地临时文件。

    3.5 StandardServletMultipartResolver#cleanupMultipart#

    StandardServletMultipartResolver#cleanupMultipart方法会将临时文件删除:

    1. public void cleanupMultipart(MultipartHttpServletRequest request) {
    2. if (!(request instanceof AbstractMultipartHttpServletRequest) ||
    3. ((AbstractMultipartHttpServletRequest) request).isResolved()) {
    4. // To be on the safe side: explicitly delete the parts,
    5. // but only actual file parts (for Resin compatibility) try {
    6. for (Part part : request.getParts()) {
    7. if (request.getFile(part.getName()) != null) {
    8. part.delete();
    9. }
    10. }
    11. }
    12. catch (Throwable ex) {
    13. LogFactory.getLog(getClass()).warn("Failed to perform cleanup of multipart items", ex);
    14. }
    15. }
    16. }

    4 CommonsMultipartResolver解析器#

    为了使用CommonsMultipartResolver解析器,除了基础的spring-boot-starter-web,还需要额外引入如下依赖:

    1. <dependency>
    2. <groupId>commons-fileupload</groupId>
    3. <artifactId>commons-fileupload</artifactId>
    4. <version>1.4</version>
    5. </dependency>

    然后,配置名为multipartResolver的bean(此时Spring Boot不会添加默认文件解析器):

    1. @Bean
    2. public MultipartResolver multipartResolver() {
    3. CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
    4. // 文件请求解析配置:multipartResolver.setXxx()
    5. multipartResolver.setResolveLazily(true);
    6. return multipartResolver;
    7. }

    4.1 CommonsMultipartResolver#isMultipart#

    CommonsMultipartResolver解析器会根据请求方法和请求头来判断文件请求,源码如下:

    1. public boolean isMultipart(HttpServletRequest request) {
    2. return (this.supportedMethods != null ?
    3. this.supportedMethods.contains(request.getMethod()) &&
    4. FileUploadBase.isMultipartContent(new ServletRequestContext(request)) :
    5. ServletFileUpload.isMultipartContent(request));
    6. }

    supportedMethods成员变量表示支持的请求方法,默认为null,可以在初始化时指定。
    supportedMethodsnull时,即在默认情况下,会调用ServletFileUpload.isMultipartContent()方法进行判断。此时文件请求的满足条件为:

    1. 请求方法为POST
    2. 请求头Content-Type为以multipart/开头

    supportedMethods不为null时,文件请求满足条件为:

    1. 请求方法在supportedMethods列表中
    2. 请求头Content-Type为以multipart/开头

    4.2 CommonsMultipartResolver#resolveMultipart#

    CommonsMultipartResolver在解析文件请求时,会将原始请求封装成DefaultMultipartHttpServletRequest对象:

    1. public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
    2. Assert.notNull(request, "Request must not be null");
    3. if (this.resolveLazily) {
    4. return new DefaultMultipartHttpServletRequest(request) {
    5. @Override
    6. protected void initializeMultipart() {
    7. MultipartParsingResult parsingResult = parseRequest(request);
    8. setMultipartFiles(parsingResult.getMultipartFiles());
    9. setMultipartParameters(parsingResult.getMultipartParameters());
    10. setMultipartParameterContentTypes(parsingResult.getMultipartParameterContentTypes());
    11. }
    12. };
    13. }
    14. else {
    15. MultipartParsingResult parsingResult = parseRequest(request);
    16. return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
    17. parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
    18. }
    19. }

    StandardServletMultipartResolver相同,CommonsMultipartResolverresolveLazily成员变量也表示是否会马上解析文件。
    resolveLazilyfalse时,即默认情况下,不会立即解析文件,只是会将原始请求进行简单封装。只有在调用DefaultMultipartHttpServletRequest#getXxx方法时,会判断文件是否已经解析。如果没有解析,会调用DefaultMultipartHttpServletRequest#initializeMultipart进行解析。
    resolveLazilytrue时,会立即调用CommonsMultipartResolver#parseRequest方法进行文件解析。

    4.3 CommonsMultipartResolver#parseRequest#

    CommonsMultipartResolver#parseRequest方法会进行文件请求解析,总的来说包括两个步骤:

    1. 解析文件请求
    2. 封装响应
    1. List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
    2. return parseFileItems(fileItems, encoding);

    深入阅读源码可以发现,在解析文件请求时,会采用与StandardServletMultipartResolver+Tomcat相同的方式保存临时文件:

    1. public List parseRequest(RequestContext ctx)
    2. throws FileUploadException {
    3. List items = new ArrayList();
    4. boolean successful = false;
    5. try {
    6. FileItemIterator iter = getItemIterator(ctx);
    7. FileItemFactory fac = getFileItemFactory();
    8. if (fac == null) {
    9. throw new NullPointerException("No FileItemFactory has been set.");
    10. }
    11. while (iter.hasNext()) {
    12. final FileItemStream item = iter.next();
    13. // Don't use getName() here to prevent an InvalidFileNameException.
    14. final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name;
    15. FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(),
    16. item.isFormField(), fileName);
    17. items.add(fileItem);
    18. try {
    19. Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
    20. } catch (FileUploadIOException e) {
    21. throw (FileUploadException) e.getCause();
    22. } catch (IOException e) {
    23. throw new IOFileUploadException(format("Processing of %s request failed. %s",
    24. MULTIPART_FORM_DATA, e.getMessage()), e);
    25. }
    26. final FileItemHeaders fih = item.getHeaders();
    27. fileItem.setHeaders(fih);
    28. }
    29. successful = true;
    30. return items;
    31. } catch (FileUploadIOException e) {
    32. throw (FileUploadException) e.getCause();
    33. } catch (IOException e) {
    34. throw new FileUploadException(e.getMessage(), e);
    35. } finally {
    36. if (!successful) {
    37. for (FileItem fileItem : items) {
    38. try {
    39. fileItem.delete();
    40. } catch (Exception ignored) {
    41. // ignored TODO perhaps add to tracker delete failure list somehow?
    42. }
    43. }
    44. }
    45. }
    46. }

    4.4 CommonsMultipartResolver#cleanupMultipart#

    CommonsMultipartResolver#cleanupMultipart方法会将临时文件删除:

    1. public void cleanupMultipart(MultipartHttpServletRequest request) {
    2. if (!(request instanceof AbstractMultipartHttpServletRequest) ||
    3. ((AbstractMultipartHttpServletRequest) request).isResolved()) {
    4. try {
    5. cleanupFileItems(request.getMultiFileMap());
    6. }
    7. catch (Throwable ex) {
    8. logger.warn("Failed to perform multipart cleanup for servlet request", ex);
    9. }
    10. }
    11. }
  • 相关阅读:
    如何与Linamar Corp 建立EDI连接?
    前端下载文件流
    软件测试工作流程?
    java高级——类加载机制
    [python3] 责任链模式
    如何自己录制教学视频?零基础也能上手
    场景应用:键盘敲入字母a时,期间发生了什么?
    java计算机毕业设计疫情防控期间网上教学管理源码+系统+mysql数据库+lw文档
    湖南省2022年成人高考招生全国统一考试考生须知
    641. 设计循环双端队列 队列模拟
  • 原文地址:https://blog.csdn.net/sinat_40572875/article/details/128128994