内容类型 multipart/mixed 不支持 / 415 UNSUPPORTED_MEDIA_TYPE (在 Spring + Spring 启动升级后)

Content type multipart/mixed not supported / 415 UNSUPPORTED_MEDIA_TYPE (after Spring + Spring Boot upgrade)

我之前使用的是以下版本的 Spring + Spring Boot:

<springVersion>5.0.16.RELEASE</springVersion>
<springBootVersion>2.0.5.RELEASE</springBootVersion>

以及下面的 类,它处理了 multipart/mixed 上传。使用上面的 Spring 版本一切正常:

StagingApi.java

import io.swagger.annotations.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.Valid;
import javax.validation.constraints.*;
@javax.annotation.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2022-05-10T11:14:13.587+01:00[Europe/London]")
@Api(value = "Staging", description = "API")
public interface StagingApi {

    @ApiOperation(value = "", nickname = "createOrReplaceBatch", notes = "", tags={  })
    @ApiResponses(value = { 
        @ApiResponse(code = 200, message = ""),
        @ApiResponse(code = 400, message = ""),
        @ApiResponse(code = 500, message = "") })
    @RequestMapping(value = "/batches/{batchId}",
        consumes = { "multipart/mixed" },
        method = RequestMethod.PUT)
    ResponseEntity<Void> createOrReplaceBatch(
        @ApiParam(value = "" ,required=true) @RequestHeader(value="X-TENANT-ID", required=true) String X_TENANT_ID,
        @Size(min=1) @ApiParam(value = "",required=true) @PathVariable("batchId") String batchId,
        @ApiParam(value = ""  )  @Valid @RequestBody Object body);
}

StagingController.java

