使用spring MVC构建RESTful Web Services(三):内容协商

在上一节我们说,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如何使用

使用spring MVC构建RESTful Web Services(三):内容协商_第1张图片

1客户端—发送请求—服务器:客户端通过请求头 Content-Type 指定内容体的媒体类型(即客户端此时是生产者),服务器根据 Content-Type 消费内容体数据(即服务器此时是消费者);

2服务器—发送请求—客户端:服务器生产响应头 Content-Type 指定的响应体数据(即服务器此时是生产者),客户端根 据 Content-Type 消费内容体数据(即客户端此时是消费者)。

在通过浏览器查看网页时,客户端就是浏览器。当我们通过程序去调用HTTP服务时,客户端就是我们的代码。

二、Accept头信息

客户端可以通过Content-type头告诉服务器它的数据是什么媒体类型,那它如何告诉服务器端它只消费什么媒体类型的数据呢?即客户端接受(需要)什么类型的数据呢?服务器应该生产什么类型的数据?此时我们可以请求的 Accept 请求头来实现这个功能。

Accept:用来指定什么媒体类型的响应是可接受的,即告诉服务器我需要什么媒体类型的数据,此时服务器应该 根据 Accept 请求头生产指定媒体类型的数据。

在java代码中,可以用这种方式指定我们需要什么数据。

request.getHeaders().set("Accept", "application/xml"):

生产者、消费者流程图:

使用spring MVC构建RESTful Web Services(三):内容协商_第2张图片

请求阶段:客户端是生产者【生产 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构建RESTful Web Services(三):内容协商_第3张图片

下面看一下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一块还是使用了开涛整理的信息。

你可能感兴趣的:(spring,mvc,REST,WebServices)