SpringMVC,SpringBoot统一响应结果返回值为String的异常处理原理分析

案例

@RestController
@RequestMapping("test")
public class TestController {
    @GetMapping(value = "string")
    public String test(HttpServletRequest request) {
        return "Hello Luck" + IPUtils.getClientAddress(request);
    }
}

先贴出异常结果

统一响应结果处理类

@ControllerAdvice
@Slf4j
public class ResultResponseBody implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 如果返回结果
        Method method = returnType.getMethod();
        // 返回格式是否是标准格式
        assert method != null;
        boolean isResult = method.getReturnType() == Result.class;
        // 获取类中是否有RestController注解
        boolean presentController = method.isAnnotationPresent(ResponseBody.class);
        // 获取类中是否有RestController注解
        boolean restController = returnType.getDeclaringClass().isAnnotationPresent(RestController.class);
        // 支持修改结果
        return isResult || presentController || restController;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (Tools.isEmpty(body) || !(body instanceof Result)) {
           return Result.OK(body);
        }
        if (Tools.isNotEmpty(body)) {
            return body;
        }
        return body;
}

直接给解决方案,着急就先走,原理在后面,最好看原理解决问题

 前提
  • 在使用HttpMessageConverter时候,是按照顺序遍历的,返回值为String默认都是由StringHttpMessageConverter来处理
  • 所以,我们只需要将MappingJackson2HttpMessageConverter设置到StringHttpMessageConverter之前就ok,可以new
  • 也可以从converters中移动到StringHttpMessageConverter就OK
@Component
public class A implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 其实,converters已经包含了MappingJackson2HttpMessageConverter,但是它的顺序默认是在StringHttpMessageConverter之后
        // 在使用HttpMessageConverter时候,是按照顺序遍历的,返回值为String默认都是是有StringHttpMessageConverter来处理
        // 所以,我们只需要将MappingJackson2HttpMessageConverter设置到StringHttpMessageConverter之前就ok,可以new,也可以从converters中移动到StringHttpMessageConverter就OK
       converters.add(0, new MappingJackson2HttpMessageConverter());
    }
 }   

方案一,在请求中指定,表示当前接口能处理的就是JSON类型

@GetMapping(value = "string", produces = MediaType.APPLICATION_JSON_VALUE)
public String test(HttpServletRequest request) {
    return "Hello Luck" + IPUtils.getClientAddress(request);
}

方案二

  • 在实现WebMvcConfigurer接口中添加@EnableWebMvc注解

原因

  • 当前请求的请求头中,支持的响应类型与SpringMVC处理响应消息的消息转换器匹配到了一个不符合预期的类
  • Accept:表示客户端(浏览器)可以接受的文件类型或内容类型,具体来说,Accept请求头用于告诉服务器客户端希望接收哪些媒体类型的响应

源码分析 

