目录
环境信息
问题描述
错误分析
解决方法
简单参数
总结
环境信息
Spring Boot:2.0.8.RELEASE
Spring Boot内置的tomcat:tomcat-embed-core 8.5.37
问题描述
收到文件上传的开发工作,要求能适配各种场景,并且各场景的请求参数不一样,因此接收的参数不能是固定的几个字段,要有类似Map的字段来接收动态参数。
拟使用MultipartFile[] files来接收文件列表,用自定义对象UploadFileDto来接收上传参数(里面包含一个Map)
UploadFileDto.java
@ApiModel("文件上传dto")@Datapublic class UploadFileDto { @ApiModelProperty("处理器,在Spring容器里的名称") private String handlerName; @ApiModelProperty("交易代码") private String bizCode; @ApiModelProperty("交易ID") private String bizId; @ApiModelProperty("上传配置名称,对应配置文件里的配置") private String uploadConfigName; @ApiModelProperty("动态信息") private Map dynamicDto; @ApiModelProperty("上传时间") private String uploadDt; @ApiModelProperty("限定文件大小") private long defaultFileSize; @ApiModelProperty("限定文件格式,多个用逗号隔开") private String defaultFileSuffix; @ApiModelProperty("默认路径之后的文件上级目录") private String parentDir; @ApiModelProperty("备注信息") private String remark; @ApiModelProperty("上传后的文件信息") private List fileInfoDtos;}
Controller:
@RestController@RequestMapping("/fileCommmon")@Api(tags = "公共-文件操作")public class FileUpDownLoadController extends BaseController { @Resource private UpDownLoadService upDownLoadService; @PostMapping(value = "/upload") @ApiOperation("文件上传") public ResponseDto uploadFile(@RequestParam("files") MultipartFile[] files, @RequestPart() UploadFileDto uploadFileDto) { upDownLoadService.uploadFile(uploadFileDto, files); return getResponseDto(uploadFileDto); }}
Controller方法,使用@RequestParam(“files”) MultipartFile[] files来接收文件列表
使用@RequestPart() UploadFileDto uploadFileDto来接收复杂参数。
UpDownLoadService是上传实现类,这里暂时不暂时源码。
可是,在访问该接口的时候,报错了:org.springframework.web.HttpMediaTypeNotSupportedException: Content type ‘application/octet-stream’ not supported
org.springframework.web.HttpMediaTypeNotSupportedException: Content type ‘application/octet-stream’ not supported
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:226)
at org.springframework.web.servlet.mvc.method.annotation.RequestPartMethodArgumentResolver.resolveArgument(RequestPartMethodArgumentResolver.java:134)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:124)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:161)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:131)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:891)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:981)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:884)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:858)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
错误分析
org.springframework.web.HttpMediaTypeNotSupportedException: Content type ‘application/octet-stream’ not supported,意思是应用不支持’application/octet-stream’这种文件类型
Swagger2里的请求参数:
分析异常堆栈,是在AbstractMessageConverterMethodArgumentResolver的readWithMessageConverters方法里抛出的:
分析这个方法里的代码,发现Spring在尝试使用各个消息转换器(HttpMessageConverter)来处理请求。这里的contentType值是’application/octet-stream’,发现找不到可用的消息转换器:
genericConverter.canRead(targetType, contextClass, contentType) ,因此无法解析、转换消息,因此抛出了异常。
确切的说,上面说的是处理请求参数uploadFileDto,第一个请求参数files已经在下面这个地方处理了(文件类型参数使用MultipartResolutionDelegate.resolveMultipartArgument来处理):
注:
messageConverters的值(这个看各个应用里注册的消息转换器类型、顺序):
经常使用的是MappingJackson2HttpMessageConverter消息转换器,这个是SpringBoot默认用来处理消息的,将原始请求转成json格式,再转成目标类型的格式,是Jackson
targetClass是UploadFileDto对应的Class
解决方法
明白了问题产生的原因之后,就有了解决思路:增加一个消息转换器,用于支持’application/octet-stream’。
最简单的添加方式,新增一个类,继承AbstractJackson2HttpMessageConverter,这个也是上面说的MappingJackson2HttpMessageConverter的父类
@Componentpublic class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter { /** * Converter for support http request with header Content-Type: multipart/form-data */ public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) { super(objectMapper, MediaType.APPLICATION_OCTET_STREAM); } @Override public boolean canWrite(Type type, Class 读取之后的body类型就是UploadFileDto
至此,问题解决,后端程序能够正常接收、处理前端发过来的文件列表,以及复杂参数。
简单参数
如果后端程序不需要接收复杂参数,而是只需要固定的几个简单参数,那么就很简单,比如:
Controller:
@PostMapping(value = "/upload2") @ApiOperation("文件上传2") public ResponseDto uploadFile(@RequestParam("files") MultipartFile[] files, @RequestPart() String remark) { System.out.println("remark = " + remark); upDownLoadService.uploadFile(remark, files); return getResponseDto(remark); }
此时,contentType值是'application/octet-stream'的String的参数,Spring有提供了默认的消息转换器StringHttpMessageConverter,支持所有的格式,就不需要自定义MappingJackson2HttpMessageConverter消息转换器了。
总结
回顾一下,如果需要复杂参数(自定义对象接收前端参数),那么需要自定义消息转换器(如AbstractJackson2HttpMessageConverter),来支持contentType值是'application/octet-stream'类型的参数,并将其转换成目标格式;
如果不需要复杂参数,只是String等类型,那么不需要自定义消息转换器;
消息转换器是SpringBoot处理前端传输的数据,并转换成接口参数的类型的转换器,转换前、转换后还支持自定义插件处理。详见AbstractMessageConverterMethodArgumentResolver的readWithMessageConverters
@RequestParam和@RequestPart的区别,可以自行查找资料
参考:
rest - @RequestPart with mixed multipart request, Spring MVC 3.2 - Stack Overflow
@RequestPart同时接收文件和json后端报错 - 简书 (jianshu.io)