1. 程式人生 > >spring-web中的StringHttpMessageConverter簡介

spring-web中的StringHttpMessageConverter簡介

getchar 指示 and frame url編碼 ise tracking intern .post

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簡介