我们标注了@RequestController或者@ResponseBody最终的响应结果是RequestResponseBodyMethodProcessor处理,具体的不讲,不是这里的重点
RequestResponseBodyMethodProcessor implements HandlerMethodReturnValueHandler,它是一个返回值处理器
在请求执行的过程中,org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle方法中,会调用到
// private HandlerMethodReturnValueHandlerComposite returnValueHandlers; 它是一个HandlerMethodReturnValue组合,内部包含了多个HandlerMethodReturnValue
// private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<>();
// 这句话就是在ServletInvocableHandlerMethod#invokeAndHandle调用的,从而会执行RequestResponseBodyMethodProcessor的handleReturnValue
this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor{
    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
       // 只讲核心逻辑,这里是使用所有的消息转换器List<HttpMessageConverter<?>> messageConverters;来处理
       writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);{
           // writeWithMessageConverters方法内部核心逻辑
           // getAcceptableMediaTypes就是获取请求头Accept中支持的类型,上面图中有
           List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
           // getProducibleMediaTypes就是遍历所有的消息转换器,找到能处理当前请求的方法(controller的方法)返回值的消息转换器
           // 将所有能处理该请求返回值的消息转换器对应的supportedMediaTypes保存起来,源码如下
           List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);{
               List<MediaType> result = new ArrayList<>();
               // 遍历所有的消息转换器,这个messageConverters在下面单独讲,有点多
                for (HttpMessageConverter<?> converter : this.messageConverters) {
                   if (converter instanceof GenericHttpMessageConverter && targetType != null) {
                        // 找到能处理当前方法返回值的消息转换器
                      if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
                          // 保存该消息转换器能处理的类型
                         result.addAll(converter.getSupportedMediaTypes());
                      }
                   }
                }
                return result;           
           }   
           List<MediaType> mediaTypesToUse = new ArrayList<>();
           // 这里使用双层遍历,找到同时兼容浏览器期望的类型与SpringMVC能处理的类型
           for (MediaType requestedType : acceptableTypes) {
               for (MediaType producibleType : producibleTypes) {
                   // 判断两者是否兼容
                  if (requestedType.isCompatibleWith(producibleType)) {
                      // 如果兼容,表示当前媒体类型是可用的
                     mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
                  }
               }
          }
          // 没有找到兼容请求和响应一致的类型就没办法处理该请求
        if (mediaTypesToUse.isEmpty()) {
           if (body != null) {
              throw new HttpMediaTypeNotAcceptableException(producibleTypes);
           }
           return;
        }   
       // 可能同时存在多个兼容的组合,这里只需要找到一种就行 
       for (MediaType mediaType : mediaTypesToUse) {
            // 问题马上出现了,如果是具体的媒体类型,那么就选择该类型
            // 我这里selectedMediaType选择的是text/html,你们的可能不是这个,都大同小异,不是自己期待的
            // 后面我会贴DEBUG图,自己期待的selectedMediaType是appliction/json,才不会报错
           if (mediaType.isConcrete()) {
              selectedMediaType = mediaType;
              break;
            }
          }        
       }
       // 上面找到了selectedMediaType可用的响应类型,那么就可以直接找到对应的消息处理器来处理响应结果了
       // 这个messageConverters在下面单独讲,有点多
       for (HttpMessageConverter<?> converter : this.messageConverters) {
         GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
         // 判断当前HttpMessageConverter能不能处理方法的返回类型以及响应的媒体类型    
         // 上面我们找到了selectedMediaType是text/html记得吧,它对应能处理的HttpMessageConverter是StringHttpMessageConverter
         if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) {
           // 如果能处理,就获取实现了ResponseBodyAdvice处理类,就是我们上面处理统一结果的自定义类,调用beforeBodyWrite
           // 最终返回处理后的结果body,此时经过我们自定义类中的beforeBodyWrite返回结果return Result.OK(body);替代了原来方法返回的String
           body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,(Class<? extends HttpMessageConverter<?>>) converter.getClass(),inputMessage, outputMessage);
           if (body != null) {
               // 因为上面已经判断canWrite为true,才能进入这里,所以调用HttpMessageConverter的write方法
               // 这里调用的就是StringHttpMessageConverter
               // 在StringHttpMessageConverter中,有一行代码报错
               genericConverter.write(body, targetType, selectedMediaType, outputMessage);{
                  if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
                      // 获取内容长度,当前t就是我们上面的body,是一个泛型,t期望的是一个String类型,但是实际传递的一个Result类型
                      // 所以导致获取内容长度,将t(body=Result)强转为String肯定抛出强转异常
                       Long contentLength = getContentLength(t, headers.getContentType());
                       if (contentLength != null) {
                          headers.setContentLength(contentLength);
                       }
                    }                
               }
           }
       }
    }    
}

出错之前DEBUG图

 

 

 

List<HttpMessageConverter<?>> messageConverters消息转换器的来源

第一种情况,只引入SpringMVC,并且没有配置单独的RequestMappingHandlerAdapter的Bean
那么SpringMVC会创建默认的RequestMappingHandlerAdapter,会给定5个默认的消息处理器
public RequestMappingHandlerAdapter() {
   this.messageConverters = new ArrayList<>(4);
   this.messageConverters.add(new ByteArrayHttpMessageConverter());
   this.messageConverters.add(new StringHttpMessageConverter());
   this.messageConverters.add(new SourceHttpMessageConverter<>());
   this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}

