SpringBoot文件上传的使用以及原理

一、使用

1、构建文件上传表单

Example block-level help text here.

2、文件上传代码

//表单提交必须用post请求    
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
                     @RequestParam("username") String username,
                     @RequestPart("headerImg") MultipartFile headerImg, // MultipartFile:用于上传文件功能   @RequestPart:获取表单里的文件项
                     @RequestPart("photos") MultipartFile[] photos) throws IOException {

    log.info("上传的信息:email={},username={},headerImg={},photos={}",
            email,username,headerImg.getSize(),photos.length);

    if(!headerImg.isEmpty()){ //isEmpty,getOriginalFilename,transferTo:都是MultipartFile接口里的方法
        String originalFilename = headerImg.getOriginalFilename();
        headerImg.transferTo(new File("D:\\cache\\"+originalFilename));//transferTo:文件保存(传输)到哪
    }
    if(photos.length>0){
        for (MultipartFile photo : photos) {
            if(!photo.isEmpty()){
                String originalFilename = photo.getOriginalFilename();
                photo.transferTo(new File("D:\\cache\\"+originalFilename));
            }
        }
    }
    return "main";
}
}

如果上传出现了超出限制的大小异常

org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field profile-photo exceeds its maximum permitted size of 1048576 bytes.

可以修改底层默认限制文件的大小:

spring.servlet.multipart.max-file-size=10 #单个文件最大的大小
#多文件上传时,总的一次提交的最大大小,默认是10MB,改成100
spring.servlet.multipart.max-request-size=100

二、原理

文件上传自动配置类是MultipartAutoConfigurationMultipartProperties所有有关文件上传的配置都封装在MultipartProperties里

  • springboot已经自动配置好了StandardServletMultipartResolver【文件上传解析器】它只能解析标准的以servlet的方式,相当于以servlet协议上传过来的文件。如果是自定义方式,直接往上传流的方式应该写自定义的文件上传解析器

    @Configuration(
      proxyBeanMethods = false
    )
    @ConditionalOnClass({Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class})
    @ConditionalOnProperty(
      prefix = "spring.servlet.multipart",
      name = {"enabled"},
      matchIfMissing = true
    )
    @ConditionalOnWebApplication(
      type = Type.SERVLET
    )
    @EnableConfigurationProperties({MultipartProperties.class})
    public class MultipartAutoConfiguration {
      private final MultipartProperties multipartProperties;
    
      public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
          this.multipartProperties = multipartProperties;
      }
    
      @Bean
      @ConditionalOnMissingBean({MultipartConfigElement.class, CommonsMultipartResolver.class})
      public MultipartConfigElement multipartConfigElement() {
          return this.multipartProperties.createMultipartConfig();
      }
    
      @Bean(
          name = {"multipartResolver"}
      )
      @ConditionalOnMissingBean({MultipartResolver.class})
      public StandardServletMultipartResolver multipartResolver() {
          StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
          multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
          return multipartResolver;
      }
    }

原理步骤
一、请求进来使用文件上传解析器判断(用isMultipart方法判断)并封装(用resolveMultipart方法封装,返回MultipartHttpServletRequest类型)文件上传请求

  • 1、请求被DispatcherServlet的doDispatch()拦截
    SpringBoot文件上传的使用以及原理_第1张图片
  • 在选择使用哪个解析器去处理请求之前,会先调用checkMultipart()检查当前的请求是否是一个文件上传的请求

  • 2、checkMultipart()逻辑:
    判断multipartResolver文件上传解析器是否在容器中存在并且判断当前请求是否是文件上传请求
    SpringBoot文件上传的使用以及原理_第2张图片
    文件上传解析器是否在容器中就看MultipartAutoConfiguration有没有给我们把文件上传解析器放到容器中
    SpringBoot文件上传的使用以及原理_第3张图片
  • 可以看到MultipartAutoConfiguration类里的multipartResolver()方法用了@ConditionalOnMissingBean注解,所以如果我们没有写自定义的文件上传解析器的话,SpringBoot会自动往容器中注入StandardServletMultipartResolver文件上传解析器。
  • 判断当前请求是否是文件上传请求:

    public boolean isMultipart(HttpServletRequest request) {
    //判断请求的ContentType是否是multipart/开头,由于我们的表单设置enctype="multipart/form-data",所以这是一个文件上传的请求
          return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
      }

  • 3、是一个文件上传请求的话,就用容器中的multipartResolver文件上传解析器解析请求

    return this.multipartResolver.resolveMultipart(request);

    并把原生的request封装成StandardMultipartHttpServletRequest类,然后所有的文件上传请求最终会被返回一个叫MultipartHttpServletRequest对象

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

    因为前面的checkMultipart()会返回一个包装后的request请求,所以包装后的request请求和原生的request不相等,那么它就是一个文件上传请求,那么multipartRequestParsed的属性值也就变成了true
    SpringBoot文件上传的使用以及原理_第4张图片


