《SpringMVC系列》第四章:响应处理

一、响应处理流程

a) invokeAndHandle()

通过SpringMVC处理请求时,肯定会经过doDispatch()方法,获取到HandlerMapping,然后找到HandlerAdapter,调用目标方法,底层会调用到这个方法,前面的过程之前已经介绍过了

	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 {
             // 处理返回值
			this.returnValueHandlers.handleReturnValue(
					returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
		}
		catch (Exception ex) {
			if (logger.isTraceEnabled()) {
				logger.trace(formatErrorForReturnValue(returnValue), ex);
			}
			throw ex;
		}
	}

b) handleReturnValue() 处理返回值

调用该方法处理返回值,相应的返回值处理器存在多个,那么需要遍历来判断是否匹配

	// 处理返回值
	@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);
	}

    // 选择返回值处理器
	@Nullable
	private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
         // 判断是否为异步请求
		boolean isAsyncValue = isAsyncReturnValue(value, returnType);
         // 遍历获取,测试的时候是15个
		for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
			if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
				continue;
			}
             // 判断返回类型是否匹配
			if (handler.supportsReturnType(returnType)) {
				return handler;
			}
		}
		return null;
	}

二、方法返回值处理器接口

public interface HandlerMethodReturnValueHandler {

	// 判断是否可以处理返回类型
	boolean supportsReturnType(MethodParameter returnType);

	// 处理返回值
	void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BLi3Vtgk-1652196149044)(./returnValueHandlers.png)]

三、处理@ReponseBody

1.RequestResponseBodyMethodProcessor

RequestResponseBodyMethodProcessor是多个返回值处理器中的一个,可以处理返回值标了@ResponseBody 注解的,也就是我们最常用的

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
	
    // 判断是否应用返回类型
	@Override
	public boolean supportsReturnType(MethodParameter returnType) {x
         // 这里是或判断 分别判断当前类 和 当前方法,是否存在@ResponseBody注解
		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);
	}
    
    // 省略部分代码...
}

2.writeWithMessageConverters()

借助消息转换器进行写出操作,因为返回结果需要经过处理,例如,最常见的我们需要返回一个JOSN串,那么将我们的返回值转换成JSON是需要经过处理的

下面方法比较长,大体的步骤:

  1. 确定返回值和目标值的类型。因为中间还需要经过一步消息转换,所以需要知道类型
  2. 确定媒体类型。结果的返回方式有多种,将请求允许返回的媒体类型和服务器产品的媒体类型,进行匹配,确定最终的媒体类型
  3. 信息转换。返回的内容和媒体类型都确定好了,剩下的就是信息转换了,这里需要借助HttpMessageConverter
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());
		}

         // 是否为资源类型,也就是一些IO流数据
		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();
             // 获取到能够接收的媒体类型,也就对应一个请求信息中,请求头中的accopt
			List<MediaType> acceptableTypes;
			try {
				acceptableTypes = getAcceptableMediaTypes(request);
			}
			catch (HttpMediaTypeNotAcceptableException ex) {
				int series = outputMessage.getServletResponse().getStatus() / 100;
				if (body == null || series == 4 || series == 5) {
					if (logger.isDebugEnabled()) {
						logger.debug("Ignoring error response content (if any). " + ex);
					}
					return;
				}
				throw ex;
			}
             // 服务器根据自身的能力,决定服务器能生产出什么样内容类型的数据
			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 (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);
			}
		}

		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(getSupportedMediaTypes(body.getClass()));
		}
	}

3.媒体类型

媒体类型就是请求的时候,在请求头里面标注的允许的媒体类型

MediaType

public class MediaType extends MimeType implements Serializable {