第二种情况,不管是在SpringBoot或者SpringMVC中
使用了@EnableWebMvc注解,该注解会导入@Import(DelegatingWebMvcConfiguration.class)这个类,
会自动注册RequestMappingHandlerAdapter的Bean具体代码在父类中WebMvcConfigurationSupport,并给RequestMappingHandlerAdapter设置messageConverters
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
      @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
      @Qualifier("mvcConversionService") FormattingConversionService conversionService,
      @Qualifier("mvcValidator") Validator validator) {

   RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
   adapter.setMessageConverters(getMessageConverters(){
       if (this.messageConverters == null) {
           this.messageConverters = new ArrayList<>();
           // 这里就是处理执行所有实现了WebMvcConfigurer接口的configureMessageConverters方法
           // 这就是有些人这样写有用的原因,class A implements WebMvcConfigurer{
           //   @Override    
           //    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { 
           //       converters.add(0, new MappingJackson2HttpMessageConverter());       
           //    }    
           // }
           configureMessageConverters(this.messageConverters);
           // 如果没有WebMvcConfigurer接口提供messageConverters,那么就添加默认的messageConverters
           // 如果WebMvcConfigurer接口提供messageConverters,那么只有你提供的messageConverters
           if (this.messageConverters.isEmpty()) {
              addDefaultHttpMessageConverters(this.messageConverters);{
                messageConverters.add(new ByteArrayHttpMessageConverter());
                messageConverters.add(new StringHttpMessageConverter());
                messageConverters.add(new ResourceHttpMessageConverter());
                messageConverters.add(new ResourceRegionHttpMessageConverter());
                messageConverters.add(new SourceHttpMessageConverter<>());
                messageConverters.add(new AllEncompassingFormHttpMessageConverter());
                messageConverters.add(new AtomFeedHttpMessageConverter());
                messageConverters.add(new RssChannelHttpMessageConverter());
                messageConverters.add(new MappingJackson2XmlHttpMessageConverter(Jackson2ObjectMapperBuilder.xml().build()));
                messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
                messageConverters.add(new MappingJackson2HttpMessageConverter(Jackson2ObjectMapperBuilder.json().build()));
                messageConverters.add(new GsonHttpMessageConverter());
                messageConverters.add(new JsonbHttpMessageConverter());
                messageConverters.add(new MappingJackson2SmileHttpMessageConverter(Jackson2ObjectMapperBuilder.smile().build()));
                messageConverters.add(new MappingJackson2CborHttpMessageConverter(Jackson2ObjectMapperBuilder.cbor().build()));
           }
           // 和configureMessageConverters几乎一样,执行WebMvcConfigurer中的extendMessageConverters,对configureMessageConverters进一步扩展
           extendMessageConverters(this.messageConverters);
        }
        return this.messageConverters;   
   });
   ... 
   return adapter;
}

第三种情况,SpringBoot的情况下,没有使用到@EnableWebMvc
依赖会自动导入这个EnableWebMvcConfigurationBean,它继承了DelegatingWebMvcConfiguration和@EnableWebMvc导入的类一样,所以和@EnableWebMvc差不多
可能SpringBoot还有包还会导入一些对应的messageConverters,这些都不重要
@Configuration(proxyBeanMethods = false)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration{
    
}

解决方案

前提

  • 在使用HttpMessageConverter时候,是按照顺序遍历的,返回值为String默认都是是有StringHttpMessageConverter来处理

  • 所以,我们只需要将MappingJackson2HttpMessageConverter设置到StringHttpMessageConverter之前就ok,可以new

  • 也可以从converters中移动到StringHttpMessageConverter就OK

自定义一个WebMvcConfigurer的Bean,添加JSON的转换器
@Component
public class A implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 其实,converters已经包含了MappingJackson2HttpMessageConverter,但是它的顺序默认是在StringHttpMessageConverter之后
        // 在使用HttpMessageConverter时候,是按照顺序遍历的,返回值为String默认都是是有StringHttpMessageConverter来处理
        // 所以,我们只需要将MappingJackson2HttpMessageConverter设置到StringHttpMessageConverter之前就ok,可以new,也可以从converters中移动到StringHttpMessageConverter就OK
       converters.add(0, new MappingJackson2HttpMessageConverter());
    }
 }   

方案一,在请求中指定,表示当前接口能处理的就是JSON类型

@GetMapping(value = "string", produces = MediaType.APPLICATION_JSON_VALUE)
public String test(HttpServletRequest request) {
    return "Hello Luck" + IPUtils.getClientAddress(request);
}

如图,可用的媒体类型就只有JSON

 

 

方案二

  • 使用@EnableWebMvc注解,上面有@EnableWebMvc导入HttpMessageConverter的原理,这里不再赘述
  • 由于在@EnableWebMvc导入的类中,如果我们使用WebMvcConfigurer添加HttpMessageConverter,那么就不会有默认的,就只有我们自己HttpMessageConverter,当然,可能还有其他包实现了WebMvcConfigurer类,提供了其他的HttpMessageConverter

总结 

总之,原因是在处理响应结果的时候,返回类型是String的情况下
使用的是StringHttpMessageConverter
我们期望的是MappingJackson2HttpMessageConverter
所以,我们只需要将HttpMessageConverter处理一下就行
要么添加MappingJackson2HttpMessageConverter到String之前
要么不要StringHttpMessageConverter

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值