从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 = {} 进行设置才可以。

时间: 2024-10-24 15:18:33

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

spring mvc mybatis集成踩的坑

开园这么多年了也没写几篇文章,现在想想光看别人的也不行啊,咱也自己写写,就写这天我我在做spring mvc与mybatis的集成时遇到的问题 1 spring与mybatis的集成 这个相信大家都弄过吧,不过我还是要说说.首先咱们得先把项目大体的架子搭起来,本人用的IDEA,废话不多说来上图 这是我的spring和mybatis的项目的结构,这我相信大家都能看明白,不用我多说什么.其实最主要的还是applicationContext.xml文件 1 <?xml version="1.0&

基于maven来Spring MVC的环境搭建遇到“坑”

1.注解配置路径问题: 在web.xml中配置spring mvc 路径时, 应该配置如下:classpath:classpath:spring-* 2.jdk版本和Spring MVC版本不一致问题: 运行4.0以上的spring web-mvc需要变更jdk为jdk8.0以上: 3.spring mvc版本变更或jdk版本变更 在变更spring 版本时,遇到,变更pom后依赖未更新问题:删除maven依赖后,重新配置.classpath;并更新maven项目才解决: 具体操作如下: htt

Eclipse开发环境设置(Maven+Spring MVC+Flex)

1. 环境设置 1.1. Java环境设置 1)JAVA_HOME D:\GreenSoftware\Java\Java8X64\jdk1.8.0_91 2)PATH ;%JAVA_HOME%/bin;%JAVA_HOME%/jre/bin 3)CLASSPATH .;%JAVA_HOME%/lib;%JAVA_HOME%/lib/dt.jar;%JAVA_HOME%/lib/tools.jar 1.2. Mave环境设置 1.2.1. Maven环境变量 4)MAVEN_HOME D:\Gre

spring mvc获取header

两种方法: 1.在方法参数中加入@RequestHeader 2.在类级别注入HttpServletRequest 建议使用第二种方法,这样可避免每个方法都加入HttpHeaders参数 @Controller @RequestMapping("/hello") public class HelloController { @Autowired private HttpServletRequest request; @RequestMapping(value="/printn

Spring MVC 学习总结(二)——控制器定义与@RequestMapping详解

一.控制器定义 控制器提供访问应用程序的行为,通常通过服务接口定义或注解定义两种方法实现. 控制器解析用户的请求并将其转换为一个模型.在Spring MVC中一个控制器可以包含多个Action(动作.方法). 1.1.实现接口Controller定义控制器 Controller是一个接口,处在包org.springframework.web.servlet.mvc下,接口中只有一个未实现的方法,具体的接口如下所示: package org.springframework.web.servlet.

Spring MVC 3.0.5+Spring 3.0.5+MyBatis3.0.4全注解实例详解(四)

这一章大象将详细分析web层代码,以及使用Spring MVC的注解及其用法和其它相关知识来实现控制器功能.     之前在使用Struts2实现MVC的注解时,是借助struts2-convention这个插件,如今我们使用Spring自带的spring-webmvc组件来实现同样的功能,而且比之以前更简单.另外,还省掉了整合两个框架带来的不稳定因素.     对于Spring MVC框架,我主要讲一下它的常用注解,再结合一些示例进行说明,方便大家能够快速理解.     一.Spring MV

string与spring mvc集成多种技术构建复杂工程(转载)

使用spring集成其他技术,最基本的配置都是模板化的,比如配置视图模板引擎.数据库连接池.orm框架.缓存服务.邮件服务.rpc调用等,以spring的xml配置为例,我将这些配置过程整理出来,并不时更新,以备参考! spring 在普通的java工程中引入spring,只需要配置以下依赖 1 2 3 4 5 <dependency>     <groupId>org.springframework</groupId>     <artifactId>sp

Spring mvc 上下文初始化过程

在软件开发的中,如果某些特性的使用比较普遍,那么这些特性往往可以作为平台特性来实现,通过对这些平台特性进行有效的封装,使其向其他应用开放.正是如此,Spring由于其IOC.AOP.事务处理.持久化驱动等特点,使得其起到了一个应用平台的作用.Spring MVC是Spring的一个重要的模块,其web应用的实现,是由Spring的来支撑的,Spring MVC的是实现也是依托再Spring平台提供的基础特性的.本文主要是介绍Spring mvc容器初始化的过程,从中可以看出Spring MVC的

Spring MVC学习回顾

Spring MVC是现在新项目中使用最多的MVC框架,超越了Structs2成为MVC框架的首选.今天抽时间看了4.2.x的官网翻译文档及相关代码,博客,将印象比较深的几点记录一下. 一.应用Spring MVC 首先引入相关依赖,以maven项目管理为例,先在pom.xml引入spring-core,spring-beans,spring-context,spring-web,spring-webmvc相关的jar包.然后再web.xml中配置前端集中控制器DispatcherServlet