SpringCloud采用Jackson序列化统一响应不正当的消息转换器导致的异常问题

SpringCloud采用Jackson序列化统一响应不正当的消息转换器导致的异常问题

环境说明

org.springframework.cloud.spring-cloud-dependencies.2020.0.0
org.springframework.boot.spring-boot-dependencies.2.4.0
com.fasterxml.jackson.core.jackson-core.2.12.0

问题说明

  1. 我们在使用@RestControllerAdvice注解与ResponseBodyAdvice制定微服务统一返回值的时候,Spring根据消息转换器的是否支持进行选择,而我们在此时更改了返回值类型,导致的返回值类型转换出现异常

出现异常:org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.ClassCastException: class com.pkk.spring.cloud.core.common.rpc.response.ResponseBody cannot be cast to class java.lang.String (com.pkk.spring.cloud.core.common.rpc.response.ResponseBody is in unnamed module of loader ‘app’; java.lang.String is in module java.base of loader ‘bootstrap’)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-5.3.1.jar:5.3.1]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.1.jar:5.3.1]

问题分析解决思路

  1. 消息处理器在处理的时候被StringHttpMessageConverter消息处理器给捕获了,并做了处理,这时我把方法返回的值给变为ResponseBody对象,再去转String出现了异常
  • 解决思路一:优先使用自定义的MappingJackson2HttpMessageConverter消息处理返回的数据
  • 解决思路二:把匹配到的StringHttpMessageConverter消息处理器给删除掉,让给我们自定义的消息处理器
  1. 通过下面的源码分析思路,发现我们配置的MappingJackson2HttpMessageConverter消息转换器在处理一个请求的时候,没有被匹配到,直接跳过?

解决代码示例

  1. 通过WebMvcConfigurer或者WebMvcConfigurerSupport下面的extendMessageConverters方法改变排序
/**
 * Mvc的配置
 *
 * @author peikunkun
 * @version V1.0
 * @date 2021-01-07 17:46
 **/
@Configuration
public class MessageConverterOrderWebMvcConfigurer implements WebMvcConfigurer {


  @Autowired
  private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;


  @Override
  public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {

    //方法一:把jackson解析器放在第一位,这样匹配完了之后,就会直接返回;[是否匹配和我们解析器支持的类型有关[supportedMediaTypes]详细见源码
    // org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters]
    converters.add(0, mappingJackson2HttpMessageConverter);
  }
}
  1. 通过WebMvcConfigurer或者WebMvcConfigurerSupport下面的extendMessageConverters方法删除此消息处理器
/**
 * Mvc的配置
 *
 * @author peikunkun
 * @version V1.0
 * @date 2021-01-07 17:46
 **/
@Configuration
public class MessageConverterOrderWebMvcConfigurer implements WebMvcConfigurer {
  
  @Override
  public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    //方法二:我们可以把匹配到的Spring自带的默认String值解析器去掉,这样匹配到的就有可能到jackson解析器来处理
    converters.removeIf(converter -> converter.getClass() == StringHttpMessageConverter.class);
  }
}
  1. 我们直接不转换返回值类型,在beforeBodyWrite方法中判断返回值如果是String类型,我们处理完之后在转为JSON字符串
/**
 * 普通响应类统一处理
 *
 * @author peikunkun
 * @version V1.0
 * @date 2021-01-06 16:26
 **/
//这里尽量让加密的判断优先级更低一点(请求的时候,加密的优先级高一点)
@Order(1)
@RestControllerAdvice
//当开启此注解的时候启用此响应处理器
//@ConditionalOnBean(annotation = EnableGlobalResponse.class)
public class ResponseHandle implements ResponseBodyAdvice<Object> {


  @Autowired
  private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;

