关于java:SpringBoot文件上传的使用以及原理

40次阅读

共计 6594 个字符,预计需要花费 17 分钟才能阅读完成。

一、应用

1、构建文件上传表单

<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
    <div class="form-group">
        <label for="exampleInputEmail1"> 邮箱 </label>
        <input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
    </div>
    <div class="form-group">
        <label for="exampleInputPassword1"> 名字 </label>
        <input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password">
    </div>
    <div class="form-group">
        <label for="exampleInputFile"> 头像 </label>
        <input type="file" name="headerImg" id="exampleInputFile">
        <p class="help-block">Example block-level help text here.</p>
    </div>
    <div class="form-group">
        <label for="exampleInputFile"> 生活照 </label>
        <input type="file" name="photo" multiple>
    </div>
    <div class="checkbox">
        <label>
            <input type="checkbox"> Check me out
        </label>
    </div>
    <button type="submit"  class="btn btn-primary"> 提交 </button>
</form>

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()拦挡
  • 在抉择应用哪个解析器去解决申请之前,会先调用 checkMultipart()查看以后的申请是否是一个文件上传的申请

  • 2、checkMultipart()逻辑:
    判断 multipartResolver 文件上传解析器是否在容器中存在并且判断以后申请是否是文件上传申请

    文件上传解析器是否在容器中就看 MultipartAutoConfiguration 有没有给咱们把文件上传解析器放到容器中

  • 能够看到 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


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

  • 1、找到这个参数解析器来解析申请中的文件内容并封装成 MultipartFile
  • 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 后,能够看到一个这样的办法,这个办法上面有一个遍历循环的操作

    进入 resolveArgument 办法,看 this.getArgumentResolver(parameter)如何获取参数解析器的

    拿到参数解析器后,就看他如何解析(RequestPartMethodArgumentResolver)

    进入 getFiles 办法

    再进入 getMultipartFiles 办法
    将 request 中的文件信息封装为一个 Map 并返回;MultiValueMap<String, MultipartFile>

因为参数的地位是这么写的,所以相当于将 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 有没有为他做主动配置、主动配置了哪些,二是调试源码看这个性能是怎么实现的。

正文完
 0