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 写出,也就是返回响应报文,其实也是不准确的。
可以看到客户端的不同导致输出也不同。
测试下:
可以看到响应报文里的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; }
spring-web中的StringHttpMessageConverter简介的更多相关文章
- spring web中的filter
昨天看了会spring web中部分代码,主要是各种filter,回顾一下: Spring的web包中中有很多过滤器,这些过滤器位于org.springframework.web.filter并且理所 ...
- Spring MVC中注解的简介
参考网址: https://blog.csdn.net/a67474506/article/details/46361195 @RequestMapping映射请求 SpringMVC 使用 @Re ...
- Spring5源码,Spring Web中的处理程序执行链
一.什么是Spring中的处理程序执行链? 二.HandlerExecutionChain类 三.自定义处理程序执行链 Spring的DispatcherServlet假如缺少几个关键元素将无法分派请 ...
- spring web中完成单元测试
对于在springweb总完单元测试,之前找过些资料,摸索了很久,记录下最终自己使用的方法 1,创建测试类,创建测试资源文件夹 src/test/resources/WEB_INFO/conf 将工程 ...
- 优化Web中的性能
优化Web中的性能 简介 web的优化就是一场阻止http请求最终访问到数据库的战争. 优化的方式就是加缓存,在各个节点加缓存. web请求的流程及节点 熟悉流程及节点,才能定位性能的问题.而且优化的 ...
- Web中的性能优化
优化Web中的性能 简介 web的优化就是一场阻止http请求最终访问到数据库的战争.优化的方式就是加缓存,在各个节点加缓存. web请求的流程及节点 熟悉流程及节点,才能定位性能的问题.而且优化的顺 ...
- Spring Security 中的过滤器
本文基于 spring-security-core-5.1.1 和 tomcat-embed-core-9.0.12. Spring Security 的本质是一个过滤器链(filter chain) ...
- Spring Boot中的Properties
文章目录 简介 使用注解注册一个Properties文件 使用属性文件 Spring Boot中的属性文件 @ConfigurationProperties yaml文件 Properties环境变量 ...
- Spring Boot中的测试
文章目录 简介 添加maven依赖 Repository测试 Service测试 测试Controller @SpringBootTest的集成测试 Spring Boot中的测试 简介 本篇文章我们 ...
随机推荐
- zabbix3.4 源码部署
centos6.8 系统 zabbix3.4.9 msyql5.7.22 php5.6.24 在centos6上面源码部署zabbix.3.4 问题比较多,需要花不少时间去解决,建议 ...
- js 和 jquery的宽高
window 和 document : window 对象表示浏览器打开的窗口,可以省略 document对象(浏览器的html文档)是window对象的一部分 document.body等于wind ...
- SPOJ HIGH Highways
In some countries building highways takes a lot of time... Maybe that's because there are many possi ...
- 转:C#制作ORM映射学习笔记一 自定义Attribute类
之前在做unity项目时发现只能用odbc连接数据库,感觉非常的麻烦,因为之前做web开发的时候用惯了ORM映射,所以我想在unity中也用一下ORM(虽然我知道出于性能的考虑这样做事不好的,不过自己 ...
- Android中节操播放器JieCaoVideoPlayer使用
效果 使用 即便是自定义UI,或者对Library有过修改,也是这五步骤来使用播放器. 1.添加类库 compile 'cn.jzvd:jiaozivideoplayer:6.0.0' 2.添加布局 ...
- github如何实现fork的项目与原项目同步
refer to https://www.jianshu.com/p/fede3333205f 作者:hitchc 链接:https://www.jianshu.com/p/fede3333205f ...
- Java获取路径的方法分析详解(Application/Web)
1.利用System.getProperty()函数获取当前路径: System.getProperty("user.dir");//user.dir用户当前的工作目录,输出:D: ...
- Android与javaScript的交互
WebView与js的交互包含两方面,一是在html中通过js调用java代码:二是在安卓java代码中调用js. 一.html中通过js调用java代码 js中调用java代码其实就记住一点,Web ...
- 【报错】项目启动,仅仅报错 One or more listeners failed to start. Full details will be found in the appropriate container log file
今天spring4.3.13 项目,整合ActiveMQ的时候,项目启动在自动部署到tomcat下的时候,不能正常的部署,仅仅报错如下: Connected to server [-- ::,] Ar ...
- hdu1874最短路
裸裸的最短路问题,将while(scanf("%d%d", &N, &M)!=EOF)粗心写为while(scanf("%d%d", & ...