• 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;
    
    }
  • 相关阅读:
    【LeetCode】Hash
    【LeetCode】Heap
    【LeetCode】Stack
    【CodeVS】 纯OI题
    【LeetCode】String
    【LeetCode】Array
    WinForm窗体 常用属性
    C# ADO.NET 实体类中的属性扩展
    C# ADO.NET 三层架构
    C# ADO.NET 数据库的安全(sql 字符串注入攻击、使用占位符防止注入攻击)
  • 原文地址:https://www.cnblogs.com/shamo89/p/9095295.html
Copyright © 2020-2023  润新知