	private static final long serialVersionUID = 2069937152339670231L;
	public static final MediaType ALL;
	public static final String ALL_VALUE = "*/*";
	public static final MediaType APPLICATION_ATOM_XML;
	public static final String APPLICATION_ATOM_XML_VALUE = "application/atom+xml";
	public static final MediaType APPLICATION_CBOR;
	public static final String APPLICATION_CBOR_VALUE = "application/cbor";
	public static final MediaType APPLICATION_FORM_URLENCODED;
	public static final String APPLICATION_FORM_URLENCODED_VALUE = "application/x-www-form-urlencoded";
	public static final MediaType APPLICATION_JSON;
	public static final String APPLICATION_JSON_VALUE = "application/json";
	@Deprecated
	public static final MediaType APPLICATION_JSON_UTF8;
	@Deprecated
	public static final String APPLICATION_JSON_UTF8_VALUE = "application/json;charset=UTF-8";
	public static final MediaType APPLICATION_OCTET_STREAM;
	public static final String APPLICATION_OCTET_STREAM_VALUE = "application/octet-stream";
	public static final MediaType APPLICATION_PDF;
	public static final String APPLICATION_PDF_VALUE = "application/pdf";
	public static final MediaType APPLICATION_PROBLEM_JSON;
	public static final String APPLICATION_PROBLEM_JSON_VALUE = "application/problem+json";
	@Deprecated
	public static final MediaType APPLICATION_PROBLEM_JSON_UTF8;
	@Deprecated
	public static final String APPLICATION_PROBLEM_JSON_UTF8_VALUE = "application/problem+json;charset=UTF-8";
	public static final MediaType APPLICATION_PROBLEM_XML;
	public static final String APPLICATION_PROBLEM_XML_VALUE = "application/problem+xml";
	public static final MediaType APPLICATION_RSS_XML;
	public static final String APPLICATION_RSS_XML_VALUE = "application/rss+xml";
	public static final MediaType APPLICATION_NDJSON;
	public static final String APPLICATION_NDJSON_VALUE = "application/x-ndjson";
	@Deprecated
	public static final MediaType APPLICATION_STREAM_JSON;
	@Deprecated
	public static final String APPLICATION_STREAM_JSON_VALUE = "application/stream+json";
	public static final MediaType APPLICATION_XHTML_XML;
	public static final String APPLICATION_XHTML_XML_VALUE = "application/xhtml+xml";
	public static final MediaType APPLICATION_XML;
	public static final String APPLICATION_XML_VALUE = "application/xml";
	public static final MediaType IMAGE_GIF;
	public static final String IMAGE_GIF_VALUE = "image/gif";
	public static final MediaType IMAGE_JPEG;
	public static final String IMAGE_JPEG_VALUE = "image/jpeg"; 
	public static final MediaType IMAGE_PNG;
	public static final String IMAGE_PNG_VALUE = "image/png";
	public static final MediaType MULTIPART_FORM_DATA;
	public static final String MULTIPART_FORM_DATA_VALUE = "multipart/form-data";
	public static final MediaType MULTIPART_MIXED;
	public static final String MULTIPART_MIXED_VALUE = "multipart/mixed";
	public static final MediaType MULTIPART_RELATED;
	public static final String MULTIPART_RELATED_VALUE = "multipart/related";
	public static final MediaType TEXT_EVENT_STREAM;
	public static final String TEXT_EVENT_STREAM_VALUE = "text/event-stream";
	public static final MediaType TEXT_HTML;
	public static final String TEXT_HTML_VALUE = "text/html";
	public static final MediaType TEXT_MARKDOWN;
	public static final String TEXT_MARKDOWN_VALUE = "text/markdown";
	public static final MediaType TEXT_PLAIN;
	public static final String TEXT_PLAIN_VALUE = "text/plain";
	public static final MediaType TEXT_XML;
	public static final String TEXT_XML_VALUE = "text/xml";
	private static final String PARAM_QUALITY_FACTOR = "q";

