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 写出,也就是返回响应报文,其实也是不准确的。 
chrome 
这里写图片描述
可以看到客户端的不同导致输出也不同。 
测试下: 
这里写图片描述
这里写图片描述

可以看到响应报文里的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;

}
原文地址:https://www.cnblogs.com/shamo89/p/9095295.html