以前通过spring使用文件上传时,使用的是commons-io、commons-fileupload组件整合来实现的,转移到springboot之后,也就没关注这块,直接取使用了,最近发现一些配置的影响,代码处理的逻辑与预期的有很大的处理,于是简单的了解了具体的实现方式。
首先我们要知道,在springboot中,处理文件上传和spring中是一样的,我们在进行请求提交时,在DispatcherServlet中会对该请求进行校验:
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " +
"this typically results from an additional MultipartFilter in web.xml");
}
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);
}
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;
}
核心还是multipartResolver这个,所有逻辑都是通过它来完成。
在 spring-boot-autoconfigure这个jar中找到spring.factories,所有的自动配置都在这里完成
org.springframework.boot.autoconfigure.EnableAutoConfiguration
关于上面说到的MultipartResolver,主要是通过org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration这个类配置来完成。
@Configuration
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class,
MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.http.multipart", name = "enabled",
matchIfMissing = true)
@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 = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
@ConditionalOnMissingBean(MultipartResolver.class)
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}
}
其实可以看到,默认情况下,系统使用了StandardServletMultipartResolver作为MultipartResolver默认实现,里面具体做了什么,跟着上面的方法resolveMultipart进去看下就知道,其实主要的实现逻辑是解析HttpServletRequest,讲其转换成另一个子类MultipartHttpServletRequest,主要是针对Servlet3的特性对附件和请求参数进行处理,全部封装到这个参数之中。因此我们在发送请求的时候,可以直接通过MultipartHttpServletRequest来接收请求进行处理。
其实到这里,springboot的文件上传基本就这个逻辑,但是我们可以看到,spring中对MultipartResolver接口还有另外一个实现类CommonsMultipartResolver,这个就是最开始说到的,因此在使用它时,我们需要将commons的两个包导入进来。然而说到的问题就是在使用时,同样在controller中用MultipartHttpServletRequest处理请求,但是无论如何,获取到的请求都取不到附件。通过调试也没发现任何问题。
在此之前我们需要分析一下,我们不管使用上面的那种方式,都是对request处理,而request一旦被消费,那么里面的附件也就没了!
比如通过Servlet3处理附件,主要是通过Requets->getParts()获取相关参数,然后解析进行下一步封装。而一旦这样做了后,apache commons处理请求时,就再也获取不到请求了。所以上面的问题会不会是在CommonsMultipartResolver处理前,请求已经被消费了?
目前通过断点也没发现具体是哪里导致这个问题,但是通过添加@EnableWebMvc或者设置 spring.http.multipart.enabled=false则可以正常,也就是不处理MultipartAutoConfiguration。
下面说几个需要注意的点:
1、CommonsMultipartResolver + @EnableWebMvc可以正常使用
2、CommonsMultipartResolver + spring.http.multipart.enabled=false 也是正常的
3、如果使用CommonsMultipartResolver在配置文件中的spring.http.multipart.*都将无效