Spring MVC 返回类型之惑

Spring MVC 返回类型之惑:

代码

 @PostMapping(value = "/interface1")
 @ResponseBody
 public BaseResponse interface1(@RequestBody BaseReqDto reqDto) {
     
     //....
     return BaseResponse.ok(null);
 }
 @PostMapping(value = "/interface2", produces = "application/json")
 @ResponseBody
 public BaseResponse interface2(@RequestBody BaseReqDto reqDto) {
     
     //....
     return BaseResponse.ok(null);
 }
 @PostMapping(value = "/interface3.json")
 @ResponseBody
 public BaseResponse interface3(@RequestBody BaseReqDto reqDto) {
     
     //....
     return BaseResponse.ok(null);
 }

问题

postman请求接口,interface1返回为XML格式,但是APP端请求返回值为json格式?

本文实际上是探究Spring MVC如何处理@ResponseBody的,
代码interface2、interface3对比使用。

涉及到的核心类:DispatcherServlet、AbstractHandlerMethodAdapter、RequestMappingHandlerAdapter、HandlerMethodReturnValueHandlerComposite、RequestResponseBodyMethodProcessor、AbstractMessageConverterMethodProcessor、WebMvcConfigurationSupport

环境

Spring版本:5.1.2
环境:大量包依赖

核心流程图:

Spring MVC 返回类型之惑_第1张图片

如何定位到核心流程

在doDispatch的前后查看response的响应头,看是在哪一步设置的值:
Spring MVC 返回类型之惑_第2张图片

=== MimeHeaders ===
Content-Type = application/json;charset=UTF-8
Transfer-Encoding = chunked
Date = Sun, 21 Jun 2020 14:11:09 GMT

可以定位到handle方法;
在RequestMappingHandlerAdapter中可以看到有对响应值处理的是invocableMethod的invokeAndHandle方法,并且该方法也是实际调用业务方法的入口;
进入这个方法,可以得到业务实际返回的类型,明显的在handleReturnValue方法中处理返回值。

核心步骤说明

RequestResponseBodyMethodProcessor

在handleReturnValue中的selectHandler方法值得去跟一遍,看Spring MVC有那些处理返回值的Handler,在上面的用法中,Spring MVC使用的是RequestResponseBodyMethodProcessor
,判断依据就是是否有注解@ResponseBody:

@Override
public boolean supportsReturnType(MethodParameter returnType) {
     
	return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
			returnType.hasMethodAnnotation(ResponseBody.class));
}

Spring MVC 返回类型之惑_第3张图片
可以看到Spring支持多种返回值处理器,包括异步返回的,Spring 扩展性真强。

AbstractMessageConverterMethodProcessor

实际上对返回值处理的是AbstractMessageConverterMethodProcessor类中的writeWithMessageConverters方法;
核心代码如下:

HttpServletRequest request = inputMessage.getServletRequest();
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request); //1.
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); //2.

if (body != null && producibleTypes.isEmpty()) {
     
	throw new HttpMessageNotWritableException(
			"No converter found for return value of type: " + valueType);
}
List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
     
	for (MediaType producibleType : producibleTypes) {
     
		if (requestedType.isCompatibleWith(producibleType)) {
      //3.
			mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
		}
	}
}

MediaType.sortBySpecificityAndQuality(mediaTypesToUse);

for (MediaType mediaType : mediaTypesToUse) {
     
	if (mediaType.isConcrete()) {
     
		selectedMediaType = mediaType;
		break;
	}
	else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
     
		selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
		break;
	}
}

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;
			addContentDispositionHeader(inputMessage, outputMessage);
			if (genericConverter != null) {
     
				genericConverter.write(body, targetType, selectedMediaType, outputMessage);
			}
			else {
     
				((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);//4.
			}
		}
		return;
	}
}
}

if (body != null) {
     
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);//5.
}
  1. 获取支持的media type,有两种策略ServletPathExtensionContentNegotiationStrategy和
    HeaderContentNegotiationStrategy;interface3如果请求不指定Accept会返回json类型是使用了ServletPathExtensionContentNegotiationStrategy的父类PathExtensionContentNegotiationStrategy的解析策略,根据URL的后缀来判断使用哪种media type;对于interface1和interface2使用的是HeaderContentNegotiationStrategy,即取决于Accept指定的类型,可以设置各种类型测试一下:
    a. 不指定
    Spring MVC 返回类型之惑_第4张图片
    b. 指定application/json
    Spring MVC 返回类型之惑_第5张图片
  2. 返回服务器支持的类型

