zoukankan      html  css  js  c++  java
  • 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;
    
    }
  • 相关阅读:
    jQuery EasyUI API 中文文档 数字框(NumberBox)
    jQuery EasyUI API 中文文档 数值微调器(NumberSpinner)
    jQuery EasyUI API 中文文档 日期时间框(DateTimeBox)
    jQuery EasyUI API 中文文档 微调器(Spinner)
    jQuery EasyUI API 中文文档 树表格(TreeGrid)
    jQuery EasyUI API 中文文档 树(Tree)
    jQuery EasyUI API 中文文档 属性表格(PropertyGrid)
    EntityFramework 数据操作
    jQuery EasyUI API 中文文档 对话框(Dialog)
    jQuery EasyUI API 中文文档 组合表格(ComboGrid)
  • 原文地址:https://www.cnblogs.com/shamo89/p/9095295.html
Copyright © 2011-2022 走看看