在上一节我们说,message converter可以处理相应MediaType对应的格式的数据与java对象间实现转换。例如Jaxb2RootElementHttpMessageConverter可以实现XML数据与java对象间的转换,可以实现JSON数据与java对象间的转换。那么,spring MVC的处理流程是怎样的呢?这就需要我们了解Content-Type、Accept头信息和内容协商的知识。
一、Content-Type头信息
Content-Type:内容类型,即请求/响应的内容区数据的媒体类型。它是HTTP头信息,在每次HTTP交互时都需要带上它。
相应很多朋友都这样写过代码:
public void response1(HttpServletResponse response) throws IOException { //1表示响应的内容区数据的媒体类型为html格式,且编码为utf-8(客户端应该以utf-8解码) response.setContentType("text/html;charset=utf-8"); //2写出响应体内容 response.getWriter().write("<font style='color:red'>hello</font>"); }
这里的setContentType就是设置的就是这次HTTP传输(请求或响应,这里是响应)的消息体的数据格式。即MediaType,后面用分号隔开指定了编码。可以看出 Content-Type 可以指定请求/响应的内容体的媒体格式和可选的编码方式。
通过下面的图片可以看到Content-type如何使用
1客户端—发送请求—服务器:客户端通过请求头 Content-Type 指定内容体的媒体类型(即客户端此时是生产者),服务器根据 Content-Type 消费内容体数据(即服务器此时是消费者);
2服务器—发送请求—客户端:服务器生产响应头 Content-Type 指定的响应体数据(即服务器此时是生产者),客户端根 据 Content-Type 消费内容体数据(即客户端此时是消费者)。
在通过浏览器查看网页时,客户端就是浏览器。当我们通过程序去调用HTTP服务时,客户端就是我们的代码。
二、Accept头信息
客户端可以通过Content-type头告诉服务器它的数据是什么媒体类型,那它如何告诉服务器端它只消费什么媒体类型的数据呢?即客户端接受(需要)什么类型的数据呢?服务器应该生产什么类型的数据?此时我们可以请求的 Accept 请求头来实现这个功能。
Accept:用来指定什么媒体类型的响应是可接受的,即告诉服务器我需要什么媒体类型的数据,此时服务器应该 根据 Accept 请求头生产指定媒体类型的数据。
在java代码中,可以用这种方式指定我们需要什么数据。
request.getHeaders().set("Accept", "application/xml"):
生产者、消费者流程图:
请求阶段:客户端是生产者【生产 Content-Type 媒体类型的请求内容区数据】,服务器是消费者【消费客户端生产的 Content-Type 媒体类型的请求内容区数据】;
响应阶段:服务器是生产者【生产客户端请求头参数 Accept 指定的响应体数据】,客户端是消费者【消费服务器根据 Accept 请求头生产的响应体数据】。
比如我访问这个URL,这是微信发送消息的接口,提交方式是post。可以看到定义了Accept头和Content-type头,因为我的数据是json格式,所以我定义了“Content-type:application/json”.
POST /cgi-bin/message/custom/send?access_token=GxuYcmghKY-2AI989eXhWCU0vptXMP_L_4o5fE-d__nkQ6AuvVUI1L_kB00qAFhdvXkDgDE1RyCmRWtFDxkQRQ HTTP/1.1 Host: api.weixin.qq.com Connection: keep-alive Content-Length: 127 Cache-Control: no-cache Content-Type: application/json Accept: application/json Accept-Encoding: gzip,deflate,sdch Accept-Language: zh-CN,zh;q=0.8,en;q=0.6 Cookie: pgv_pvid=108371614 {"touser":"o7yc0uCkXmUGcoukIctVT0AmdT9w","msgtype":"text","text":{"content":"Hello World"}}
因为我们希望微信返回JSON数据,所以我们定义了“Accept:application/json”。看看微信服务器返回的信息。可以看到它也加了content-Type头,表示它返回的数据是JSON格式的。
HTTP/1.1 200 OK Server: nginx/1.4.4 Date: Wed, 21 May 2014 12:59:44 GMT Content-Type: application/json; encoding=utf-8 Content-Length: 27 Connection: keep-alive {"errcode":0,"errmsg":"ok"}
其实我们在请求这个接口的时候,是不需要加“Accept:application/json”这个头的,因为微信接口里规定它返回的就是JSON数据,它才不管你想要什么格式。为了不像它这么专制,我们下面使用内容协商。和客户商量,如果你想要JSON格式,我服务器端就把数据以JSON格式给你,如果你想要XML格式,我服务器就把数据以XML格式给你。这多和气!当然,我们的服务器也是有脾气的,只支持这两种格式,你需要其他格式那我就不管你了。
三、内容协商
如果你想将数据转换成JSON或XML格式返回给客户,在spring MVC有两种方式,一种是方法返回View对象,这个View对象去帮你转换数据;还有一种方式就是我们现在用的,方法返回类型是java对象,然后加@ReqponseBody注解,调用message converter来转换。
在spring MVC3.2之前,@ResponseBody这种方式的内容协商功能并不强大,它只支持根据客户端的Accept头来决定返回数据的格式。返回View对象方式的内容协商功能强大很多,它支持我们定义用后缀对应数据返回格式,如xxxx.xml返回XML格式数据,xxxx.json返回JSON格式的数据,以减少对Accept头的依赖,因为Accept头有时候会很复杂,比如这样:Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,我们访问网页的时候浏览器默认加的Accept头就是这样,除了多个mediaType,后面还在权重,真挺晕。spring MVC3.2把这两个做了整合,现在两种数据处理方法都使用通用的内容协商方法。
sping MVC3的内容协商支持三种方式:
1.使用后缀,如.json、.xml后缀和处理类型的关系可以自己定义
2.前面说的使用Accept头
3.在访问时request请求的参数,比如每次请求request都会加format=xml参数,表示要求返回XML格式数据,默认参数名是format,可以修改。
spring MVC规定,如果同时开启了上面的部分或全部方式,解析顺序是后缀、参数、Accept头。对我来说,还是比较喜欢用Accept头,用的时间长,比较适应。
spring文件中的配置方式如下:
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"> <mvc:message-converters register-defaults="false"> <bean class="org.springframework.http.converter.FormHttpMessageConverter"/> <bean class="org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter"/> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper" ref="objectMapper"/> </bean> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <constructor-arg name="defaultCharset" value="UTF-8"/> </bean> </mvc:message-converters> </mvc:annotation-driven> <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean"> <property name="favorPathExtension" value="true" /> <property name="favorParameter" value="false" /> <property name="ignoreAcceptHeader" value="false"/> <property name="mediaTypes" > <value> json=application/json xml=application/xml </value> </property> <property name="defaultContentType" value="application/json"/> </bean>
favorPathExtension参数表示是否开启后缀,默认true。favorParameter参数表示是否开启request参数识别,默认false。ignoreAcceptHeader表示是否关闭accept头识别,默认false,即默认开启accept头识别。defaultContentType表示服务器默认的MediaType类型。
在这里spring MVC使用了策略模式,每一种协商方式都是一种策略,在策略选择时,按照我们之前说的顺序来处理。下面是类图。PathExtensionContentNegotiationStrategy对应后缀处理,ParameterContentNegotiationStrategy对应request参数识别,HeaderContentNegotiationStrategy对应accept头识别,FixedContentNegotiationStrategy对应指定MediaType类型。
下面看一下spring MVC的处理逻辑,这是org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor类的一段代码
protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException { // 返回值类型 Class<?> returnValueClass = returnValue.getClass(); HttpServletRequest servletRequest = inputMessage.getServletRequest(); //计算request请求中的客户端需要的MediaType List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest); //计算服务器端支持的MeidaType类型(根据返回数据类型判断) List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass); //将请求的类型和服务端支持的类型合并计算,协商得到客户端和服务端都可以支持的MediaType类型 Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>(); for (MediaType r : requestedMediaTypes) { for (MediaType p : producibleMediaTypes) { if (r.isCompatibleWith(p)) { compatibleMediaTypes.add(getMostSpecificMediaType(r, p)); } } } //从可支持的类型中再作选择,最终选出最适合的一个MediaType,代码略 MediaType selectedMediaType = null; //调用message converter来作数据转换,转换后的流写入response if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); for (HttpMessageConverter<?> messageConverter : messageConverters) { if (messageConverter.canWrite(returnValueClass, selectedMediaType)) { ((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" + messageConverter + "]"); } return; } } } throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes); }
计算request请求中的客户端需要的MediaType时使用的就是我们前面设置的contentNegotiationManager:
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request)); return mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes; }
计算服务器端支持的MeidaType类型(根据返回数据类型判断)时使用的就是我们之前设置的message converter,它会把能处理当前返回结果的所有message converter配置的MediaType全部计算出来并返回。
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> returnValueClass) { Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList<MediaType>(mediaTypes); } else if (!allSupportedMediaTypes.isEmpty()) { List<MediaType> result = new ArrayList<MediaType>(); for (HttpMessageConverter<?> converter : messageConverters) { if (converter.canWrite(returnValueClass, null)) { result.addAll(converter.getSupportedMediaTypes()); } } return result; } else { return Collections.singletonList(MediaType.ALL); } }
下面看一个简单的例子,比如我调用返回用户信息的接口。先贴代码,POJO。可以看到为了返回集合我们还专门定义了WsUserList类,它封装了WsUser的集合,因为JAXB需要加注解,无解,只能多一个类了。
@XmlRootElement(name = "users") public class WsUserList { @XmlElement(name = "user") private List<WsUser> users; public List<WsUser> getUsers() { if (users == null) users = new ArrayList<WsUser>(); return users; } } @XmlRootElement(name = "wsUser") @XmlAccessorType(XmlAccessType.FIELD) @XmlType(propOrder = {"userId", "userName", "birthday", "description", "dept"}) public class WsUser implements Serializable { private Long userId; private String userName; @XmlJavaTypeAdapter(DateAdapter.class) private Date birthday; private String description; private WsDept dept; public WsUser() { } public WsUser(Long userId, String userName, Date birthday, String description, WsDept dept) { this.userId = userId; this.userName = userName; this.birthday = birthday; this.description = description; this.dept = dept; } //getter and setter } @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) public class WsDept { private String deptName; @XmlJavaTypeAdapter(DateAdapter.class) private Date createDate; private String provinceName; public WsDept() { } public WsDept(String deptName, Date createDate, String provinceName) { this.deptName = deptName; this.createDate = createDate; this.provinceName = provinceName; } //getter and setter }
控制器类:
@Controller @RequestMapping(value = "/users") public class RestUserController { @Autowired private WsUserService wsUserService; @RequestMapping(method = RequestMethod.GET) public @ResponseBody WsUserList listAll() { return wsUserService.listAll(); } @RequestMapping(value = "/{id}") public @ResponseBody WsUser findById(@PathVariable("id") Long id) { return wsUserService.findById(id); } @ResponseStatus(HttpStatus.OK) @RequestMapping(method = RequestMethod.POST) public void create(@RequestBody WsUser user) { wsUserService.create(user); } }
service层的代码就不贴了,只是简单的返回数据。
我们调用返回单条数据的url:http://localhost:8080/restful/users/1。如果我们的请求头加Accept:application/xml,我们返回的数据是这个格式:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <wsUser> <userId>1</userId> <userName>name1</userName> <birthday>2014-05-21 22:01:39</birthday> <description>name1</description> <dept> <deptName>北京</deptName> <createDate>2014-05-21 22:01:39</createDate> <provinceName>北京</provinceName> </dept> </wsUser>
如果我们的请求头加Accept:application/json,我们返回的数据是这个格式:
{ "userId": 1, "userName": "name1", "birthday": "2014-05-21 22:01:39", "description": "name1", "dept": { "deptName": "北京", "createDate": "2014-05-21 22:01:39", "provinceName": "北京" } }
可见,我们的代码能够根据客户端的要求协商返回数据,实现了我们内容协商的功能。
ps:关于content-type和accept一块还是使用了开涛整理的信息。