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; }
原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/15727.html