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
。並通過構造器傳遞給父類AbstractHttpMessageConverter
的defaultCharset
屬性
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走起
呼叫堆疊如下圖所示:
響應頭的設定在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-Type
與 Content-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
屬性作為預設的字符集。