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/15727.html

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

相关推荐

发表回复

登录后才能评论