	static {
		// Not using "valueOf' to avoid static init cost
		ALL = new MediaType("*", "*");
		APPLICATION_ATOM_XML = new MediaType("application", "atom+xml");
		APPLICATION_CBOR = new MediaType("application", "cbor");
		APPLICATION_FORM_URLENCODED = new MediaType("application", "x-www-form-urlencoded");
		APPLICATION_JSON = new MediaType("application", "json");
		APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8);
		APPLICATION_NDJSON = new MediaType("application", "x-ndjson");
		APPLICATION_OCTET_STREAM = new MediaType("application", "octet-stream");
		APPLICATION_PDF = new MediaType("application", "pdf");
		APPLICATION_PROBLEM_JSON = new MediaType("application", "problem+json");
		APPLICATION_PROBLEM_JSON_UTF8 = new MediaType("application", "problem+json", StandardCharsets.UTF_8);
		APPLICATION_PROBLEM_XML = new MediaType("application", "problem+xml");
		APPLICATION_RSS_XML = new MediaType("application", "rss+xml");
		APPLICATION_STREAM_JSON = new MediaType("application", "stream+json");
		APPLICATION_XHTML_XML = new MediaType("application", "xhtml+xml");
		APPLICATION_XML = new MediaType("application", "xml");
		IMAGE_GIF = new MediaType("image", "gif");
		IMAGE_JPEG = new MediaType("image", "jpeg");
		IMAGE_PNG = new MediaType("image", "png");
		MULTIPART_FORM_DATA = new MediaType("multipart", "form-data");
		MULTIPART_MIXED = new MediaType("multipart", "mixed");
		MULTIPART_RELATED = new MediaType("multipart", "related");
		TEXT_EVENT_STREAM = new MediaType("text", "event-stream");
		TEXT_HTML = new MediaType("text", "html");
		TEXT_MARKDOWN = new MediaType("text", "markdown");
		TEXT_PLAIN = new MediaType("text", "plain");
		TEXT_XML = new MediaType("text", "xml");
	}
 	
    // 省略大量代码...
}

4.内容协商

在发送请求的时候,可以根据请求允许返回的媒体类型和服务器产生的媒体类型来匹配,进行内容协商,来得到最终的结果

1)测试实例

例如:像我们发送一个请求,比较普遍的是返回一个JSON结果,但是如果想返回的结果类型为XML,那么需要导入一个依赖,因为JSON依赖是SpringBoot默认导入的

<dependency>
     <groupId>com.fasterxml.jackson.dataformat</groupId>
     <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

2)获取请求能够接收的媒体类型

获取请求允许返回的媒体类型,该信息保存在请求的请求头Accept

	// 第1个方法
	private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request)
			throws HttpMediaTypeNotAcceptableException {
		// 调用第2个方法
		return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
	}
	
	// 第2个方法
	@Override
	public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
         // this.strategies内容协商管理器 默认只有1个
         // 默认使用基于请求头的策略 contentNegotiationManager
		for (ContentNegotiationStrategy strategy : this.strategies) {
             // 解析媒体类型 调用方法3
			List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
			if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
				continue;
			}
			return mediaTypes;
		}
		return MEDIA_TYPE_ALL_LIST;
	}

	// 第3个方法
	@Override
	public List<MediaType> resolveMediaTypes(NativeWebRequest request)
			throws HttpMediaTypeNotAcceptableException {
		
         // 重点在这里  就是获取请求头中的【Accept】
		String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
		if (headerValueArray == null) {
			return MEDIA_TYPE_ALL_LIST;
		}
		
		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());
		}
	}

3)获取服务可以返回的媒体类型

	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);
		}
		List<MediaType> result = new ArrayList<>();
         // 从消息转换器中找到能处理该返回值类型的
		for (HttpMessageConverter<?> converter : this.messageConverters) {
			if (converter instanceof GenericHttpMessageConverter && targetType != null) {
				if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
                      // 将该消息转换器支持的媒体类型存入结果
					result.addAll(converter.getSupportedMediaTypes(valueClass));
				}
			}
			else if (converter.canWrite(valueClass, null)) {
				result.addAll(converter.getSupportedMediaTypes(valueClass));
			}
		}
		return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result);
	}

4)内容协商策略

内容协商策略有很多种,但是普通的接口,使用的都是请求头内容协商策略,还可以通过基于参数的内容协商策略

像之前我们都是通过PostMan来改变请求头,实现内容协商,但是浏览器无法改变请求头,但我们可以在SpringBoot中开启配置,通过基于参数的内容协商策略

