SpringMVC Content-Type解析

来源:互联网 发布:手机桌面图标整理软件 编辑:程序博客网 时间:2024/05/19 04:53

响应

为了测试方便,我们编写了一个简单的HttpMessageConverter

package cn.bjut.converter;import java.io.IOException;import java.io.UnsupportedEncodingException;import java.nio.charset.Charset;import org.springframework.http.HttpInputMessage;import org.springframework.http.HttpOutputMessage;import org.springframework.http.MediaType;import org.springframework.http.converter.AbstractHttpMessageConverter;import org.springframework.util.StreamUtils;public class MyStringHttpMessageConverter extends AbstractHttpMessageConverter<String> {    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");    public MyStringHttpMessageConverter() {        this(DEFAULT_CHARSET);    }    public MyStringHttpMessageConverter(Charset defaultCharset) {        super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);    }    @Override    public boolean supports(Class<?> clazz) {        return String.class == clazz;    }    @Override    protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {        Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());        return StreamUtils.copyToString(inputMessage.getBody(), charset);    }    @Override    protected Long getContentLength(String str, MediaType contentType) {        Charset charset = getContentTypeCharset(contentType);        try {            return (long) str.getBytes(charset.name()).length;        }        catch (UnsupportedEncodingException ex) {            // should not occur            throw new IllegalStateException(ex);        }    }    @Override    protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {        Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());        StreamUtils.copy(str, charset, outputMessage.getBody());    }    private Charset getContentTypeCharset(MediaType contentType) {        if (contentType != null && contentType.getCharset() != null) {            return contentType.getCharset();        }        else {            return getDefaultCharset();        }    }}

以上代码(修改自StringHttpMessageConverter),我们把DEFAULT_CHARSET 即默认的字符集改为UTF-8。并通过构造器传递给父类AbstractHttpMessageConverterdefaultCharset 属性
super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);

为了测试方便,我们把其他的所有消息转换器屏蔽掉

<mvc:annotation-driven>        <mvc:message-converters register-defaults="false">            <bean class="cn.bjut.converter.MyStringHttpMessageConverter"/>        </mvc:message-converters>    </mvc:annotation-driven>

测试代码:

@Controllerpublic class TestController {    @RequestMapping("/test")    @ResponseBody    public String test() {        return "你大爷";    }}

debug走起
Fiddler

调用堆栈如下图所示:
调用堆栈

响应头的设置在AbstractHttpMessageConverter 类中

public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)            throws IOException, HttpMessageNotWritableException {        final HttpHeaders headers = outputMessage.getHeaders();        addDefaultHeaders(headers, t, contentType);        //...    }

经测试outputMessage.getHeaders(); 获得的HttpHeaders始终都是空。HttpHeaders实际上就是个Map,用来保存Http Header
public class HttpHeaders implements MultiValueMap<String, String>, Serializable

所以真正的处理在addDefaultHeaders 方法中。

/** * 在输出消息中设置响应头 * MediaType: 形如 text/plain 的媒体类型*/protected void addDefaultHeaders(HttpHeaders headers, T t, MediaType contentType) throws IOException{        if (headers.getContentType() == null) {            MediaType contentTypeToUse = contentType;            // 判断媒体类型是否包含通配符*            if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {                //                 contentTypeToUse = getDefaultContentType(t);            }            // 判断媒体类型是不是 application/octet-stream            else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {                MediaType mediaType = getDefaultContentType(t);                contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);            }            if (contentTypeToUse != null) {                // 判断媒体类型是否包含字符集(一般的媒体类型形如: "text/plain;charset=UTF-8")                if (contentTypeToUse.getCharset() == null) {                    // 设置默认字符集 this.defaultCharset                    Charset defaultCharset = getDefaultCharset();                    if (defaultCharset != null) {                        // 组建媒体类型(一般就形成了: "text/plain;charset=UTF-8")                        contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);                    }                }                // 将 Content-Type 添加到Http Header                headers.setContentType(contentTypeToUse);            }        }        if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {            Long contentLength = getContentLength(t, headers.getContentType());            if (contentLength != null) {                // 将 Content-Length 添加到Http Header                headers.setContentLength(contentLength);            }        }    }
protected MediaType getDefaultContentType(T t) throws IOException {        List<MediaType> mediaTypes = getSupportedMediaTypes();        return (!mediaTypes.isEmpty() ? mediaTypes.get(0) : null);    }

以上方法用来获得默认的Content-Type

@Override    public List<MediaType> getSupportedMediaTypes() {        return Collections.unmodifiableList(this.supportedMediaTypes);    }

getSupportedMediaTypes 方法获得属性supportedMediaTypes 保存的媒体类型。

supportedMediaTypes 是一个List集合
private List<MediaType> supportedMediaTypes = Collections.emptyList();

这里写图片描述
可见可以通过AbstractHttpMessageConverter 的子类来设置该属性。
那么既然是一个setter方法,我们也可以自行注入进去的(会覆盖构造函数的设置内容)。

<mvc:annotation-driven>        <mvc:message-converters register-defaults="false">            <bean class="cn.bjut.converter.MyStringHttpMessageConverter">                <property name="supportedMediaTypes">                    <list>                        <value>text/plain;charset=UTF-8</value>                        <value>text/plain;charset=UTF-8</value>                        <value>text/plain;charset=UTF-8</value>                    </list>                </property>            </bean>        </mvc:message-converters>    </mvc:annotation-driven>

如代码所示,默认取第一个mediaTypes.get(0)

public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {        Assert.notEmpty(supportedMediaTypes, "MediaType List must not be empty");        this.supportedMediaTypes = new ArrayList<MediaType>(supportedMediaTypes);    }

如上面的代码所示,如果我们自行配置supportedMediaTypes 则会覆盖掉通过构造函数添加进来的。

所以说addDefaultHeaders 方法添加默认Http Headers也就是添加Content-TypeContent-Length
这里写图片描述

请求

关于SpringMVC如何获得请求的Content-Type

在以上MyStringHttpMessageConverter 类中:

protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {        Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());        return StreamUtils.copyToString(inputMessage.getBody(), charset);    }
private Charset getContentTypeCharset(MediaType contentType) {        if (contentType != null && contentType.getCharset() != null) {            return contentType.getCharset();        }        else {            return getDefaultCharset();        }    }

如上面代码所示,如果我们在发出请求时没有携带Content-Type
请求头则使用AbstractHttpMessageConverter 里的defaultCharset 属性作为默认的字符集。