  /**
   * 是否支持此消息响应处理器
   *
   * @return boolean
   * @Param methodParameter
   * @Param aClass
   * @author peikunkun
   * @date 2021/1/6 0006 下午 4:29
   * @since
   */
  @Override
  public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
    //当前方法的类上存在或者方法上存在此注解,取消给解析器
    if (methodParameter.getMethod().getDeclaringClass().isAnnotationPresent(IgnoreResponseConverter.class) ||
        methodParameter.hasMethodAnnotation(IgnoreResponseConverter.class)) {
      return false;
    }
    return true;
  }

  /**
   * 在选择HttpMessageConverter之后且在调用其write方法之前调用。
   * <p>
   * 参数:正文–要写的正文
   * returnType –控制器方法的返回类型
   * selectedContentType –通过内容协商选择的内容类型
   * selectedConverterType –选择要写入响应的转换器类型
   * 请求–当前请求
   * 响应–当前响应
   * 返回值:
   * 传入的正文或经过修改的(可能是新的)实例
   */
  @SneakyThrows
  @Override
  public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
      Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest,
      ServerHttpResponse serverHttpResponse) {
	//字符串的特殊处理
    if (o instanceof String) {
      return mappingJackson2HttpMessageConverter.getObjectMapper().writeValueAsString(R.success(o));
    }

    ResponseBody result = null;
    if (o instanceof ResponseBody) {
      result = (ResponseBody) o;
    } else {
      result = R.success(o);
    }
    return result;
  }
}
  1. 分析发现【converter.canWrite】不符合,不符合的原因就是MediaType的原因,增加支持相应的MediaType(原因分析见org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#canWrite)
/**
 * 消息转换处理器
 *
 * @author peikunkun
 * @version V1.0
 * @date 2021-01-06 17:36
 **/
public class MessageConverterConfig {


  /**
   * 使用jackson序列化消息转换
   *
   * @return org.springframework.boot.autoconfigure.http.HttpMessageConverters
   * @Param
   * @author peikunkun
   * @date 2021/1/6 0006 下午 6:09
   * @since
   */
  @Bean
  public MappingJackson2HttpMessageConverter fastJsonHttpMessageConverters() {
    MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
    messageConverter.setDefaultCharset(Charset.defaultCharset());

    //@formatter:off
    ObjectMapper objectMapper = new ObjectMapper();
    // 忽略json字符串中不识别的属性
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    // 忽略无法转换的对象
    objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    // PrettyPrinter 格式化输出
    objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
    // 指定时区
    objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
    // 日期类型字符串处理
    objectMapper.setDateFormat(new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN));

    // java8日期日期处理
    JavaTimeModule javaTimeModule = new JavaTimeModule();
    javaTimeModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN)));
    javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN)));
    javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN)));
    javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN)));
    javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN)));
    javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN)));
    objectMapper.registerModule(javaTimeModule);
    messageConverter.setObjectMapper(objectMapper);



    //支持的媒体类型
    List<MediaType> supportedMediaTypes = new LinkedList<>();
    supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);


    //页面直接请求的类型(这里是新增加的支持的匹配的类型,页面访问的时候类型为text/html)
    supportedMediaTypes.add(MediaType.TEXT_HTML);


    messageConverter.setSupportedMediaTypes(supportedMediaTypes);
    //@formatter:on
    return messageConverter;
  }

}

问题源码分析

HttpMessageConverter类是Spring的消息转换类,他是用来处理流和接口的参数类型或返回值类型之间的转换的。

  1. 我们通过Debug模式定位出现异常的

    • 首先定位到异常代码位置org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters
      在这里插入图片描述
    • 有上图可以看出,代码是获取了所有的消息转换器,依次进行尝试,根据converter.canWrite判断是否可输出,可以的话,获取所有的请求响应链(RequestResponseBodyAdviceChain)调用其beforeBodyWrite方法进行处理,这时会调用我们自定义的ResponseHandle#beforeBodyWrite方法,我们在这个方法中改变了其返回值,将返回值更改为ResponseBody类型;这之后的body类型将会由[String->ResponseBody类型],最终会调用converter.write()方进行输出;过程见下图
    if (selectedMediaType != null) {
    		selectedMediaType = selectedMediaType.removeQualityValue();
    		for (HttpMessageConverter<?> converter : this.messageConverters) {
    			GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
    					(GenericHttpMessageConverter<?>) converter : null);
    			if (genericConverter != null ?
    					((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
    					converter.canWrite(valueType, selectedMediaType)) {
    				body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
    						(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
    						inputMessage, outputMessage);
    				if (body != null) {
    					Object theBody = body;
    					LogFormatUtils.traceDebug(logger, traceOn ->
    							"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
    					addContentDispositionHeader(inputMessage, outputMessage);
    					if (genericConverter != null) {
    						genericConverter.write(body, targetType, selectedMediaType, outputMessage);
    					}
    					else {
    						((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
    					}
    				}
    				else {
    					if (logger.isDebugEnabled()) {
    						logger.debug("Nothing to write: null body");
    					}
    				}
    				return;
    			}
    		}
    	}
    
    • 图例
      在这里插入图片描述
    • 异常转换核心步骤
      在这里插入图片描述

项目MAVEN相关依赖支持

<dependencies>
    <!--自动装配的配置-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>


    <!--@RestControllerAdvice的统一响应处理支持-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
    </dependency>

    <!--ResponseBodyAdvice接口的拓展支持响应类操作-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
    </dependency>

    <!--使用jackson的序列化-->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
    </dependency>
    <!--使用jackson的序列化的JavaTimeModule序列化操作-->
    <dependency>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <artifactId>jackson-datatype-jsr310</artifactId>
    </dependency>
  </dependencies>