spring.mvc.contentnegotiation.favor-parameter=true

开启之后,当我们在浏览器请求的时候,在最默认添加format参数,就可以了,例如:

http://127.0.0.1:8080/test8?id=1&format=json

http://127.0.0.1:8080/test8?id=1&format=xml

实现原理

当我们开启该配置以后,会在容器中给我们添加一个内容协商策略类,例如下图,在我们没开启配置之前,默认只有一个HeaderContentNegotiationStrategy,但是当我们开启了以后,多了一个ParameterContentNegotiationStrategy,基于参数的内容协商策略,它的优先级比较高

然后这样就对上了,从代码中看到,匹配到一个就直接返回,所以我们的ParameterContentNegotiationStrategy优先级更高,而且传递的参数是format,在下图中也可以看到。

	@Override
	public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
		for (ContentNegotiationStrategy strategy : this.strategies) {
			List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
             // 这个equals被重写了
			if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
				continue;
			}
             // 匹配到直接返回
			return mediaTypes;
		}
		return MEDIA_TYPE_ALL_LIST;
	}

5.Http消息转换器

在上面的writeWithMessageConverters()中,会借助消息转化器执行写出操作,那么接下来看一下这个接口

public interface HttpMessageConverter<T> {
	
    // 可以读 是否支持将此Class类型的对象,转为MediaType类型的数据
	boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
	
    // 可以写
	boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

	List<MediaType> getSupportedMediaTypes();

	default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
		return (canRead(clazz, null) || canWrite(clazz, null) ?
				getSupportedMediaTypes() : Collections.emptyList());
	}

	T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException;

	void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException;

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yVigkvV2-1652196149046)(./HttpMessageConverter.png)]

// 这里列举了上面的消息转化器允许的类型
0 - 只支持Byte类型的
1 - String
2 - String
3 - Resource
4 - ResourceRegion
5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
6 - MultiValueMap
7 - true  // 这里是因为方法直接返回true,代表接收任何类型
8 - true
9 - 支持注解方式xml处理的。

初始化

// 调用链
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#configureMessageConverters()
org.springframework.boot.autoconfigure.http.HttpMessageConverters#getConverters()
org.springframework.boot.autoconfigure.http.HttpMessageConverters#构造方法
org.springframework.boot.autoconfigure.http.HttpMessageConverters#getDefaultConverters()
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getMessageConverters()
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addDefaultHttpMessageConverters()

addDefaultHttpMessageConverters()

添加默认的消息转换器

	protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
		messageConverters.add(new ByteArrayHttpMessageConverter());
		messageConverters.add(new StringHttpMessageConverter());
		messageConverters.add(new ResourceHttpMessageConverter());
		messageConverters.add(new ResourceRegionHttpMessageConverter());
		if (!shouldIgnoreXml) {
			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 (!shouldIgnoreXml) {
             // 这里判断条件为 jackson2XmlPresent,
             // 从类里面看,发现它的判断条件为是否导入了 com.fasterxml.jackson.dataformat.xml.XmlMapper类
             // 这也就对应了之前我们如果需要使用XML,需要引入一个 jackson-dataformat-xml 依赖
			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 (kotlinSerializationJsonPresent) {
			messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
		}
		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()));
		}
	}

write()

此方法对应上面通过MappingJackson2HttpMessageConverter消息转换器处理JSON返回结果时,在writeWithMessageConverters()中调用写出操作

	@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();
         // 添加默认的请求头,
         // 如果是JSON的话,key=Content-Type value=application/json
		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()

这里是最底层的,写出JSON数据

	@Override
	protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {
		
         // 媒体类型
		MediaType contentType = outputMessage.getHeaders().getContentType();
         // 编码方式
		JsonEncoding encoding = getJsonEncoding(contentType);

		Class<?> clazz = (object instanceof MappingJacksonValue ?
				((MappingJacksonValue) object).getValue().getClass() : object.getClass());
		ObjectMapper objectMapper = selectObjectMapper(clazz, contentType);
		Assert.state(objectMapper != null, "No ObjectMapper for " + clazz.getName());

		OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody());
		try (JsonGenerator generator = objectMapper.getFactory().createGenerator(outputStream, 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);
			}

			ObjectWriter objectWriter = (serializationView != null ?
					objectMapper.writerWithView(serializationView) : 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.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);
		}
	}