a: 不设置produces
这个与引入的jar包有关,可以在WebMvcConfigurationSupport中找到支持的类型
Spring MVC 返回类型之惑_第6张图片
b: 设置了produces = “application/json”,如interface2
Spring MVC 返回类型之惑_第7张图片

  1. 选出最合适的media type,但是如interface1接口,就是去默认支持的几种,然后取第一个,即”application/xml“
  2. 根据selectedMediaType的类型,选择媒体类型转换器转换,如果是json就是MappingJackson2HttpMessageConverter,如xml就是MappingJackson2XmlHttpMessageConverter
  3. 如果找不到合适的media type就会报406

    1592751425701
    406
    Not Acceptable
    Could not find acceptable representation
    /chatroom/interface2

分析

  1. interface1没有指定produces,则acceptableTypes返回“/“,返回类型取决于请求的Accept中指定的类型:
    a: 不指定,取决于项目引入的jar包,按照我的环境默认是”application/xml“;
    b:指定类型,如”application/json“,则返回也是”application/json“

看一下WebMvcConfigurationSupport的配置:

static {
		ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader();
		romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
		jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
		jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
						ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
		jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
		jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
		jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
		gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
		jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
	}

protected final void addDefaultHttpMessageConverters(List> messageConverters) {
	StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
	stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316

	messageConverters.add(new ByteArrayHttpMessageConverter());
	messageConverters.add(stringHttpMessageConverter);
	messageConverters.add(new ResourceHttpMessageConverter());
	messageConverters.add(new ResourceRegionHttpMessageConverter());
	try {
		messageConverters.add(new SourceHttpMessageConverter<>());
	}
	catch (Throwable ex) {
		// Ignore when no TransformerFactory implementation is available...
	}
	messageConverters.add(new AllEncompassingFormHttpMessageConverter());

	if (romePresent) {
		messageConverters.add(new AtomFeedHttpMessageConverter());
		messageConverters.add(new RssChannelHttpMessageConverter());
	}

	if (jackson2XmlPresent) {
		Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
		if (this.applicationContext != null) {
			builder.applicationContext(this.applicationContext);
		}
		messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
	}
	else if (jaxb2Present) {
		messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
	}

	if (jackson2Present) {
		Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
		if (this.applicationContext != null) {
			builder.applicationContext(this.applicationContext);
		}
		messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
	}
	else if (gsonPresent) {
		messageConverters.add(new GsonHttpMessageConverter());
	}
	else if (jsonbPresent) {
		messageConverters.add(new JsonbHttpMessageConverter());
	}

	if (jackson2SmilePresent) {
		Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
		if (this.applicationContext != null) {
			builder.applicationContext(this.applicationContext);
		}
		messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
	}
	if (jackson2CborPresent) {
		Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
		if (this.applicationContext != null) {
			builder.applicationContext(this.applicationContext);
		}
		messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
	}
}

项目中引入了jackson-dataformat-xml,所以激活了xml的选项,这里有两种修改方式,但是都不推荐:
c: 排除jar包,但是有人引入可能有人用,或者引入的地方过多,或者后面可能有人会引入,不要挖坑
d: 指定默认的ContentType,即调整一下顺序:

@Configuration
public class WebInterceptorAdapter implements WebMvcConfigurer {
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.defaultContentType(MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,MediaType.TEXT_XML);
    }
}

但是这种会打包了Spring Boot的自动配置能力

  1. interface2方式指定了produces解决了问题,但是如果是提供的第三方API则不鼓励这种方式,并且多写了几行代码,也可能忘记写
  2. interface3方式是最常用方式,带上扩展名,约定大于配置

总结

  1. Spring帮我们完成了几乎所有的事情,但是怎么用却需要了解Spring 大概是怎么实现这些功能的,这个简单的问题,之前有解决过,但是再次遇到时完全忘记了;
  2. 本质上还是Spring MVC对HTTP协议的扩展,基础。。。
  3. 遇到了这种问题就怀疑是后端那个地方出问题了,实际上是正常的,从interface1的处理上,可以看到Spring设计上的扩展是极致的。

你可能感兴趣的:(Spring,spring)