public ResponseEntity<Void> createOrReplaceBatch(
    @ApiParam(value = "Identifies the tenant making the request.", required = true)
    @RequestHeader(value = "X-TENANT-ID", required = true) String X_TENANT_ID,
    @Size(min = 1) @ApiParam(value = "Identifies the batch.", required = true)
    @PathVariable("batchId") String batchId,
    Object body)
{

    final ServletFileUpload fileUpload = new ServletFileUpload();
    final FileItemIterator fileItemIterator;
    try {
        fileItemIterator = fileUpload.getItemIterator(request);
    } catch (final FileUploadException | IOException ex) {
        LOGGER.error("Error getting FileItemIterator", ex);
        throw new WebMvcHandledRuntimeException(HttpStatus.BAD_REQUEST, ex.getMessage());
    }
    try {
        batchDao.saveFiles(new TenantId(X_TENANT_ID), new BatchId(batchId), fileItemIterator);
        return new ResponseEntity<>(HttpStatus.OK);
    } catch (final InvalidTenantIdException | InvalidBatchIdException | IncompleteBatchException | InvalidBatchException ex) {
        throw new WebMvcHandledRuntimeException(HttpStatus.BAD_REQUEST, ex.getMessage());
    } catch (final StagingException ex) {
        throw new WebMvcHandledRuntimeException(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
    }
}

成功处理请求的日志:

[2022-05-10 11:12:14.861Z #bc7.042 DEBUG -            -   ] o.s.w.s.m.m.a.RequestMappingHandlerMapping: Looking up handler method for path /batches/test-batch
[2022-05-10 11:12:14.861Z #bc7.042 DEBUG -            -   ] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.861Z #bc7.042 DEBUG -            -   ] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.862Z #bc7.042 DEBUG -            -   ] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.863Z #bc7.042 DEBUG -            -   ] o.s.w.s.m.m.a.RequestMappingHandlerMapping: Returning handler method [public org.springframework.http.ResponseEntity<java.lang.Void> com.acme.corp.staging.StagingController.createOrReplaceBatch(java.lang.String,java.lang.String,java.lang.Object)]
[2022-05-10 11:12:14.863Z #bc7.042 DEBUG -            -   ] o.s.b.f.s.DefaultListableBeanFactory: Returning cached instance of singleton bean 'stagingController'
[2022-05-10 11:12:14.866Z #bc7.042 DEBUG -            -   ] o.s.b.w.s.f.OrderedRequestContextFilter: Bound request context to thread: org.apache.catalina.connector.RequestFacade@37f2254a
[2022-05-10 11:12:14.870Z #bc7.042 DEBUG -            -   ] o.s.w.s.DispatcherServlet: DispatcherServlet with name 'dispatcherServlet' processing PUT request for [/batches/test-batch]
[2022-05-10 11:12:14.871Z #bc7.042 DEBUG -            -   ] o.s.w.s.m.m.a.RequestMappingHandlerMapping: Looking up handler method for path /batches/test-batch
[2022-05-10 11:12:14.871Z #bc7.042 DEBUG -            -   ] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.871Z #bc7.042 DEBUG -            -   ] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.871Z #bc7.042 DEBUG -            -   ] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.871Z #bc7.042 DEBUG -            -   ] o.s.w.s.m.m.a.RequestMappingHandlerMapping: Returning handler method [public org.springframework.http.ResponseEntity<java.lang.Void> com.acme.corp.staging.StagingController.createOrReplaceBatch(java.lang.String,java.lang.String,java.lang.Object)]
[2022-05-10 11:12:14.871Z #bc7.042 DEBUG -            -   ] o.s.b.f.s.DefaultListableBeanFactory: Returning cached instance of singleton bean 'stagingController'
[2022-05-10 11:12:14.932Z #bc7.042 DEBUG -            10c9] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.933Z #bc7.042 DEBUG -        -       ] o.s.w.s.DispatcherServlet: Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapter completed request handling
[2022-05-10 11:12:14.933Z #bc7.042 DEBUG -        -       ] o.s.w.s.DispatcherServlet: Successfully completed request

但是,当我尝试更新 Spring 和 Spring 的版本时 Boot to:

<springVersion>5.2.4.RELEASE</springVersion>
<springBootVersion>2.2.6.RELEASE</springBootVersion>

相同的请求失败并返回 415 UNSUPPORTED_MEDIA_TYPE 响应。

请求失败日志:

[2022-05-10 11:31:52.158Z #dc2.024 INFO  -            -   ] o.a.c.c.C..localhost.: Initializing Spring DispatcherServlet 'dispatcherServlet'
[2022-05-10 11:31:52.158Z #dc2.024 INFO  -            -   ] o.s.w.s.DispatcherServlet: Initializing Servlet 'dispatcherServlet'
[2022-05-10 11:31:52.158Z #dc2.024 DEBUG -            -   ] o.s.w.s.DispatcherServlet: Detected StandardServletMultipartResolver
[2022-05-10 11:31:52.162Z #dc2.024 DEBUG -            -   ] o.s.w.s.DispatcherServlet: enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
[2022-05-10 11:31:52.162Z #dc2.024 INFO  -            -   ] o.s.w.s.DispatcherServlet: Completed initialization in 4 ms
[2022-05-10 11:31:52.165Z #dc2.024 DEBUG -            -   ] o.s.w.s.DispatcherServlet: PUT "/batches/test-batch", parameters={}
[2022-05-10 11:31:52.192Z #dc2.024 DEBUG -            -   ] o.s.w.s.m.m.a.RequestMappingHandlerMapping: Mapped to com.acme.corp.staging.StagingController#createOrReplaceBatch(String, String, Object)
[2022-05-10 11:31:52.202Z #dc2.024 DEBUG -            a17e] o.s.w.s.m.m.a.ServletInvocableHandlerMethod: Could not resolve parameter [2] in public org.springframework.http.ResponseEntity<java.lang.Void> com.acme.corp.staging.StagingController.createOrReplaceBatch(java.lang.String,java.lang.String,java.lang.Object): Content type 'multipart/mixed;boundary=efb8369b-607b-4dcf-9f92-e6cd8244db1e;charset=UTF-8' not supported
[2022-05-10 11:31:52.204Z #dc2.024 DEBUG -            a17e] o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver: Using @ExceptionHandler acme.corp.staging.exceptions.WebMvcExceptionHandler#handleException(Exception, WebRequest)
[2022-05-10 11:31:52.206Z #dc2.024 DEBUG -            a17e] o.s.w.s.m.m.a.HttpEntityMethodProcessor: No match for [application/json, */*], supported: []
[2022-05-10 11:31:52.206Z #dc2.024 DEBUG -            a17e] o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver: Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'multipart/mixed;boundary=efb8369b-607b-4dcf-9f92-e6cd8244db1e;charset=UTF-8' not supported]
[2022-05-10 11:31:52.206Z #dc2.024 DEBUG -            a17e] o.s.w.s.DispatcherServlet: Completed 415 UNSUPPORTED_MEDIA_TYPE

调试这个,这是我发现的:

当我向上述端点发送 PUT 请求时,使用最新的 Spring 版本,调用 AbstractMessageConverterMethodArgumentResolver

它循环遍历消息转换器列表,并检查是否有任何转换器 canRead 请求:

for (HttpMessageConverter<?> converter : this.messageConverters) {
    Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
    GenericHttpMessageConverter<?> genericConverter =
            (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
    if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
            (targetClass != null && converter.canRead(targetClass, contentType))) {
        if (message.hasBody()) {
            HttpInputMessage msgToUse =
                    getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
            body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                    ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
            body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
        }
        else {
            body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
        }
        break;
    }
}
}

targetTypetargetClass 变量是 java.lang.Object。

这是循环遍历的消息转换器列表:

这是contentType的值:

所以,当被问到时,这些转换器中的每一个都返回 false

canRead(targetClass=java.lang.Object, contentType=multipart/mixed; boundary=efb8369b-607b-4dcf-9f92-e6cd8244db1e;charset=UTF-8

这导致 body 没有得到一个值,并且 HttpMediaTypeNotSupportedException 被抛出:

if (body == NO_VALUE) {
    if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
            (noContentType && !message.hasBody())) {
        return null;
    }
    throw new HttpMediaTypeNotSupportedException(contentType,
            getSupportedMediaTypes(targetClass != null ? targetClass : Object.class));
}

我尝试使用原始 post 中列出的较旧 Spring 版本逐步执行相同的代码(例如,为了查看较早的 Spring 版本是否具有更多消息转换器):

<springVersion>5.0.16.RELEASE</springVersion>
<springBootVersion>2.0.5.RELEASE</springBootVersion>

但我发现循环遍历消息转换器的代码不是使用这些较旧的 Spring 版本执行的

我不确定这些 Spring 版本之间有什么变化,现在正在执行这段代码(并抛出异常),而以前没有?

请参阅 spring 中的 @wilkinsona answer 打开的启动问题:

Spring Framework 5.1 also supports multipart PUT requests. This means that Spring MVC is now reading the body, making it unavailable to commons upload.

You should switch to using MVC's built-in multipart support. There are a few different ways to do that, for example:

@RestController
public class StagingController implements StagingApi
{    
    public ResponseEntity<Void> createOrReplaceBatch(@PathVariable("batchId") String batchId, MultipartHttpServletRequest request)
    {
        try {
            Collection<Part> parts = request.getParts();