自定义消息转换器

有的时候我们想同1个接口,根据请求服务的不同,返回不同的数据,例如:
网页端请求返回XML数据,ajax请求直接返回JSON数据,移动端请求返回自定义协议数据,那么这个怎么实现呢?

如果我们是通过PostMan来发送请求,那么可以直接通过请求头来设置,如果是通过浏览器来发送请求,那么只能通过参数内容协商策略来实现
实现XML和JSON的不同数据返回,如果是通过PostMan发送请求,可以在请求头里面来标注,如果是浏览器发送请求的话,因为浏览器输入网址无法设置
但是如果想自定义协议数据,那么就需要自定义消息转换器,这个就需要实现HttpMessageConverter接口

实现方式:

  1. 创建类实现HttpMessageConverter接口,里面标注自定义消息转换器允许的媒体类型和读写具体操作逻辑
  2. 需要把自定义的消息转换器添加进去,所以这里我们需要重新自定义SpringMVC的配置,通过configureMessageConverters()
  3. 上面的2步已经支持通过PostMan设置请求头来区别协议数据,但是如果通过浏览器来请求的话,需要通过参数的内容协商策略,这里我们就需要把新的媒体类型添加到自己新创建的参数内容协商策略
  4. 正常我们的内容协商策略只有一个请求头协商策略,但是当我们把基于参数的内容协商策略添加到容器中的时候,就会覆盖掉另一个,所以需要重新添加一下
  5. 这个内容协商策略确定用哪个是在getAcceptableMediaTypes()中进行确定的,上面已经介绍了

自定义SpringMV配置WebMvcConfigurer

@Configuration(proxyBeanMethods = false)
public class UserConfig {

    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {

            @Override
            public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
                converters.add(new H3cMessageConverter());
            }

            @Override
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
                HashMap<String, MediaType> map = new HashMap<>();
                map.put("json", MediaType.APPLICATION_JSON);
                map.put("xml", MediaType.APPLICATION_XML);
                map.put("x-h3c", MediaType.parseMediaType("application/x-h3c"));
				
                // 参数内容协商策略   参数就是上面允许的媒体类型
                // 这里添加这个 是为了支持 基于参数的内容协商策略 也就是通过浏览器来设置媒体类型
                ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(map);
                // 这里在添加一个请求头协商策略, 因为这里属于重新自定义策略
                // 当上面添加了参数协商策略,就会把正常默认自带的请求头协商策略给覆盖掉,所以这里需要重新添加一下
                HeaderContentNegotiationStrategy headerContentNegotiationStrategy = new HeaderContentNegotiationStrategy();

                configurer.strategies(Arrays.asList(strategy, headerContentNegotiationStrategy));
            }
        };
    }

}

自定义转换器类

自定义消息转换器,实现HttpMessageConverter接口

public class H3cMessageConverter implements HttpMessageConverter<User> {

    // 判断能否读
    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return false;
    }
	
    // 判断能否写
    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        // 这里自定义的,可以根据具体逻辑修改
        // 这里判断的是返回值类型是否为User 也就是测试接口写的返回值类型
        return clazz.isAssignableFrom(User.class);
    }

    // 获取到允许的媒体类型,
    @Override
    public List<MediaType> getSupportedMediaTypes() {
        // 也就是上面判断通过后,这里返回允许的媒体类型
        return MediaType.parseMediaTypes("application/x-h3c");
    }

    @Override
    public User read(Class<? extends User> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    @Override
    public void write(User user, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        // 写操作
        // 这里正常业务逻辑肯定不是这样写 这里只是测试
        String data = "h3c" + user.toString();

        OutputStream body = outputMessage.getBody();
        body.write(data.getBytes());
    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

为人师表好少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值