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; 
 
}

 

原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/15727.html

(0)
上一篇 2021年7月19日 18:42
下一篇 2021年7月19日 18:42

相关推荐

发表回复

登录后才能评论