Spring boot 2/3.x 中 MultipartFile 接收问题分析

857 阅读2分钟

一、场景

在写一个统一文件上传的时候,在采用MultipartFile对象接收参数时会出现无法传递或者一直为空情况。

二、根因分析

出现上述问题主要根因如下:

  1. 前后端参数名对应不一致,即@RequestPart(value = "multipartFile",required = false) MultipartFile multipartFile 解决;

  2. 关闭文件上传支持,即spring.servlet.multipart.enabled=false;

  3. 配置文件中指定了文件上传时的大小值问题,即配置如下

    spring:
        servlet:
            multipart:
                enabled: false
                max-file-size: 10MB
                max-request-size: 10MB
    
  4. 切换内嵌容器tomcat到undertow的配置问题;

  5. 指定了临时文件站,但路径不存在即spring.servlet.multipart.location=/tmp;

  6. 多次读取HttpServletRequest流;

  7. Spring Boot已经有CommonsMultipartResolver,需要排除原有的Multipart配置@EnableAutoConfiguration(exclude = {MultipartAutoConfiguration.class}),此种情况在Spring boot3.x版本中不存在,主要是在Spring boot3.x版本中CommonsMultipartResolver已经不存在。

针对上述原因除了第六种比较隐蔽比较深的外,其余解决容易发现解决。

三、包装request处理多次读流的无法获取MultipartFile分析

(一)原因

处理Multipart/form类型和别的post类型都不太一样。x-www-form-urlencoded和普通post携带参数都是直接从request的inpustream里读取,Multipart/form类型你需要获取Parts 即request.getParts

在缓存body[]时已经将流读取。Multipart获取方式和普通表单并不一样。在重写方法时并没有对getParts单独重写处理即可。

(二)解决方式

  1. 处理Multipart/form时请缓存Parts.重写getParts方法,可以看一下,可以看下Springmvc是怎么处理Multipart/form和普通的application/x-www-form-urlencoded的或者参考下request的getParts方法;

  2. 过滤器做兼容性处理即可。完整代码

    Request包装类

    @Slf4j
    public class RepeatBodyRequestWrapper extends HttpServletRequestWrapper {
    ​
        private final byte[] bodyByteArray;
    ​
        private final Map<String, String[]> parameterMap;
    ​
        public RepeatBodyRequestWrapper(HttpServletRequest request) {
            super(request);
            this.bodyByteArray = getByteBody(request);
            this.parameterMap = super.getParameterMap();
        }
    ​
        @Override
        public BufferedReader getReader() {
            return ObjectUtils.isEmpty(this.bodyByteArray) ? null
                    : new BufferedReader(new InputStreamReader(getInputStream()));
        }
    ​
        @Override
        public ServletInputStream getInputStream() {
            final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.bodyByteArray);
            return new ServletInputStream() {
                @Override
                public boolean isFinished() {
                    return false;
                }
    ​
                @Override
                public boolean isReady() {
                    return false;
                }
    ​
                @Override
                public void setReadListener(ReadListener readListener) {
                    // doNoting
                }
    ​
                @Override
                public int read() {
                    return byteArrayInputStream.read();
                }
            };
        }
    ​
        private static byte[] getByteBody(HttpServletRequest request) {
            byte[] body = new byte[0];
            try {
                body = StreamUtils.copyToByteArray(request.getInputStream());
            } catch (IOException e) {
                log.error("解析流中数据异常", e);
            }
            return body;
        }
    ​
        /**
         * 重写 getParameterMap() 方法 解决 undertow 中流被读取后,会进行标记,从而导致无法正确获取 body 中的表单数据的问题
         *
         * @return Map<String, String [ ]> parameterMap
         */
        @Override
        public Map<String, String[]> getParameterMap() {
            return this.parameterMap;
        }
    }
    

    过滤器处理

    @Component
    @WebFilter(urlPatterns = "/*")
    public class RepeatBodyRequestWrapperFilter implements Filter {
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                             FilterChain filterChain) throws IOException, ServletException {
    ​
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
            String contentType = httpServletRequest.getContentType();
            if (contentType == null) {
                // 表单请求
                filterChain.doFilter(servletRequest, servletResponse);
            } else if (contentType.startsWith("multipart/")) {
                // 文件上传类型
                filterChain.doFilter(servletRequest, servletResponse);
            } else if (contentType.startsWith("application/json")) {
                // json请求
                ServletRequest requestWrapper =
                        new RepeatBodyRequestWrapper((HttpServletRequest) servletRequest);
                filterChain.doFilter(requestWrapper, servletResponse);
            }
        }
    }
    

    经过上述改造,即可解决该问题。