spring-web中的StringHttpMessageConverter簡介
spring的http請求內容轉換,類似netty的handler轉換。本文旨在通過分析StringHttpMessageConverter
來初步認識消息轉換器HttpMessageConverter
的處理流程。分析完StringHttpMessageConverter
便可以窺視SpringMVC消息處理的廬山真面目了。
/** * HttpMessageConverter 的實現類:完成請求報文到字符串和字符串到響應報文的轉換 * 默認情況下,此轉換器支持所有媒體類型(*/*),並使用 Content-Type 為 text/plain 的內容類型進行寫入 * 這可以通過 setSupportedMediaTypes(父類 AbstractHttpMessageConverter 中的方法) 方法設置 supportedMediaTypes 屬性來覆蓋*/ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> { // 默認字符集(產生亂碼的根源) public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1"); //可使用的字符集 private volatile List<Charset> availableCharsets; //標識是否輸出 Response Headers:Accept-Charset(默認輸出)private boolean writeAcceptCharset = true; /** * 使用 "ISO-8859-1" 作為默認字符集的默認構造函數 */ public StringHttpMessageConverter() { this(DEFAULT_CHARSET); } /** * 如果請求的內容類型 Content-Type 沒有指定一個字符集,則使用構造函數提供的默認字符集 */ public StringHttpMessageConverter(Charset defaultCharset) {super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL); } /** * 標識是否輸出 Response Headers:Accept-Charset * 默認是 true */ public void setWriteAcceptCharset(boolean writeAcceptCharset) { this.writeAcceptCharset = writeAcceptCharset; } @Override public boolean supports(Class<?> clazz) { return String.class == clazz; } /** * 將請求報文轉換為字符串 */ @Override protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException { //通過讀取請求報文裏的 Content-Type 來獲取字符集 Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType()); //調用 StreamUtils 工具類的 copyToString 方法來完成轉換 return StreamUtils.copyToString(inputMessage.getBody(), charset); } /** * 返回字符串的大小(轉換為字節數組後的大小) * 依賴於 MediaType 提供的字符集 */ @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 { //輸出 Response Headers:Accept-Charset(默認輸出) if (this.writeAcceptCharset) { outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets()); } Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType()); //調用 StreamUtils 工具類的 copy 方法來完成轉換 StreamUtils.copy(str, charset, outputMessage.getBody()); } /** * 返回所支持的字符集 * 默認返回 Charset.availableCharsets() * 子類可以覆蓋該方法 */ protected List<Charset> getAcceptedCharsets() { if (this.availableCharsets == null) { this.availableCharsets = new ArrayList<Charset>( Charset.availableCharsets().values()); } return this.availableCharsets; } /** * 獲得 ContentType 對應的字符集 */ private Charset getContentTypeCharset(MediaType contentType) { if (contentType != null && contentType.getCharset() != null) { return contentType.getCharset(); } else { return getDefaultCharset(); } } }
解讀:
private boolean writeAcceptCharset = true;
是說是否輸出以下內容:
可以使用如下配置屏蔽它:
<mvc:annotation-driven> <mvc:message-converters> <bean id="messageConverter" class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="writeAcceptCharset" value="false"/> </bean> </mvc:message-converters> </mvc:annotation-driven>
private volatile List<Charset> availableCharsets;
沒有看到使用場合。
使用 text/plain
寫出,也就是返回響應報文,其實也是不準確的。
可以看到客戶端的不同導致輸出也不同。
測試下:
可以看到響應報文裏的Content-Type依賴於請求報文裏的Accept。
那麽當我們指定帶編碼的Accept
能否解決亂碼問題呢?
其實很簡單的道理,你他丫的希望接受的數據類型是Accept: text/plain;charset=UTF-8
,我他丫的發送的數據類型Content-Type: text/plain;charset=UTF-8
當然也要保持一致。
StringHttpMessageConverter的哲學便是:你想要什麽類型的數據,我便發送給你該類型的數據。
在操蛋的Windows操作系統上處理編解碼問題是真的操蛋!
cmd下 chcp 65001
或者使用Cygwin都他媽的各種非正常亂碼
索性去Ubuntu測試去了。
@RequestMapping(value = "/testCharacter", method = RequestMethod.POST) @ResponseBody public String testCharacter2(@RequestBody String str) { System.out.println(str); return "你大爺"; }
curl -H "Content-Type: text/plain; charset=UTF-8" -H "Accept: text/plain; charset=UTF-8" -d "你大爺"
http://localhost:8080/SpringMVCDemo/testCharacter
Jetty容器輸出:你大爺
控制臺輸出:你大爺
curl -H "Accept: text/plain; charset=UTF-8" -d "你大爺"
http://localhost:8080/SpringMVCDemo/testCharacter
Jetty容器輸出:%E4%BD%A0%E5%A4%A7%E7%88%B7
控制臺輸出:你大爺
%E4%BD%A0%E5%A4%A7%E7%88%B7
使用了URL編碼解碼後還是字符串你大爺
curl -H "Content-Type: text/plain; charset=UTF-8" -d "你大爺"
http://localhost:8080/SpringMVCDemo/testCharacter
Jetty容器輸出:你大爺
控制臺輸出:???
原理通過讀一下代碼就清楚了:
@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 void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException { if (this.writeAcceptCharset) { outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets()); } Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType()); StreamUtils.copy(str, charset, outputMessage.getBody()); }
而以往我們解決亂碼問題的辦法形如:
@RequestMapping(value = "/test1", method = RequestMethod.POST) @ResponseBody public void test1(HttpServletRequest request) throws IOException { InputStream in = request.getInputStream(); byte[] buffer = new byte[in.available()]; in.read(buffer); in.close(); String str = new String(buffer, "gb2312"); System.out.println(str); }
以什麽格式輸入的字符串,就得以相應的格式進行轉換。
/** * 實現 HttpMessageConverter 的抽象基類 * * 該基類通過 Bean 屬性 supportedMediaTypes 添加對自定義 MediaTypes 的支持 * 在輸出響應報文時,它還增加了對 Content-Type 和 Content-Length 的支持 */ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> { /** Logger 可用於子類 */ protected final Log logger = LogFactory.getLog(getClass()); // 存放支持的 MediaType(媒體類型)的集合 private List<MediaType> supportedMediaTypes = Collections.emptyList(); // 默認字符集 private Charset defaultCharset; /** * 默認構造函數 */ protected AbstractHttpMessageConverter() { } /** * 構造一個帶有一個支持的 MediaType(媒體類型)的 AbstractHttpMessageConverter */ protected AbstractHttpMessageConverter(MediaType supportedMediaType) { setSupportedMediaTypes(Collections.singletonList(supportedMediaType)); } /** * 構造一個具有多個支持的 MediaType(媒體類型)的 AbstractHttpMessageConverter */ protected AbstractHttpMessageConverter(MediaType... supportedMediaTypes) { setSupportedMediaTypes(Arrays.asList(supportedMediaTypes)); } /** * 構造一個帶有默認字符集和多個支持的媒體類型的 AbstractHttpMessageConverter */ protected AbstractHttpMessageConverter(Charset defaultCharset, MediaType... supportedMediaTypes) { this.defaultCharset = defaultCharset; setSupportedMediaTypes(Arrays.asList(supportedMediaTypes)); } /** * 設置此轉換器支持的 MediaType 對象集合 */ public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) { // 斷言集合 supportedMediaTypes 是否為空 Assert.notEmpty(supportedMediaTypes, "MediaType List must not be empty"); this.supportedMediaTypes = new ArrayList<MediaType>(supportedMediaTypes); } @Override public List<MediaType> getSupportedMediaTypes() { return Collections.unmodifiableList(this.supportedMediaTypes); } /** * 設置默認字符集 */ public void setDefaultCharset(Charset defaultCharset) { this.defaultCharset = defaultCharset; } /** * 返回默認字符集 */ public Charset getDefaultCharset() { return this.defaultCharset; } /** * 該實現檢查該轉換器是否支持給定的類,以及支持的媒體類型集合是否包含給定的媒體類型 */ @Override public boolean canRead(Class<?> clazz, MediaType mediaType) { return supports(clazz) && canRead(mediaType); } /** * 如果該轉換器所支持的媒體類型集合包含給定的媒體類型,則返回true * mediaType: 要讀取的媒體類型,如果未指定,則可以為null。 通常是 Content-Type 的值 */ protected boolean canRead(MediaType mediaType) { if (mediaType == null) { return true; } for (MediaType supportedMediaType : getSupportedMediaTypes()) { if (supportedMediaType.includes(mediaType)) { return true; } } return false; } /** * 該實現檢查該轉換器是否支持給定的類,以及支持的媒體類型集合是否包含給定的媒體類型 */ @Override public boolean canWrite(Class<?> clazz, MediaType mediaType) { return supports(clazz) && canWrite(mediaType); } /** * 如果給定的媒體類型包含任何支持的媒體類型,則返回true * mediaType: 要寫入的媒體類型,如果未指定,則可以為null。通常是 Accept 的值 * 如果支持的媒體類型與傳入的媒體類型兼容,或媒體類型為空,則返回 true */ protected boolean canWrite(MediaType mediaType) { if (mediaType == null || MediaType.ALL.equals(mediaType)) { return true; } for (MediaType supportedMediaType : getSupportedMediaTypes()) { if (supportedMediaType.isCompatibleWith(mediaType)) { return true; } } return false; } /** * readInternal(Class, HttpInputMessage) 的簡單代理方法 * 未來的實現可能會添加一些默認行為 */ @Override public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException { return readInternal(clazz, inputMessage); } /** * 該實現通過調用 addDefaultHeaders 來設置默認頭文件,然後調用 writeInternal 方法 */ @Override public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { final HttpHeaders headers = outputMessage.getHeaders(); addDefaultHeaders(headers, t, contentType); if (outputMessage instanceof StreamingHttpOutputMessage) { StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage; streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() { @Override public void writeTo(final OutputStream outputStream) throws IOException { writeInternal(t, new HttpOutputMessage() { @Override public OutputStream getBody() throws IOException { return outputStream; } @Override public HttpHeaders getHeaders() { return headers; } }); } }); } else { writeInternal(t, outputMessage); outputMessage.getBody().flush(); } } /** * 將默認 HTTP Headers 添加到響應報文 */ 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); } else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) { MediaType mediaType = getDefaultContentType(t); contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse); } if (contentTypeToUse != null) { if (contentTypeToUse.getCharset() == null) { Charset defaultCharset = getDefaultCharset(); if (defaultCharset != null) { contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset); } } //設置Content-Type headers.setContentType(contentTypeToUse); } } if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) { Long contentLength = getContentLength(t, headers.getContentType()); if (contentLength != null) { //設置Content-Length headers.setContentLength(contentLength); } } } /** * 返回給定類型的默認內容類型 * 當 write(final T t, MediaType contentType, HttpOutputMessage outputMessage) 的 MediaType * 為 null 時,被調用 * 默認情況下,這將返回 supportedMediaTypes 集合中的第一個元素(如果有) * 可以在子類中被覆蓋 */ protected MediaType getDefaultContentType(T t) throws IOException { List<MediaType> mediaTypes = getSupportedMediaTypes(); return (!mediaTypes.isEmpty() ? mediaTypes.get(0) : null); } /** * 返回給定類型(字符集)的內容長度 */ protected Long getContentLength(T t, MediaType contentType) throws IOException { return null; } /** * 指示該轉換器是否支持給定的類 */ protected abstract boolean supports(Class<?> clazz); /** * 抽象模板方法:讀取實際對象 */ protected abstract T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; /** * 抽象模板方法: 輸出響應報文 */ protected abstract void writeInternal(T t, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; }
spring-web中的StringHttpMessageConverter簡介