紧接上篇的SpringMVC执行流程,到最后一个writeWithMessageConverters细节,该方法里面封装了SpringMVC如何处理响应请求,内容协商,HttpMessageConverter的内部细节.逐步分析可以掌握SpringMVC是如何知道浏览器想要什么格式的请求(json/xml或自定义的协议),底层如何将响应写出
分析之前需要先了解HandlerMethodReturnValueHandler接口
public interface HandlerMethodReturnValueHandler {
// 是否支持
boolean supportsReturnType(MethodParameter returnType);
// 支持之后如何处理
void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
我们标注了@RequestBody注解的接口会调用RequestResponseBodyMethodProcessor来处理这种响应
该类的实现也比较简单
// 标注了@ResponseBody
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}
// 如何写出逻辑
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
返回值处理器的原理
- 返回值处理器判断是否支持该类型的返回值supportsReturnType 方法如RequestResponseBodyMethodProcessor判断是否标注了@ResponseBody注解
- 调用ReturnValueHandler的 handleReturnValue方法进行返回值的处理
- 以RequestResponseBodyMethodProcessor为例
- 先进行内容协商,根据浏览器请求头中的Accept字段判断浏览器能接受什么类型
- 服务器遍历所有的HttpMessageConverter获取到自身能处理的所有MediaType
- 最终会找到MappingJackson2HttpMessageConverter可以处理application/json的请求,底层利用Jackson2Json的核心类ObjectMapper将响应写出去
SpringMVC底层提供了默认的10个MessageConverter
- ByteArrayHttpMessageConverter 用于处理byte数组
- StringHttpMessageConverter 用于处理String类型
- Resource用于处理org.springframework.core.io.Resource类型的返回值
- ResourceRegionHttpMessageConverter 处理ResourceRegion
- SourceHttpMessageConverter 处理excel相关的DOMSource/SAXSource/StAXSource/StreamSource/Source 类型
- AllEncompassingFormHttpMessageConverter 处理MultiValueMap
- MappingJackson2HttpMessageConverter 处理 this.objectMapper.canSerialize(clazz, causeRef)支持的类型
- Jaxb2RootElementHttpMessageConverter 支持被@XmlRootElement标注的类型
内容协商
用于支持返回xml类型的数据需要加入
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
- MediaType contentType = outputMessage.getHeaders().getContentType();// 查看之前是否在Response中已经设置了ContentType如果已经设置了直接用
- List acceptableTypes = getAcceptableMediaTypes(request); // 获取浏览器中可以接受的MediaType,底层使用contentNegotiationManager内容协商管理器中resolveMediaTypes方法根据内容协商管理器中的策略来匹配可以接受的类型,SpringMVC默认配置了HeaderContentNegotiationStrategy策略 默认获取请求头的Accept字段来确定 没有就是*/*
- 接着马上获取服务器能处理哪些类型getProducibleMediaTypes(request, valueType, targetType);
- 进入方法内部本质就是循环遍历所有的MessageConverter
- 调用MessageConverter#canWrite方法判断该MessageConverter能否处理该类型的数据
- 最终getProducibleMediaTypes方法返回获取到所有服务器能处理的媒体类型
- 进入最佳适配的媒体类型
- 进入一个双重for循环 外层循环浏览器可以接受的媒体类型 内存循环服务器提供的媒体类型,选择匹配的媒体类型
- 最媒体类型进行最佳排序sortBySpecificityAndQuality
- 最终得到selectedMediaType 选择最终的媒体类型
- 根据选择媒体类型 再次循环遍历判断所有的HttpMessageConverter找到能够处理该类型的MessageConverter
- 再次调用MessageConverter的canWrite方法获取到可以处理的Converter
- canwrite返回为true,进去该Converter的write方法
- write 方法进入MessageConverter的写出逻辑 以MappingJackson2HttpMessageConverter为例
- 底层调用了MappingJackson2的writeInternal 方法
- 底层给response设置了一些默认的响应头
- 调用MappingJackson2的核心类ObjectMapper#writeValue方法写出
- 再调用generator#flush方法将流刷新出去
- 最终调用原生response的getBody().flush()写入到响应流中
- 获取到最终封装好了ModelAndViewContainer对象,最终调用getModelAndView方法传入container和原生的request response得到ModelAndView
- 此时HandlerExecutionChain的hanlder方法真正返回了ModelAndView对象
- 后面进入视图渲染逻辑 下篇详细的分析
源码分析
writeWithMessageConverters方法
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body;
Class<?> valueType;
Type targetType;
// 判断返回值是不是String
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());
}
// 判断是不是Resource类型
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;
// 如果之前已经操作过response设置了ContentType直接拿到用不进行内容协商
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();
// 拿到浏览器请求的可以接受的所有媒体类型 本质是获取内容协商管理器contentNegotiationManager的所有策略挨个遍历调用strategy策略的resolveMediaTypes方法获取媒体类型,默认SpringMVC只有一个内容协商策略HeaderContentNegotiation本质就是从Accept字段获取
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
// 获取所有服务器能处理的所有媒体类型,本质是遍历SpringMVC的所有MessageConverter调用canWrite方法是否能处理该类型数据,最终调用getSupportedMediaTypes拿到所有服务器能处理的媒体类型加入集合返回
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
List<MediaType> mediaTypesToUse = new ArrayList<>();
// 双重for循环匹配 浏览器接受和服务器能提供的媒体类型交集
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);
// 选择最佳的一个媒体类型 赋值给selectedMediaType
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);
}
}
// 如果已经选择好了媒体类型
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
// 再次遍历所有MessageConverter 调用canWrite方法获取到能够处理该类型的Converter 调用write方法,将数据写入 response中
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
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) {
// 调用Converter的write 方法
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);
}
}
获取浏览器能接受媒体类型逻辑
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request)
throws HttpMediaTypeNotAcceptableException {
// 使用内容协商管理器来处理
return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
for (ContentNegotiationStrategy strategy : this.strategies) {
// 遍历内容协商管理器的所有strage策略来获取 默认只有从请求头Accept字段获取的策略
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
continue;
}
return mediaTypes;
}
return MEDIA_TYPE_ALL_LIST;
}
HeaderContentNegotiationStrategy选择逻辑分析
public List<MediaType> resolveMediaTypes(NativeWebRequest request)
throws HttpMediaTypeNotAcceptableException {
// 逻辑很简单从原生request拿到Accpet字段
String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
// 如果没有返回 */*
if (headerValueArray == null) {
return MEDIA_TYPE_ALL_LIST;
}
// 有值就一一解析返回所有的MediaType
List<String> headerValues = Arrays.asList(headerValueArray);
try {
List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
MediaType.sortBySpecificityAndQuality(mediaTypes);
return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotAcceptableException(
"Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
}
}
获取所有服务器支持的媒体类型getProducibleMediaTypes方法分析
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调用canWrite方法判断该Converter是否支持如果支持调用getSupportedMediaTypes 加入媒体集合返回
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter instanceof GenericHttpMessageConverter && targetType != null) {
if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
// 获取该Converter支持的媒体类型
result.addAll(converter.getSupportedMediaTypes());
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
return result;
}
else {
return Collections.singletonList(MediaType.ALL);
}
}
HttpMessageConverter的write方法核心逻辑
先设置了一些默认响应头,调用具体子类的writeInternal方法具体处理
@Override
public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType,
HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}));
}
else {
writeInternal(t, type, outputMessage);
outputMessage.getBody().flush();
}
}
writeInternal方法
底层使用了ObjectMappe获取到ObjectWriterr#writeValue执行具体写出操作
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding)) {
writePrefix(generator, object);
Object value = object;
Class<?> serializationView = null;
FilterProvider filters = null;
JavaType javaType = null;
if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = getJavaType(type, null);
}
// 拿到所有的ObjectMapper
ObjectWriter objectWriter = (serializationView != null ?
this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
if (filters != null) {
objectWriter = objectWriter.with(filters);
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.forType(javaType);
}
SerializationConfig config = objectWriter.getConfig();
if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
objectWriter = objectWriter.with(this.ssePrettyPrinter);
}
// objectWriter 写出操作
objectWriter.writeValue(generator, value);
writeSuffix(generator, object);
generator.flush();
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
}
}
自定义HttpMessageConverter处理自定义协议
示例: 如果我们需要自己定义个协议application/myContentType,需要自定义一个HttpMessageConverter重写接口中的方法,并添加到MessageConverter中
示例中只重写write相关方法,如果需要读还需要需要重写canRead 和 Read方法
/**
自定义 一个 MessageConverter 来处理Map类型和协议为application/myContentType的数据
*/
public class MyMapHttpMessageConverter implements HttpMessageConverter<Map> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
// 根据媒体类型判断是不是我们自己的媒体类型
if (mediaType != null && clazz.isAssignableFrom(Map.class)) {
return mediaType.toString().equalsIgnoreCase("application/myContentType");
}
return ClassUtils.isAssignable(Map.class, clazz);
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.singletonList(MediaType.parseMediaType("application/myContentType"));
}
@Override
public Map read(Class<? extends Map> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Map map, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// 写出 简单的[k1=v1,k2=v2]的形式写出
Object result = map.keySet().stream().map(k -> k + "=" + map.get(k)).collect(Collectors.joining(","));
outputMessage.getBody().write(result.toString().getBytes());
}
}
注册自定义的MessageConverter
extendMessageConverters添加我们自定义的MessageConverter
@Configuration
public class MyWebConfiguration implements WebMvcConfigurer {
/**
* 添加自己的messageConverter
*
* @param converters
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MyMapHttpMessageConverter());
}
}
Controller
package com.corn.turorial.spring.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author : Jim Wu
* @version 1.0
* @function :
* @since : 2021/2/1 15:21
*/
@Controller
public class MVCDebugSourceCodeController {
@GetMapping("/test/{id}")
@ResponseBody
public Map test(@RequestParam("code") String code,
@PathVariable("id") String id,
@RequestHeader("User-Agent") String userAgent,
@RequestBody String body,
@CookieValue("jsessionid") String cookie,
ModelAndView modelAndView,
Model model
) {
Map<String, Object> result = new HashMap<>();
result.put("code", code);
result.put("id", id);
result.put("userAgent", userAgent);
result.put("body", body);
result.put("cookie", cookie);
return result;
}
}
发送请求
GET http://localhost:9999/test/111?code=3333
User-Agent: MS/111
Cookie: jsessionid=123123
Accept: application/myContentType
{
"name": "zs",
"age": 15
}
返回结果
添加参数形式修改内容协商方式
有一些情况下,我们没有办法修改请求头中的Accept字段 SpringBoot默认还提供了一种通过携带参数的方式来修改内容协商模式
开启方式
spring:
mvc:
contentnegotiation:
favor-parameter: true # 开启基于参数的协商协议
本质是在ContentNegotiationManager添加了一个stratege策略 ParameterContentNegotiation策略,该策略是获取format参数中值来获取MediaType,开启了基于参数的协商模式之后这个Stratege优先级高于基于Header Accept的Stratege
pom 加入xml支持开始测试
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
GET http://localhost:9999/test/111?code=3333&format=xml
User-Agent: MS/111
Cookie: jsessionid=123123
{
"name": "zs",
"age": 15
}
###
GET http://localhost:9999/test/111?code=3333&format=json
User-Agent: MS/111
Cookie: jsessionid=123123
{
"name": "zs",
"age": 15
}
添加了format参数 为xml返回的就是xml
但现在format参数不能满足我们自定义的协议,默认这个策略只支持xml和json如果需要满足我们自定义的协议,注意的是一旦覆盖了WebMvcConfigurer#configureContentNegotiation的方法.需要自己把HeaderContentNegotiationStrategy添加进去
/**
* 注意覆盖这个方法之后 默认HeaderContentNegotiationStrategy为被覆盖
*
* @param configurer
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
List<ContentNegotiationStrategy> strategies = new ArrayList<>();
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json", MediaType.APPLICATION_JSON);
mediaTypes.put("xml", MediaType.APPLICATION_XML);
mediaTypes.put("myContentType", MediaType.parseMediaType("application/myContentType"));
// ParameterContentNegotiationStrategy 构造器可以传入map 添加自定义的MediaType和format参数值的映射关系
ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
// 为了避免与前端传入的参数名重复还可以自定义参数名
parameterStrategy.setParameterName("myFormat");
strategies.add(parameterStrategy);
strategies.add(new HeaderContentNegotiationStrategy());
configurer.strategies(strategies);
}
GET http://localhost:9999/test/111?code=3333&myFormat=myContentType
User-Agent: MS/111
Cookie: jsessionid=123123
{
"name": "zs",
"age": 15
}