从content-type设置看Spring MVC处理header的一个坑

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从content-type设置看Spring MVC处理header的一个坑相关的知识,希望对你有一定的参考价值。

我们经常需要在HttpResponse中设置一些headers,我们使用Spring MVC框架的时候我们如何给Response设置Header呢?

Sooooooooooooo easy, 看下面的代码:

    @RequestMapping(value = "/rulelist", method = RequestMethod.GET)
    @ResponseBody
    public String getRuleList(HttpServletRequest request,
            HttpServletResponse response) {
        response.addHeader("test", "test"); 
        return service.getRuleList();
    }

通过验证,我们可以看到test项已经被成功添加到response的头部信息

Content-Length:	2 kilobytes
Content-Type:	text/plain;charset=ISO-8859-1
Server:	Apache-Coyote/1.1
test: test

接下来,我们希望修改Content-Type,从而统一服务器端和客户端的内容编码。我们继续修改代码,

    @RequestMapping(value = "/rulelist", method = RequestMethod.GET)
    @ResponseBody
    public String getRuleList(HttpServletRequest request,
            HttpServletResponse response) {
        response.addHeader("Content-Type", "application/json;charset=UTF-8");
        return service.getRuleList();
    }

接下来,我们验证一下结果:

Content-Length:	2 kilobytes
Content-Type:	text/plain;charset=ISO-8859-1
Server:	Apache-Coyote/1.1

和我们预想的并一样,response的content-type header没有被设置成"application/json;charset=UTF-8",很令人困惑。

那么,接下来让我们来探索下Spring MVC内部是如何处理这一过程的。首先我们先要对Spring MVC框架处理Http请求的流程有一个整体的了解。

下图清晰地向大家展示了Spring MVC处理HTTP请求的流程,(图片来自网络)

 

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

具体流程如下:

1. DispatcherServlet接收到Request请求

2. HandlerMapping选择一个合适的Handler处理Request请求

3-4. 选择合适的HandlerAdapter,调用用户编写的Controller处理业务逻辑。(HandlerAdapter主要是帮助Spring MVC支持多种类型的Controller)

5. Controller将返回结果放置到Model中并且返回view名称给Handler Adapter

6. DispatcherServlet选择合适的ViewResolver来生成View对象

7-8. View对象利用Model中的数据进行渲染并返回数据

相信大家对于上面的处理流程并不陌生,上面的流程图向我们展示了SpringMVC生成ModelAndView并返回response的大体流程。

下面我们来看看我们上面代码片段的处理流程是如何进行的?

从上面的流程图我们可以看到,content-type header是单独被处理的,具体过程可以参考下面的源码(AbstractMessageConverterMethodProcessor):

	protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType,
			ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
			throws IOException, HttpMediaTypeNotAcceptableException {

		Class<?> returnValueClass = getReturnValueType(returnValue, returnType);
		HttpServletRequest servletRequest = inputMessage.getServletRequest();
		List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest);

//适合的兼容media types类型
实际上,我们可以使用produces = {}来指定我们需要的mediatype
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass); Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>(); for (MediaType requestedType : requestedMediaTypes) { for (MediaType producibleType : producibleMediaTypes) { if (requestedType.isCompatibleWith(producibleType)) { compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType)); } } } if (compatibleMediaTypes.isEmpty()) { if (returnValue != null) { throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes); } return; } List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes); MediaType.sortBySpecificityAndQuality(mediaTypes); MediaType selectedMediaType = null;
   //选择最匹配的mediaType for (MediaType mediaType : mediaTypes) { 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<?> messageConverter : this.messageConverters) {
//遍历messageConvertors, 寻找可以处理相应返回类型和mediatype的HttpMessageConvertor if (messageConverter.canWrite(returnValueClass, selectedMediaType)) { returnValue = this.adviceChain.invoke(returnValue, returnType, selectedMediaType, (Class<HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage); if (returnValue != null) {
//这里将会填充mediatype到header,并将httpmessage发送给请求者 ((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { logger.debug("Written [" + returnValue + "] as \\"" + selectedMediaType + "\\" using [" + messageConverter + "]"); } } return; } } } if (returnValue != null) { throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes); } }

接下来,将选择好的mediatype写入到HttpOutputMessage中

	public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {

		final HttpHeaders headers = outputMessage.getHeaders();
//设置contenttype到HttpOutputMessage if (headers.getContentType() == null) { MediaType contentTypeToUse = contentType; if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) { contentTypeToUse = getDefaultContentType(t); } if (contentTypeToUse != null) { headers.setContentType(contentTypeToUse); } } if (headers.getContentLength() == -1) { Long contentLength = getContentLength(t, headers.getContentType()); if (contentLength != null) { headers.setContentLength(contentLength); } }           /* 省略了不相干代码 */ }

最终的Headers设置在ServletServerHttpResponse类中完成,

	private void writeHeaders() {
		if (!this.headersWritten) {
			for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
				String headerName = entry.getKey();
				for (String headerValue : entry.getValue()) {
//将复合类中之前设置的header(content-type)内容补充到servletResponse this.servletResponse.addHeader(headerName, headerValue); } } // HttpServletResponse exposes some headers as properties: we should include those if not already present if (this.servletResponse.getContentType() == null && this.headers.getContentType() != null) { this.servletResponse.setContentType(this.headers.getContentType().toString()); } if (this.servletResponse.getCharacterEncoding() == null && this.headers.getContentType() != null && this.headers.getContentType().getCharSet() != null) { this.servletResponse.setCharacterEncoding(this.headers.getContentType().getCharSet().name()); } this.headersWritten = true; } }

从上述的代码中,我们可以看到在RequestResponseBodyMethodProcessor这个ReturnValueHandler中,media-type被单独的逻辑进行处理,因此直接在ServletResponse中设置content-type header并不能正常生效。

需要在@RequestMapping中添加produces = {} 进行设置才可以。

 

以上是关于从content-type设置看Spring MVC处理header的一个坑的主要内容,如果未能解决你的问题,请参考以下文章

Http请求中Accept、Content-Type讲解以及在Spring MVC中的应用

在 Spring Reactive 中使用 application.properties 在请求标头中设置 Content-type

深入理解Spring系列之十一:SpringMVC-@RequestBody接收json数据报415

朴春新曲《Spring》MV公开,朴山多拉参与Feat

Spring Boot配置FastJson报错'Content-Type' cannot contain wildcard type '*'

常用Rest API各类注解参数接收对照