二、判断文件上传请求是用哪个参数解析器最终把参数值确定的

  • 1、找到这个参数解析器来解析请求中的文件内容并封装成MultipartFile
    SpringBoot文件上传的使用以及原理_第5张图片
  • 2、找到该解析器的执行控制器方法

    /**
       * Invoke the {@link RequestMapping} handler method preparing a {@link ModelAndView}
       * if view resolution is required.
       * @since 4.2
       * @see #createInvocableHandlerMethod(HandlerMethod)
       */
    @Nullable
      protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
              HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
          invocableMethod.invokeAndHandle(webRequest, mavContainer);
    }

    一路step into后,可以看到一个这样的方法,这个方法下面有一个遍历循环的操作
    SpringBoot文件上传的使用以及原理_第6张图片
    进入resolveArgument方法,看this.getArgumentResolver(parameter)如何获取参数解析器的
    SpringBoot文件上传的使用以及原理_第7张图片
    SpringBoot文件上传的使用以及原理_第8张图片
    拿到参数解析器后,就看他如何解析(RequestPartMethodArgumentResolver)
    SpringBoot文件上传的使用以及原理_第9张图片

    SpringBoot文件上传的使用以及原理_第10张图片
    SpringBoot文件上传的使用以及原理_第11张图片
    进入getFiles方法
    SpringBoot文件上传的使用以及原理_第12张图片
    再进入getMultipartFiles方法
    将request中的文件信息封装为一个Map并返回;MultiValueMap
    SpringBoot文件上传的使用以及原理_第13张图片
    SpringBoot文件上传的使用以及原理_第14张图片

因为参数的位置是这么写的,所以相当于将headerImg,photos全部封装到map中,想要获取headerImg里的数据可以从map里拿出来,photo也是。
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
                     @RequestParam("username") String username,
                     @RequestPart("headerImg") MultipartFile headerImg,
                     @RequestPart("photos") MultipartFile[] photos)

最终是通过文件上传解析器把所有的文件信息获取到。


MultipartFile接口中还有另外非常强大的方法

public interface MultipartFile extends InputStreamSource {
//获取表单中的属性名
    String getName();

//获取原始的文件名
    @Nullable
    String getOriginalFilename();

//获取内容的类型(multipart/form-data)
    @Nullable
    String getContentType();

//判断当前文件是否为空
    boolean isEmpty();

//当前大小
    long getSize();

//获取字节流
    byte[] getBytes() throws IOException;

//获取输入流
    InputStream getInputStream() throws IOException;

//获取文件资源的路径信息
    default Resource getResource() {
        return new MultipartFileResource(this);
    }

//把文件保存到哪里
    void transferTo(File var1) throws IOException, IllegalStateException;

    default void transferTo(Path dest) throws IOException, IllegalStateException {
//FileCopyUtils文件复制工具类
        FileCopyUtils.copy(this.getInputStream(), Files.newOutputStream(dest));
    }
}

----------------FileCopyUtils------------------------------
//把文件复制到哪里:实现文件流的拷贝
public static int copy(File in, File out) throws IOException {
        Assert.notNull(in, "No input File specified");
        Assert.notNull(out, "No output File specified");
        return copy(Files.newInputStream(in.toPath()), Files.newOutputStream(out.toPath()));
    }

分析原理从两处着手,一是这个功能springboot有没有为他做自动配置、自动配置了哪些,二是调试源码看这个功能是怎么实现的。

你可能感兴趣的:(javaspringboot)