案例
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping(value = "string")
public String test(HttpServletRequest request) {
return "Hello Luck" + IPUtils.getClientAddress(request);
}
}
先贴出异常结果![](https://img-blog.csdnimg.cn/direct/28d60caed8154ccda2eb8c93f294e48c.png)
统一响应结果处理类
@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![](https://img-blog.csdnimg.cn/direct/ac0710d6ed8441bc93ea4176182c9140.png)
方案二
- 使用@EnableWebMvc注解,上面有@EnableWebMvc导入HttpMessageConverter的原理,这里不再赘述
- 由于在@EnableWebMvc导入的类中,如果我们使用WebMvcConfigurer添加HttpMessageConverter,那么就不会有默认的,就只有我们自己HttpMessageConverter,当然,可能还有其他包实现了WebMvcConfigurer类,提供了其他的HttpMessageConverter
总结
总之,原因是在处理响应结果的时候,返回类型是String的情况下
使用的是StringHttpMessageConverter
我们期望的是MappingJackson2HttpMessageConverter
所以,我们只需要将HttpMessageConverter处理一下就行
要么添加MappingJackson2HttpMessageConverter到String之前
要么不要StringHttpMessageConverter