SpringMVC对象解析的“浅坑”
SpringMVC这个坑主要原因不在curl,不过算是背景吧,也顺带记一下。如果想直接看curl的坑请跨过该“浅坑”,直接到后面的“深坑”。
最近在写一个接口,接受个推推送的回执数据,由于看文档那边推送的数据是json格式的,所以就想着直接用一个对象接受数据,如下:
public GetuiResponse getuiCallback(@RequestBody GetuiCallback getuiCallback)
测试都好好的,没想到一上线就跪了,抛下面异常:
org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/unknown;charset=UTF-8' not supported
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:237)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:150)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:128)
然后紧急回滚后开始排查问题。照着异常栈信息定位到SpringMVC的解析数据的代码:
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
MediaType contentType;
boolean noContentType = false;
try {
contentType = inputMessage.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
Class<?> contextClass = (parameter != null ? parameter.getContainingClass() : null);
Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
if (targetClass == null) {
ResolvableType resolvableType = (parameter != null ?
ResolvableType.forMethodParameter(parameter) : ResolvableType.forType(targetType));
targetClass = (Class<T>) resolvableType.resolve();
}
HttpMethod httpMethod = ((HttpRequest) inputMessage).getMethod();
Object body = NO_VALUE;
try {
inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage);
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
if (converter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
if (genericConverter.canRead(targetType, contextClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}
if (inputMessage.getBody() != null) {
inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
body = genericConverter.read(targetType, contextClass, inputMessage);
body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
}
break;
}
}
else if (targetClass != null) {
if (converter.canRead(targetClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}
if (inputMessage.getBody() != null) {
inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
}
break;
}
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("Could not read document: " + ex.getMessage(), ex);
}
if (body == NO_VALUE) {
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
(noContentType && inputMessage.getBody() == null)) {
return null;
}
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes); //异常抛自这里
}
return body;
}
观察上面代码可以看到,如果让SpringMVC来解析请求的数据,它会先遍历已有的转化器集合,里面包含JSON,XML等等转化器,如下图。不同的解析器会支持对应的MediaType,代码中会根据用户请求头中的ContentType值来做判断,确定具体使用哪种converter(详见converter.canRead(targetClass, contentType)
)。
所以问题就很清楚了,个推那边请求时,请求头中的ContentType给的值是application/unknown;charset=UTF-8
,跟我们想要的MappingJackson2HttpMessageConverter
所支持的application/json
,application/*+json
不匹配,所以没有定位到MappingJackson2HttpMessageConverter
。而且也成功绕过了所有的converter(因为没有一个MediaType是对应application/unknown;charset=UTF-8
这么坑的类型),所以最后抛异常了。
测试环境之所以没有测出来是因为我用swagger生成的curl默认给我加了--header 'Content-Type: application/json'
,毕竟swagger很规范,知道我请求的数据是json。
解决办法请接着往下看。
curl请求默认ContentType的“深坑”
上面的问题解决办法是改成这样接受请求public GetuiResponse getuiCallback(HttpServletRequest request)
,然后从request中拿到InputStream读出请求体JSON数据自己用ObjectMapper(jackson转化的一个类)转成对象。
写完后用POST man测试正常,想再用curl测试一下。考虑到之前是ContentType造成的问题,这次改成读流的方式就不传ContentType了。
大致请求是这样:
curl --request POST \
--url http://xxx/callback/getui \
--data '{数据保密}'
结果又发现服务端报:
com.fasterxml.jackson.databind.JsonMappingException: No content to map due to end-of-input
at [Source: com.enniu.cloud.service.server.filter.BaseServletRequestWrapper$ResettableServletInputStream@2fc8503; line: 1, column: 0]
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:270)
at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:3838)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3783)
调试后发现从request中的InputStream读出来的数据是空的,导致ObjectMapper转化空数据报错,很困惑,为啥postman测试都可以,用charles抓包发现请求体却是是有数据的,实在不知道问题出在哪。
于是开始在网上搜索原因,找到了这个:
根据Servlet规范,如果同时满足下列条件,则请求体(Entity)中的表单数据,将被填充到request的parameter集合中(request.getParameter系列方法可以读取相关数据):
1 这是一个HTTP/HTTPS请求2 请求方法是POST(querystring无论是否POST都将被设置到parameter中)
3 请求的类型(Content-Type头)是application/x-www-form-urlencoded
4 Servlet调用了getParameter系列方法
JSR-340 3.1.1
然后调试发现如果没有设置curl的ContentType,它会默认传application/x-www-form-urlencoded
,这样就导致request中的stream流已经被读完了,后面再读就是空的了。
于是curl请求改下,加个任意的ContentType头,这样就不满足Servlet规范就可以了。
curl --request POST \
--url http://xxx/callback/getui \
--header 'Content-Type: application/unknown;charset=UTF-8' \
--data '{数据保密}'
这下问题终于水落石出了,当然代码写的是没有问题的,因为个推那边不是用curl给我们推数据,只是测试过程中遇到这种坑还是陷的蛮深的,纠结了好长时间。
参考链接:
https://blog.csdn.net/liuyang755855737/article/details/79998716