1. 程式人生 > >SpringMVC Content-Type解析

SpringMVC Content-Type解析

響應

為了測試方便,我們編寫了一個簡單的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>

測試程式碼:

@Controller
public 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 屬性作為預設的字符集。