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

Day611.SpringWebBody转化常见错误 -Spring编程常见错误,第1张

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.springframeworkgroupId>
    <artifactId>spring-webmvcartifactId>
    <version>5.2.3.RELEASEversion>
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.gsongroupId>
    <artifactId>gsonartifactId>
    <version>2.8.6version>
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.coregroupId>
     <artifactId>jackson-databindartifactId>
     <version>2.9.6version>
 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>myFilterfilter-name>
  <filter-class>com.puzzles.ReadBodyFilterfilter-class>
filter>
<filter-mapping>
  <filter-name>myFilterfilter-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.getBody();
   if (inputStream.markSupported()) {
      //省略其他非关键代码
   }
   else {
      PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
      int b = pushbackInputStream.read();
      if (b == -1) {
         this.body = null;
      }
      else {
         this.body = pushbackInputStream;
         pushbackInputStream.unread(b);
      }
   }
}
public InputStream getBody() {
   return (this.body != null ? this.body : StreamUtils.emptyInput());
}

Body 为空的判断是由 pushbackInputStream.read() 其值为 -1 来判断出的,即没有数据可以读取。

看到这里,你可能会有疑问:假设有 Body,read() 的执行不就把数据读取走了一点么?

确实如此,所以这里我使用了 pushbackInputStream.unread(b) 调用来把读取出来的数据归还回去,这样就完成了是否有 Body 的判断,又保证了 Body 的完整性。

分析到这里,再结合前面的案例,你应该能想到造成 Body 缺失的原因了吧?

  • 本身就没有 Body;
  • 有 Body,但是 Body 本身代表的流已经被前面读取过了。

很明显,我们的案例属于第 2 种情况,即在过滤器中,我们就已经将 Body 读取完了,关键代码如下:

//request 是 ServletRequest
String requestBody = IOUtils.toString(request.getInputStream(), “utf-8);

在这种情况下,作为一个普通的流,已经没有数据可以供给后面的转化器来读取了。


所以我们可以直接在Filter过滤器中去掉 Body 读取的代码,这样后续 *** 作就又能读到数据了。

但是这样又不满足我们的需求,如果我们坚持如此怎么办呢?对应的解决方案如下

定义一个 RequestBodyAdviceAdapter 的 Bean:他会在Body已经从数据流转化后进行执行

@ControllerAdvice
public class PrintRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter {
    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }
    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage,MethodParameter parameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType) {
        System.out.println("print request body in advice:" + body);
        return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
    }
}

我们可以看到方法 afterBodyRead 的命名,很明显,这里的 Body 已经是从数据流中转化过的。

那么它是如何工作起来的呢?我们可以查看下面的代码(参考 AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters):

protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType){
  //省略其他非关键代码  
  if (message.hasBody()) {
    HttpInputMessage msgToUse = getAdvice().beforeBodyRead(message,      parameter, targetType, converterType);
    //读取请求中的body数据流,并转化
    body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :                    ((HttpMessageConverter<T>)converter).read(targetClass, msgToUse));
    //转换完,找到对应的Advice,并执行他的afterBodyRead()
    body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
    //省略其他非关键代码  
   }
   //省略其他非关键代码          
   return body;
}

当一个 Body 被解析出来后,会调用 getAdvice() 来获取 RequestResponseBodyAdviceChain;

然后在这个 Chain 中,寻找合适的 Advice 并执行。正好我们前面定义了 PrintRequestBodyAdviceAdapter,所以它的相关方法就被执行了。

从执行时机来看,此时 Body 已经解析完毕了,也就是说,传递给 PrintRequestBodyAdviceAdapter 的 Body 对象已经是一个解析过的对象,而不再是一个流了。

通过上面的 Advice 方案,我们满足了类似的需求,又保证了程序的正确执行。至于其他的一些方案,你可以来思考一下。


四、总结
  • 不同的 Body 需要不同的编解码器,而使用哪一种是协商出来的,协商过程大体如下:
    • 查看请求头中是否有 ACCEPT头,如果没有则可以使用任何类型;
    • 查看当前针对返回类型(即 Student 实例)可以采用的编码类型
    • 取上面两步获取的结果的交集来决定用什么方式返回。
  • 在非 Spring Boot 程序中,JSON 等编解码器不见得是内置好的,需要添加相关的 JAR 才能自动依赖上,而自动依赖的实现是通过检查 Class 是否存在来实现的:当依赖上相关的 JAR 后,关键的 Class 就存在了,响应的编解码器功能也就提供上了。
  • 不同的编解码器的实现(例如 JSON 工具 Jaskson 和 Gson)可能有一些细节上的不同,所以你一定要注意当依赖一个新的 JAR 时,是否会引起默认编解码器的改变,从而影响到一些局部行为的改变。
  • 在尝试读取 HTTP Body 时,你要注意到 Body 本身是一个流对象,不能被多次读取。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/876645.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-13
下一篇 2022-05-13

发表评论

登录后才能评论

评论列表(0条)

保存