升级SpringBoot版本导致文件上传失败的问题

升级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,为什么会这样呢?
image
可以看到是RequestWrapper这个类,是不是很奇怪?感觉没见过这个类(假装很熟悉SpringMvc源码),点开看看,是冈先生自定义的一个servlet
image
然后发现是一个filter引用了这个类

解决问题

一般情况下,Javaweb下request流只能读一次,基于这个判断这个类莫名处理了流可能会出现问题,禁用Filter,果然问题解决!!!!!

追根溯源

这算是解决问题吗?不算,因为之前版本为什么就可以?我们再看一下
image
F7进去,看到


如果为空,其实load了,看看loadParts方法

可以看到parseFormData是关键代码,进去看看
image
发现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即可,不影响冈先生神奇的代码


原文:https://www.yuque.com/docs/share/69f992f8-e8dd-46e9-be10-ccd4604f297f?#