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
问题说明
- 我们在使用@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]
问题分析解决思路
- 消息处理器在处理的时候被StringHttpMessageConverter消息处理器给捕获了,并做了处理,这时我把方法返回的值给变为ResponseBody对象,再去转String出现了异常
- 解决思路一:优先使用自定义的MappingJackson2HttpMessageConverter消息处理返回的数据
- 解决思路二:把匹配到的StringHttpMessageConverter消息处理器给删除掉,让给我们自定义的消息处理器
- 通过下面的源码分析思路,发现我们配置的MappingJackson2HttpMessageConverter消息转换器在处理一个请求的时候,没有被匹配到,直接跳过?
解决代码示例
- 通过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);
}
}
- 通过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);
}
}
- 我们直接不转换返回值类型,在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;
}
}
- 分析发现【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的消息转换类,他是用来处理流和接口的参数类型或返回值类型之间的转换的。
-
我们通过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; } } }
- 图例
- 异常转换核心步骤
- 首先定位到异常代码位置org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters
项目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>