Spring Boot 2.5.x:所需的请求部分“文件”不存在

Man*_*tha 5 spring file-upload multipartform-data spring-mvc spring-boot

spring boot我有一个文件上传 api,在该版本下工作得很好2.1.13。版本升级到 后2.5.2,开始抛出异常。查看更改日志,我看不到任何与处理相关的重大更改Multipart。我在这里可能会缺少什么?以下是我的示例代码。

例外

org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present
    at org.springframework.web.servlet.mvc.method.annotation.RequestPartMethodArgumentResolver.resolveArgument(RequestPartMethodArgumentResolver.java:161) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121) ~[spring-web-5.3.8.jar:5.3.8]
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:170) ~[spring-web-5.3.8.jar:5.3.8]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137) ~[spring-web-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1063) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) [spring-webmvc-5.3.8.jar:5.3.8]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) [spring-webmvc-5.3.8.jar:5.3.8]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:681) [tomcat-embed-core-9.0.48.jar:4.0.FR]
Run Code Online (Sandbox Code Playgroud)

应用程序属性

spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=20MB
Run Code Online (Sandbox Code Playgroud)

控制器端点

@PostMapping(
    value = "/upload", 
    consumes = MediaType.MULTIPART_FORM_DATA_VALUE, 
    produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<Object> uploadFile(@RequestPart("file") MultipartFile file) {
...
}
Run Code Online (Sandbox Code Playgroud)

请求有效负载样本

POST http://localhost:8080/upload
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryGG9dgUb5THDV0eDB

------WebKitFormBoundaryGG9dgUb5THDV0eDB
Content-Disposition: form-data; name="file"; filename="Sample.pdf"
Content-Type: application/pdf

------WebKitFormBoundaryGG9dgUb5THDV0eDB--
Run Code Online (Sandbox Code Playgroud)

注意:MultipartResolver我的配置中没有定义任何bean。我尝试按如下方式添加 MultipartResolver bean 定义(一次仅一个),但似乎没有解决问题。

@Bean
public CommonsMultipartResolver multipartResolver() { // didn't work
    return new CommonsMultipartResolver();
}

@Bean
public StandardServletMultipartResolver multipartResolver() { // didn't work
    return new StandardServletMultipartResolver();
}
Run Code Online (Sandbox Code Playgroud)

Man*_*tha 11

事实证明,这个问题是在Spring Boot 2.2. HttpHiddenMethodFilter从该版本开始,默认情况下禁用过滤器。启用过滤器后,问题得到解决application.properties

spring.mvc.hiddenmethod.filter.enabled=true
Run Code Online (Sandbox Code Playgroud)

我的详细发现

上述过滤器的目的与我收到的错误无关。但是请求parts被初始化作为执行过滤器的副作用。更具体地说,当过滤器尝试检索_method参数值时(e.g. request.getParameter("_method"),实例getParameter的方法request在内部似乎会解析参数,然后初始化请求parts。所以2.2版本禁用过滤器后spring-boot,就没有什么可以初始化请求了parts

我觉得请求parts初始化应该在Spring框架本身内修复。但在那之前,我们要么可以启用HttpHiddenMethodFilter过滤器,要么可以定义一个自定义过滤器来负责初始化请求parts,如下所示:

@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestInitializerFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        request.getParameterNames(); // this takes care of initializing request `parts`

        filterChain.doFilter(request, response);
    }
}
Run Code Online (Sandbox Code Playgroud)