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中的测试 简介 本篇文章我们 ...
随机推荐
- [暑假集训--数论]poj1365 Prime Land
Everybody in the Prime Land is using a prime base number system. In this system, each positive integ ...
- Java EE 学习(8):IDEA + maven + spring 搭建 web(4)- 用户管理
转载:Gaussic(一个致力于AI研究却不得不兼顾项目的研究生) 注:在阅读本文前,请先阅读: Java EE 学习(5):IDEA + maven + spring 搭建 web(1) ava E ...
- 使用state改变的jsx监听不到数据变化的问题
当使用state来改变一个组件内部的虚拟dom的时候,该虚拟dom是无法监听到state数据的变化的,他只会绑定state改变dom当时的数据.
- 生成静态页面方法 .NET
原文发布时间为:2009-09-30 -- 来源于本人的百度文章 [由搬家工具导入] 采用模板法:【例子中的两个页面以及生成的页面均在同一个目录,自己可以去改】 模板Template.htm: < ...
- food(洛谷P4040 [AHOI2014/JSOI2014]宅男计划)
题目在这里 题目描述 外卖店一共有N种食物,分别有1到N编号.第i种食物有固定的价钱Pi和保质期Si.第i种食物会在Si天后过期.JYY是不会吃过期食物的. 比如JYY如果今天点了一份保质期为1天的食 ...
- JDBC连接池&DBUtils使用
使用连接池改造JDBC的工具类: 1.1.1 需求: 传统JDBC的操作,对连接的对象销毁不是特别好.每次创建和销毁连接都是需要花费时间.可以使用连接池优化的程序. * 在程序开始的 ...
- Java发送邮件----自己封装的方法
发送邮件的封装类: package com.email; import java.util.Properties; import javax.mail.Authenticator; import ja ...
- 点击添加按钮,使用ajax动态添加一行和移除一行,并且序号重新排序和数据不重复操作判断
<div class="control-group " style="top: -20px;position: relative;"> <la ...
- HDU 1033 Edge[地图型模拟/给你一串字符串,A代表以此点为参照顺时针90°,V代表逆时针90°]
Edge Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submis ...
- Topcoder SRM 145 DIV 1
Bonuses 模拟 题意:给你一个序列,按照百分比排序,再将百分比取整,再把剩余百分比加到最大的那几个. 题解:按照题意模拟就好.