Day611.SpringWebBody转化常见错误 -Spring编程常见错误

Posted 阿昌喜欢吃黄桃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day611.SpringWebBody转化常见错误 -Spring编程常见错误相关的知识,希望对你有一定的参考价值。

SpringWebBody转化常见错误

Spring 对于 Body 的处理很多是借助第三方编解码器来完成的。

例如常见的 JSON 解析,Spring 都是借助于 JacksonGson 等常见工具来完成。

所以在 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解析器
    
    @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);
            
        
    
    //省略其他非关键代码
    
    
    我们用自动注入的方式获取到 RequestMappingHandlerAdapter,然后找到 Jackson 解析器,进行配置即可。

三、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

Day Five

NSATP-A学习笔记之Day3-4常见注入类型

Day 3 python入门拾遗篇:bytes和str转化;三元运算;进制转化

每日一题 错选择 及 编程题 周总结

每日一题 错选择 及 编程题 周总结