升级SpringBoot版本导致文件上传失败的问题
问题描述
因更换SpringBoot版本导致文件上传失败
老版本:spring-web:5.1.7.RELEASE
新版本:spring-web:5.2.4.RELEASE
SpringMvc流程图
debug过程
我们都知道,SpringMVC是基于DispatcherServlet做统一请求处理的,那就从DispatcherServlet开始debug,下面是入口方法
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
this.logRequest(request);
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap();
Enumeration attrNames = request.getAttributeNames();
label95:
while(true) {
String attrName;
do {
if (!attrNames.hasMoreElements()) {
break label95;
}
attrName = (String)attrNames.nextElement();
} while(!this.cleanupAfterInclude && !attrName.startsWith("org.springframework.web.servlet"));
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource());
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
try {
this.doDispatch(request, response);
} finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {
this.restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
可以看到38行调用了doDispatch方法,如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
因为是附件上传,13行应该是关键代码,如下:
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
if (request.getDispatcherType().equals(DispatcherType.REQUEST)) {
this.logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
}
} else if (this.hasMultipartException(request)) {
this.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 var3) {
if (request.getAttribute("javax.servlet.error.exception") == null) {
throw var3;
}
}
this.logger.debug("Multipart resolution failed for error dispatch", var3);
}
}
return request;
}
根据名字,11行应该对Multipart进行了解析,如下:
可以看到MultipartResolver有两个实现类,分别是StandardServletMultipartResolver和CommonsMultipartResolver,通过查看引用(alt+F7)可以看到,在MultipartAutoConfiguration中自动装配了StandardServletMultipartResolver
所以打开StandardServletMultipartResolver查看:
再跟进去:
调用了解析的方法,如下:
通过debug发现,老版本这里是能获取到Collection的,新版本获取的Collection的size为0,为什么会这样呢?
可以看到是RequestWrapper这个类,是不是很奇怪?感觉没见过这个类(假装很熟悉SpringMvc源码),点开看看,是冈先生自定义的一个servlet
然后发现是一个filter引用了这个类
解决问题
一般情况下,Javaweb下request流只能读一次,基于这个判断这个类莫名处理了流可能会出现问题,禁用Filter,果然问题解决!!!!!
追根溯源
这算是解决问题吗?不算,因为之前版本为什么就可以?我们再看一下
F7进去,看到
如果为空,其实load了,看看loadParts方法
可以看到parseFormData是关键代码,进去看看
发现readStarted返回null,看看readStarted什么时候更改的,会发现在getReader()和getInputStream()的时候改为true,巧了,冈先生自定义的servlet刚好getInputStream()了!!!那么问题来了,老版本为什么没问题???大概看下HttpServletRequestImpl这个类的源码会发现有很多地方(getParameter(String name)、getParameterNames()等)调用了,parseFormData()方法,是不是想到了什么?没错,在readStarted为true之前调用parseFormData()就可以了,因为parseFormData()方法是private的,所以肯定是内部调用,随便在getParameter(String name)打了个断点,巧了,就是这里调用了
可以看到这里是个filter,通过跟踪可以发现,新版本没有调用这个filter,为什么呢?继续跟进,alt+f7看看哪里用了
发现了吧,这是老版本,自动装配了这个过滤器,再看看新版本
根本问题
对比发现,根本问题是新版本的springMVC在装配HiddenHttpMethodFilter的时候matchIfMissing=false,所以导致parseFormData()没有调用,又恰好冈先生提前getInputStream()导致无法再次parseFormData(),从而造成上传失败的问题,所以最终解决方案在application.yml中加入spring.mvc.hiddenmethod.filter=true即可,不影响冈先生神奇的代码