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中的测试 简介 本篇文章我们 ...
随机推荐
- shell script 的简单介绍
一 什么叫shell script (程序化脚本)? shell script 是利用 shell 的功能所写的一个 “程序”(program),这个程序是使用纯文本文件,将一些 shell 的语法与 ...
- C# 自动注册OCX方法
C#开发系统时,有时候会遇到调用其他语言开发的模块.如果对方提供了OCX时,就需要注册使用,但是实时时,每个客户端都注册一遍就比较麻烦.所以需要系统第一次启动时自动注册OCX. 一:C#注册OCX ...
- 心跳 CSS
生活中我们所见到的大部分图形(正方形.长方形.圆形.椭圆.三角形.多边形...)都是可以用css3来实现,以及一些复杂点的图形——其实都是由基本图形组合而成的. 由于明天就是情人节了,所以今天我们就用 ...
- mysql 排序字段与索引有关系吗?
mysql 排序字段与索引有关系吗?答案与否需要你explain一下你的sql脚本 另外记住:date_add()方法会影响Index_modify_time索引(即:时间字段索引) 一般遇到这样的 ...
- Accelerating Enum-Based Dictionaries with Generic EnumComparer
原文发布时间为:2011-03-03 -- 来源于本人的百度文章 [由搬家工具导入] 文章:http://www.codeproject.com/KB/cs/EnumComparer.aspx 源码: ...
- .Net Framework 4.0: Using System.Lazy<T>
原文发布时间为:2011-04-26 -- 来源于本人的百度文章 [由搬家工具导入] http://weblogs.asp.net/gunnarpeipman/archive/2009/05/19/n ...
- HashMap和TreeMap的常用排序方法
一.简单描述 Map是键值对的集合接口,它的实现类主要包括:HashMap,TreeMap,HashTable以及LinkedHashMap等. TreeMap:能够把它保存的记录根据键(key)排序 ...
- 玲珑杯 Round #5 Problem E Tetration (枚举 + 欧拉公式)
题目链接 Tetration 题意 给定一个排列 现在可以任意调整这个排列的顺序 求$a_{1}^{a_{2}^{a_{3}^{...^{a_{n}}}}}$对$p$取模的最小值 直接枚举$a$ ...
- oracle中的替换函数replace和translate函数
.translate 语法:TRANSLATE(char, from, to) 用法:返回将出现在from中的每个字符替换为to中的相应字符以后的字符串. 若from比to字符串长,那么在from中比 ...
- Linux(一) 软件安装
前言:在Linux中安装软件时,我们经常要考虑到这样几个个问题: (1).怎样安装软件; (2).软件安装在什么地方; (3).如何卸载删除不要的软件...... 下面,我们就 ...