该文SpringBoot版本:2.3.4
SpringBoot的web场景中,底层依然使用的是SpringMVC,框架会根据请求url找到它对应的handler,然后再找到该handler的handlerAdapter,handlerAdapter先回处理handler(controller.method())的形参,然后执行该方法,并返回方法的返回值,对应源码ServletInvocableHandlerMethod.invokeAndHandle()中的第一行
,但这个返回结果并不能直接返回,而是需要进行进一步的处理,
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
//处理参数,执法方法,返回方法的返回值
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
//处理返回值,将结果保存在mavContainer中
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}
该方法中关键的一步就是this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer,
,invokeAndHandle
方法没有返回值,它最后将处理的返回值保存在传入方法的一个mavContainer对象中传递出去。
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
handleReturnValue
方法也还是那个老套路,先遍历选择容器中现有的可以处理该返回值的处理器,然后再使用该返回值处理器处理。
内置的returnValueHandlers有14中,也表示了框架能够处理的14中返回值。
举例来说,被@RespondBody
标注的方法的返回值则是被RequestResponseBodyMethodProcessor
,我们可以从它的判断方法中得出。
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}
在获得需要的returnValueHandler后,则是开始真正的处理返回值,一路向下可以追踪到writeWithMessageConverters
中,这也是内容协商的代码所在!
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body;
Class<?> valueType;
Type targetType;
if (value instanceof CharSequence) {
body = value.toString();
valueType = String.class;
targetType = String.class;
}
else {
body = value;
valueType = getReturnValueType(body, returnType);
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}
if (isResourceType(value, returnType)) {
outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");
if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null &&
outputMessage.getServletResponse().getStatus() == 200) {
Resource resource = (Resource) value;
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
body = HttpRange.toResourceRegions(httpRanges, resource);
valueType = body.getClass();
targetType = RESOURCE_REGION_LIST_TYPE;
}
catch (IllegalArgumentException ex) {
outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());
outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
}
}
}
//内容协商
MediaType selectedMediaType = null;
//先看在此之前有没有已经设置了返回类型
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {
if (logger.isDebugEnabled()) {
logger.debug("Found 'Content-Type:" + contentType + "' in response");
}
selectedMediaType = contentType;
}
else {
HttpServletRequest request = inputMessage.getServletRequest();
//从请求中获取浏览器支持的返回值类型
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
//获取服务器支持的返回值类型
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
//双重for循环来得到两边都支持的内容类型
List<MediaType> mediaTypesToUse = new ArrayList<>();
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);
}
if (logger.isDebugEnabled()) {
logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
return;
}
//排序
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
//选择最佳匹配
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
if (logger.isDebugEnabled()) {
logger.debug("Using '" + selectedMediaType + "', given " +
acceptableTypes + " and supported " + producibleTypes);
}
}
//下面则是使用MessageConverter具体的处理内容
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;
}
}
}
if (body != null) {
Set<MediaType> producibleMediaTypes =
(Set<MediaType>) inputMessage.getServletRequest()
.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (isContentTypePreset || !CollectionUtils.isEmpty(producibleMediaTypes)) {
throw new HttpMessageNotWritableException(
"No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'");
}
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
}
}
获取浏览器想要的返回值类型List
是由内容协商管理器执行的。
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request)
throws HttpMediaTypeNotAcceptableException {
return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}
该方法遍历现有的内容协商策略,
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
for (ContentNegotiationStrategy strategy : this.strategies) {
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
continue;
}
return mediaTypes;
}
return MEDIA_TYPE_ALL_LIST;
}
默认的内容协商策略是在请求头中获取浏览器支持的数据类型,其实也就是request header中的accept字段的值。
我们可以利用在配置中设置
spring:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
来开启浏览器请求内容协商功能,请求带上format=XX参数就可以参与内容协商
发请求:
http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
对于服务器支持的内容类型,类似的,用List
获得服务器支持的内容类型,这又是咋操作的呢?
protected List<MediaType> getProducibleMediaTypes(
HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {
Set<MediaType> mediaTypes =
(Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<>(mediaTypes);
}
else if (!this.allSupportedMediaTypes.isEmpty()) {
List<MediaType> result = new ArrayList<>();
//遍历所有的MessageConverter,将他们可以处理的内容类型都找到
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter instanceof GenericHttpMessageConverter && targetType != null) {
if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
//可以处理的内容类型汇总
result.addAll(converter.getSupportedMediaTypes());
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
return result;
}
else {
return Collections.singletonList(MediaType.ALL);
}
}
MessageConverter也反应了服务器能处理的返回值类型有哪些。
MappingJackson2HttpMessageConverter 是我们常用的,它把对象转为JSON(利用底层的jackson的objectMapper转换的)。
当然,这里的MessageConverter也可以来自定义,这样就可以返回满足我们业务需求定制的数据协议的数据。
SpringBoot预留的接口。
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
}
}
实现HttpMessageConverter
接口,然后在WebMVC中配置。
以上处理请求返回值的过程可以总结一下:
1.返回值处理器判断是否支持这种类型返回值 supportsReturnType
2. 返回值处理器调用handleReturnValue 进行处理
3. RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。
利用 MessageConverters 进行处理将数据写为json
■ 3.1、内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
■ 3.2、服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
■ 3.3、SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
● 3.3.1、得到MappingJackson2HttpMessageConverter可以将对象写为json
● 3.3.2、利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
现在,我们可以以自定义的方式让浏览器告诉服务器支持什么样的内容(ContentNegotiationStrategy
接口),也可以定制我们自己的数据协议,让服务器返回(HttpMessageConverter
接口)