Day611.SpringWebBody转化常见错误 -Spring编程常见错误
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day611.SpringWebBody转化常见错误 -Spring编程常见错误相关的知识,希望对你有一定的参考价值。
SpringWebBody转化常见错误
Spring 对于 Body 的处理很多是借助第三方编解码器来完成的。
例如常见的 JSON 解析,Spring 都是借助于 Jackson
、Gson
等常见工具来完成。
所以在 Body 处理中,我们遇到的很多错误都是第三方工具使用中的一些问题。
真正对于 Spring 而言,错误并不多,特别是 Spring Boot 的自动包装以及对常见问题的不断完善,让我们能犯的错误已经很少了。
不过,毕竟不是每个项目都是直接基于 Spring Boot 的。如下会记录一些常见的问题错误:
一、匹配不到JSON转换器问题
在直接用 Spring MVC
而非 Spring Boot 来编写 Web 程序时
我们基本都会遇到 “No converter found for return value of type
” 这种错误。实际上,我们编写的代码都非常简单,例如下面这段代码:
//定义的数据对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student
private String name;
private Integer age;
//定义的 API 借口
@RestController
public class HelloController
@GetMapping("/hi1")
public Student hi1()
return new Student("xiaoming", Integer.valueOf(12));
然后,我们的 pom.xml 文件也都是最基本的必备项,关键配置如下:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
代码十分的简单,就是直接标注@RestController
,告诉Spring是Json对象返回,同时在/hi1
的controller中直接return new Student("xiaoming", Integer.valueOf(12))
,但是代码执行发现会报错:No converter found for return value of type
那为什么会出现这样子的问题呢???
当我们的请求到达 Controller 层后,我们获取到了一个对象,即案例中的 new Student(“xiaoming”, Integer.valueOf(12)),那么这个对象应该怎么返回给客户端呢?
用 JSON 还是用 XML,还是其他类型编码?
此时就需要一个决策,我们可以先找到这个决策的关键代码所在,参考方法 AbstractMessageConverterMethodProcessor#writeWithMessageConverters:
HttpServletRequest request = inputMessage.getServletRequest();
//获取请求中指定的accept类型
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
//获取对应的编码类型
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
//如果body会空,或者没有可处理的编码类型,就抛异常
if (body != null && producibleTypes.isEmpty())
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
//根据上面的accept类型+解码类型,选择出一个适合处理这个body数据返回
List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes)
for (MediaType producibleType : producibleTypes)
if (requestedType.isCompatibleWith(producibleType))
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
如上代码主要处理步骤如下:
- 查看请求的头中是否有
ACCEPT 头
,如果没有则可以使用任何类型; - 查看当前针对返回类型(即 Student 实例)可以采用的
编码类型
; - 取上面两步获取结果的
交集
来决定用什么方式返回。
假设第 2 步中就没有找到合适的编码方式,则直接报案例中的错误,具体的关键代码行如下:
if (body != null && producibleTypes.isEmpty())
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
那么当前可采用的编码类型是怎么决策出来的呢?我们可以进一步查看方法 AbstractMessageConverterMethodProcessor#getProducibleMediaTypes:
protected List<MediaType> getProducibleMediaTypes(
HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType)
Set<MediaType> mediaTypes =
(Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes))
return new ArrayList<>(mediaTypes);
else if (!this.allSupportedMediaTypes.isEmpty())
List<MediaType> result = new ArrayList<>();
for (HttpMessageConverter<?> converter : this.messageConverters)
if (converter instanceof GenericHttpMessageConverter && targetType != null)
if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null))
result.addAll(converter.getSupportedMediaTypes());
else if (converter.canWrite(valueClass, null))
result.addAll(converter.getSupportedMediaTypes());
return result;
else
return Collections.singletonList(MediaType.ALL);
假设当前没有显式指定返回类型(例如给 GetMapping 指定 produces 属性),那么则会遍历所有已经注册的 HttpMessageConverter 查看是否支持当前类型,从而最终返回所有支持的类型。
那么这些 MessageConverter 是怎么注册过来的?
在 Spring MVC(非 Spring Boot)启动后,我们都会构建 RequestMappingHandlerAdapter 类型的 Bean 来负责路由和处理请求。
具体而言,当我们使用 时,我们会通过 AnnotationDrivenBeanDefinitionParser 来构建这个 Bean。
而在它的构建过程中,会决策出以后要使用哪些 HttpMessageConverter,相关代码参考 AnnotationDrivenBeanDefinitionParser#getMessageConverters:
messageConverters.add(createConverterDefinition(ByteArrayHttpMessageConverter.class, source));
RootBeanDefinition stringConverterDef = createConverterDefinition(StringHttpMessageConverter.class, source);
stringConverterDef.getPropertyValues().add("writeAcceptCharset", false);
messageConverters.add(stringConverterDef);
messageConverters.add(createConverterDefinition(ResourceHttpMessageConverter.class, source));
//省略其他非关键代码
if (jackson2Present)
Class<?> type = MappingJackson2HttpMessageConverter.class;
RootBeanDefinition jacksonConverterDef = createConverterDefinition(type, source);
GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source);
jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef);
messageConverters.add(jacksonConverterDef);
else if (gsonPresent) messageConverters.add(createConverterDefinition(GsonHttpMessageConverter.class, source));
//省略其他非关键代码
这里我们会默认使用一些编解码器,例如 StringHttpMessageConverter,但是像 JSON、XML 等类型,若要加载编解码,则需要 jackson2Present、gsonPresent 等变量为 true。
这里我们可以选取 gsonPresent 看下何时为 true,参考下面的关键代码行:
gsonPresent = ClassUtils.isPresent(“com.google.gson.Gson”, classLoader);
假设我们依赖了 Gson 包,我们就可以添加上 GsonHttpMessageConverter 这种转化器。
但是可惜的是,我们的案例并没有依赖上任何 JSON 的库,所以最终在候选的转换器列表里,并不存在 JSON 相关的转化器。
最终候选列表示例如下:
由此可见,并没有任何 JSON 相关的编解码器。
而针对 Student 类型的返回对象,上面的这些编解码器又不符合要求,所以最终走入了下面的代码行:
if (body != null && producibleTypes.isEmpty())
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
那知道了为什么会出现这样子的错误,那对于的解决方案是什么???
针对这个案例,有了源码的剖析,可以看出,不是每种类型的编码器都会与生俱来,而是根据当前项目的依赖情况决定是否支持。
要解析 JSON,我们就要依赖相关的包,所以这里我们可以以 Gson 为例修正下这个问题:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
我们添加了 Gson 的依赖到 pom.xml。
重新运行程序和测试案例,你会发现不再报错了。
另外,这里我们还可以查看下 GsonHttpMessageConverter 这种编码器是如何支持上 Student 这个对象的解析的。
二、无声无息改变的JSON解析器
可能还会发现在代码并未改动的情况下,返回结果不再和之前相同了。例如我们看下这段代码:
@RestController
public class HelloController
@PostMapping("/hi2")
public Student hi2(@RequestBody Student student)
return student;
上述代码接受了一个 Student 对象,然后原样返回。我们使用下面的测试请求进行测试:
POST http://localhost:8080/springmvc3_war/app/hi2
Content-Type: application/json
“name”: “xiaoming”
经过测试,我们会得到以下结果:
“name”: “xiaoming”
但是随着项目的推进,在代码并未改变时,我们可能会返回以下结果:
“name”: “xiaoming”,
“age”: null
即当 age 取不到值,开始并没有序列化它作为响应 Body 的一部分,后来又序列化成 null 作为 Body 返回了。
在什么情况下会如此?如何规避这个问题,保证我们的返回始终如一。
如上,即在后续的代码开发中,我们直接依赖或者间接依赖了新的 JSON 解析器,例如下面这种方式就依赖了 Jackson:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.6</version>
</dependency>
当存在多个 Jackson 解析器时,我们的 Spring MVC 会使用哪一种呢?如下是SpringMVC中决断
使用哪个来解析JSON的源码:
if (jackson2Present)
Class<?> type = MappingJackson2HttpMessageConverter.class;
RootBeanDefinition jacksonConverterDef = createConverterDefinition(type, source);
GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source);
jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef);
messageConverters.add(jacksonConverterDef);
else if (gsonPresent)
messageConverters.add(createConverterDefinition(GsonHttpMessageConverter.class, source));
在代码中,我们发现针对jackson
,SpringMVC中对于他的优先级要高于gson
;
也就是说,如果一开始使用gson
,后面因为某种原因引入了jackson
,那SpringMVC会在不知不觉中给我们更换了JSON的解析器
- 对于 Gson 而言:它是默认不序列化 null 的。
- 对于 Jackson 而言:默认对于 null 的处理是做序列化的
那知道了上面的问题和情况后,我们如何解决这种问题呢?
- 局部设置JSON解析器:@JsonInclude(JsonInclude.Include.NON_NULL)
@Data @NoArgsConstructor @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class Student private String name; //或直接加在 age 上:@JsonInclude(JsonInclude.Include.NON_NULL) private Integer age;
- 全局设置JSON解析器
在我们的案例中是针对SpringMVC的情况,而不是SpringBoot的项目,所以可以通过如下的方式来设置对应的JSON解析器
我们用自动注入的方式获取到 RequestMappingHandlerAdapter,然后找到 Jackson 解析器,进行配置即可。@RestController public class HelloController public HelloController(RequestMappingHandlerAdapter requestMappingHandlerAdapter) List<HttpMessageConverter<?>> messageConverters = requestMappingHandlerAdapter.getMessageConverters(); for (HttpMessageConverter<?> messageConverter : messageConverters) if(messageConverter instanceof MappingJackson2HttpMessageConverter ) (((MappingJackson2HttpMessageConverter)messageConverter).getObjectMapper()).setSerializationInclusion(JsonInclude.Include.NON_NULL); //省略其他非关键代码
三、Required request body is missing问题
通过案例 1,我们已经能够解析 Body 了,但是有时候,我们会有一些很好的想法。
例如为了查询问题方便,在请求过来时,自定义一个 Filter 来统一输出具体的请求内容,关键代码如下:
public class ReadBodyFilter implements Filter
//省略其他非关键代码
@Override
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
throws IOException, ServletException
String requestBody = IOUtils.toString(request.getInputStream(), "utf-8");
System.out.println("print request body in filter:" + requestBody);
chain.doFilter(request, response);
然后,我们可以把这个 Filter 添加到 web.xml 并配置如下:
<filter>
<filter-name>myFilter</filter-name>
<filter-class>com.puzzles.ReadBodyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/app/*</url-pattern>
</filter-mapping>
再测试下 Controller 层中定义的接口:
@PostMapping("/hi3")
public Student hi3(@RequestBody Student student)
return student;
运行测试,我们会发现下面的日志:
print request body in filter:
“name”: “xiaoming”,
“age”: 10
25-Mar-2021 11:04:44.906 璀﹀憡 [http-nio-8080-exec-5] org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.logException Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public com.puzzles.Student com.puzzles.HelloController.hi3(com.puzzles.Student)]
可以看到,请求的 Body 确实在请求中输出了,但是后续的操作直接报错了,错误提示:Required request body is missing。
那为什么经过一个Filter过滤器就让Body丢失了呢???
查阅请求 Body 转化的相关代码,有这样一段关键逻辑(参考 RequestResponseBodyMethodProcessor#readWithMessageConverters):
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
//读取 Body 并进行转化
Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
//@RequestBody 且是必须时,如果解析出的 Body 为 null,则报错提示 Required request body is missing
if (arg == null && checkRequired(parameter))
throw new HttpMessageNotReadableException("Required request body is missing: " +
parameter.getExecutable().toGenericString(), inputMessage);
return arg;
protected boolean checkRequired(MethodParameter parameter)
RequestBody requestBody = parameter.getParameterAnnotation(RequestBody.class);
return (requestBody != null && requestBody.required() && !parameter.isOptional());
当使用了 @RequestBody 且是必须时,如果解析出的 Body 为 null,则报错提示 Required request body is missing。
所以我们要继续追踪代码,来查询什么情况下会返回 body 为 null。关键代码参考 AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters:
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType)
//省略非关键代码
Object body = NO_VALUE;
EmptyBodyCheckingHttpInputMessage message;
try
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
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())
//省略非关键代码:读取并转化 body
else
//处理没有 body 情况,默认返回 null
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
break;
catch (IOException ex)
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
//省略非关键代码
return body;
当 message 没有 body 时( message.hasBody() 为 false ),则将 body 认为是 null。继续查看 message 本身的定义,它是一种包装了请求 Header 和 Body 流的 EmptyBodyCheckingHttpInputMessage 类型。其代码实现如下:
public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException
this.headers = inputMessage.getHeaders();
InputStream inputStream = inputMessage.五一 DAY 4