User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Spring   ›   正文
Spring

SpringMVC—HandlerAdapter—HttpMessageConverter消息转换器

2022-10-09 23:57:11
1910  0 4
参考目录 隐藏
1) 为何需要消息转换器
2) HttpMessageConverter
3) HttpMessage
4) HttpInputMessage和HttpOutputMessage
5) HttpRequest
6) ReactiveHttpInputMessage和ReactiveHttpOutputMessage
7) FormHttpMessageConverter
8) AbstractHttpMessageConverter
9) SourceHttpMessageConverter
10) ResourceHttpMessageConverter
11) ByteArrayHttpMessageConverter
12) ObjectToStringHttpMessageConverter
13) Json相关转换器
14) GsonHttpMessageConverter
15) ProtobufHttpMessageConverter、ProtobufJsonFormatHttpMessageConverter
16) StringHttpMessageConverter
17) BufferedImageHttpMessageConverter
18) GenericHttpMessageConverter 子接口
19) FastJsonHttpMessageConverter
20) ResourceRegionHttpMessageConverter
21) 自定义消息转换器PropertiesHttpMessageConverter处理Properties类型数据
22) Spring MVC默认注册哪些HttpMessageConverter
23) 细节
24) 匹配选择原理
25) HTTP MediaType的基本知识
26) Spring MVC默认加载的消息转换器有哪些
27) Spring MVC的转换器匹配原理
28) Response返回向body里write时消息转换器的匹配
29) Request请求read请求参数、请求body时消息转换器的匹配(本文重点)
30) RequestResponseBodyMethodProcessor匹配入参的消息转换器
31) 借助RequestBodyAdvice实现对请求参数进行干预
32) 自定义消息转换器HttpMessageConverter
33) FastJsonHttpMessageConverter
34) 使用Spring MVC实现优雅的文件下载
35) ResponseEntity方式对比传统Java方式

阅读完需:约 57 分钟

本文介绍Spring MVC中的一个极其重要的组件:HttpMessageConverter消息转换器。

有一副非常著名的图,来形容Spring MVC对一个请求的处理:

从图中可见HttpMessageConverter对Spring MVC的重要性。它对请求、响应都起到了非常关键的作用

HttpMessageConverter—关于一个字符串到java对象的转化

这是以前关于HttpMessageConverter的笔记,只是一个初探。

为何需要消息转换器

HttpMessageConverter是用来处理request和response里的数据的。.

请求和响应都有对应的body,而这个body就是需要关注的主要数据。

请求体的表述一般就是一段字符串,当然也可以是二进制数据(比如上传)。

响应体则是浏览器渲染页面的依据,对于一个普通html页面得响应,响应体就是这个html页面的源代码。

请求体和响应体都是需要配合Content-Type头部使用的,这个头部主要用于说明body中得字符串是什么格式的,比如:text,json,xml等。

对于请求报文,只有通过此头部,服务器才能知道怎么解析请求体中的字符串,对于响应报文,浏览器通过此头部才知道应该怎么渲染响应结果,是直接打印字符串还是根据代码渲染为一个网页。

对于HttpServletRequest和HttpServletResponse,可以分别调用getInputStream和getOutputStream来直接获取body。但是获取到的仅仅只是一段字符串。

而对于java来说,处理一个对象肯定比处理一个字符串要方便得多,也好理解得多。所以根据Content-Type头部,将body字符串转换为java对象是常有的事。反过来,根据Accept头部,将java对象转换客户端期望格式的字符串也是必不可少的工作。这就是我们本文所讲述的消息转换器的工作。

消息转换器它能屏蔽你对底层转换的实现,分离你的关注点,让你专心操作java对象,其余的事情你就交给我Spring MVC吧~大大提高你的编码效率(可议说比源生Servlet开发高级太多了)

Spring内置了很多HttpMessageConverter,比如MappingJackson2HttpMessageConverter,StringHttpMessageConverter,甚至还有FastJsonHttpMessageConverter(需导包和自己配置)

HttpMessageConverter

在具体讲解之前,先对所有的转换器来个概述:

名称 作用 读支持MediaType 写支持MediaType 备注
FormHttpMessageConverter 表单与MultiValueMap的相互转换 application/x-www-form-urlencoded application/x-www-form-urlencoded和multipart/form-data 可用于处理下载
XmlAwareFormHttpMessageConverter Spring3.2后已过期,使用下面AllEnc…代替 略 略
AllEncompassingFormHttpMessageConverter 对FormHttp…的扩展,提供了对xml和json的支持 略 略
SourceHttpMessageConverter 数据与javax.xml.transform.Source的相互转换 application/xml和text/xml和application/*+xml 同read 和Sax/Dom等有关
ResourceHttpMessageConverter 数据与org.springframework.core.io.Resource */*
*/*
ByteArrayHttpMessageConverter 数据与字节数组的相互转换 */* application/octet-stream
ObjectToStringHttpMessageConverter 内部持有一个StringHttpMessageConverter和ConversionService 他俩的&& 他俩的&&
RssChannelHttpMessageConverter 处理RSS <channel> 元素 application/rss+xml
application/rss+xml
MappingJackson2HttpMessageConverter 使用Jackson的ObjectMapper转换Json数据 application/json和application/*+json application/json和application/*+json 默认编码UTF-8
MappingJackson2XmlHttpMessageConverter 使用Jackson的XmlMapper转换XML数据 application/xml和text/xml application/xml和text/xml 需要额外导包Jackson-dataformat-XML才能生效。从Spring4.1后才有
GsonHttpMessageConverter 使用Gson处理Json数据 application/json application/json 默认编码UTF-8
ResourceRegionHttpMessageConverter 数据和org.springframework.core.io.support.ResourceRegion的转换 application/octet-stream application/octet-stream Spring4.3才提供此类
ProtobufHttpMessageConverter 转换com.google.protobuf.Message数据 application/x-protobuf和text/plain和application/json和application/xml 同read @since 4.1
StringHttpMessageConverter 数据与String类型的相互转换 */* */* 转成字符串的默认编码为ISO-8859-1
BufferedImageHttpMessageConverter 数据与java.awt.image.BufferedImage的相互转换 Java I/O API支持的所有类型 Java I/O API支持的所有类型
FastJsonHttpMessageConverter 使用FastJson处理Json数据 */* */* 需要导入Jar包和自己配置,Spring并不默认内置

Jaxb也是和Sax、Dom、JDOM类似的解析XML的类库,jackson-module-jaxb-annotations对它提供了支持,但是由于关注太少了,所以Jaxb相关的转换器省略

MarshallingHttpMessageConverter也是Spring采用Marshaller/Unmarshaller的方式进行xml的解析,也不关注了FastJsonHttpMessageConverter4和FastJsonpHttpMessageConverter4都继承自FastJsonHttpMessageConverter,现在都已经标记为过期。直接使用FastJsonHttpMessageConverter它即可

需要知道的是:上面说的支持都说的是默认支持,当然你是可以自定义让他们更强大的。比如:我们可以自己配置StringHttpMessageConverter,改变(增强)他的默认行为:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">
            <property name="supportedMediaTypes">
                <list>
                    <value>text/plain;charset=UTF-8</value>
                    <value>text/html;charset=UTF-8</value>
                </list>
             </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

既然它是HttpMessageConverter,所以铁定和HttpMessage有关,因为此接口涉及的内容相对来说比较偏底层,因此本文只在接口层面做简要的一个说明。

HttpMessage

它是Spring 3.0后增加一个非常抽象的接口。表示:表示HTTP请求和响应消息的基本接口

public interface HttpMessage {
	// Return the headers of this message
	HttpHeaders getHeaders();
}

看看它的继承树:

HttpInputMessage和HttpOutputMessage

这就是目前都在使用的接口,表示输入、输出信息

public interface HttpInputMessage extends HttpMessage {
	InputStream getBody() throws IOException;
}
public interface HttpOutputMessage extends HttpMessage {
	OutputStream getBody() throws IOException;
}

HttpRequest

代表着一个Http请求信息,提供了多的几个API,是对HttpMessage的一个补充。Spring3.1新增的

public interface HttpRequest extends HttpMessage {
	@Nullable
	default HttpMethod getMethod() {
		// 可议根据String类型的值  返回一个枚举
		return HttpMethod.resolve(getMethodValue());
	}
	String getMethodValue();
	// 可以从请求消息里  拿到URL
	URI getURI();
}

ReactiveHttpInputMessage和ReactiveHttpOutputMessage

显然,是Spring5.0新增的接口,也是Spring5.0最重磅的升级之一。自此Spring容器就不用强依赖于Servlet容器了。它还可以选择依赖于reactor这个框架。

比如这个类:reactor.core.publisher.Mono就是Reactive的核心类之一

因为属于Spring5.0的最重要的新特性之一


HttpMessageConverter接口是Spring3.0之后新增的一个接口,它负责将请求信息转换为一个对象(类型为T),并将对象(类型为T)绑定到请求方法的参数中或输出为响应信息

// @since 3.0  Spring3.0后推出的   是个泛型接口
// 策略接口,指定可以从HTTP请求和响应转换为HTTP请求和响应的转换器
public interface HttpMessageConverter<T> {

	// 指定转换器可以读取的对象类型,即转换器可将请求信息转换为clazz类型的对象
	// 同时支持指定的MIME类型(text/html、application/json等)
	boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
	// 指定转换器可以将clazz类型的对象写到响应流当中,响应流支持的媒体类型在mediaType中定义
	boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
	// 返回当前转换器支持的媒体类型~~
	List<MediaType> getSupportedMediaTypes();

	// 将请求信息转换为T类型的对象 流对象为:HttpInputMessage
	T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
	// 将T类型的对象写到响应流当中,同事指定响应的媒体类型为contentType 输出流为:HttpOutputMessage 
	void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}

看看它的继承树:

它的继承树,用品牌繁多来形容真的非常贴切。

按照层级划分,它的直接子类是如下四个:

FormHttpMessageConverter、AbstractHttpMessageConverter、BufferedImageHttpMessageConverter、GenericHttpMessageConverter(Spring3.2出来的,支持到了泛型)


FormHttpMessageConverter

form表单提交/文件下载

从名字知道,它和Form表单有关。浏览器原生表单默认的提交数据的方式(就是没有设置enctype属性),它默认是这个:Content-Type: application/x-www-form-urlencoded;charset=utf-8

从请求和响应读取/编写表单数据。默认情况下,它读取媒体类型 application/x-www-form-urlencoded 并将数据写入 MultiValueMap。因为它独立的存在,所以可以看看源码内容:

// @since 3.0
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {

	// 默认UTF-8编码  MediaType为:application/x-www-form-urlencoded
	public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
	private static final MediaType DEFAULT_FORM_DATA_MEDIA_TYPE = new MediaType(MediaType.APPLICATION_FORM_URLENCODED, DEFAULT_CHARSET);
	
	// 缓存下它所支持的MediaType们
	private List<MediaType> supportedMediaTypes = new ArrayList<>();
	// 用于二进制内容的消息转换器们~~~ 毕竟此转换器还支持`multipart/form-data`这种  可以进行文件下载~~~~~
	private List<HttpMessageConverter<?>> partConverters = new ArrayList<>();

	private Charset charset = DEFAULT_CHARSET;
	@Nullable
	private Charset multipartCharset;

	// 唯一的一个构造函数~
	public FormHttpMessageConverter() {
		// 默认支持处理两种MediaType:application/x-www-form-urlencoded和multipart/form-data
		this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
		this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);

		StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
		stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316

		// === 它自己不仅是个转换器,还内置了这三个转换器 至于他们具体处理那种消息,请看下面 都有详细说明 ==
		// 注意:这些消息转换器都是去支持part的,支持文件下载
		this.partConverters.add(new ByteArrayHttpMessageConverter());
		this.partConverters.add(stringHttpMessageConverter);
		this.partConverters.add(new ResourceHttpMessageConverter());

		// 这是为partConverters设置默认的编码~~~
		applyDefaultCharset();
	}
	// 省略属性额get/set方法
	
	// 从这可以发现,只有Handler的入参类型是是MultiValueMap它才会去处理~~~~
	@Override
	public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
		if (!MultiValueMap.class.isAssignableFrom(clazz)) {
			return false;
		}
		// 若没指定MedieType  会认为是可读的~
		if (mediaType == null) {
			return true;
		}
		// 显然,只有我们Supported的MediaType才会是true(当然multipart/form-data例外,此处是不可读的)
		for (MediaType supportedMediaType : getSupportedMediaTypes()) {
			// We can't read multipart....
			if (!supportedMediaType.equals(MediaType.MULTIPART_FORM_DATA) && supportedMediaType.includes(mediaType)) {
				return true;
			}
		}
		return false;
	}

	// 注意和canRead的区别,有点对着干的意思~~~
	@Override
	public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
		if (!MultiValueMap.class.isAssignableFrom(clazz)) {
			return false;
		}
		// 如果是ALL 说明支持所有的类型  那就恒返回true  当然null也是的
		if (mediaType == null || MediaType.ALL.equals(mediaType)) {
			return true;
		}
		for (MediaType supportedMediaType : getSupportedMediaTypes()) {
			// isCompatibleWith是否是兼容的
			if (supportedMediaType.isCompatibleWith(mediaType)) {
				return true;
			}
		}
		return false;
	}

	// 把输入信息读进来,成为一个 MultiValueMap<String, String>
	// 注意:此处发现class这个变量并没有使用~
	@Override
	public MultiValueMap<String, String> read(@Nullable Class<? extends MultiValueMap<String, ?>> clazz,
			HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {

		// 拿到请求的ContentType请求头~~~~
		MediaType contentType = inputMessage.getHeaders().getContentType();
		// 这里面 编码放在contentType里面  若没有指定  走默认的编码
		// 类似这种形式就是我们自己指定了编码:application/json;charset=UTF-8
		Charset charset = (contentType != null && contentType.getCharset() != null ? contentType.getCharset() : this.charset);
		
		// 把body的内容读成字符串~
		String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
		// 用"&"分隔   因为此处body一般都是hello=world&fang=shi这样传进来的
		String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
		MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);

		// 这个就不说了,就是把键值对保存在map里面。注意:此处为何用多值Map呢?因为一个key可能是会有多个value的
		for (String pair : pairs) {
			int idx = pair.indexOf('=');
			if (idx == -1) {
				result.add(URLDecoder.decode(pair, charset.name()), null);
			}
			else {
				String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
				String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
				result.add(name, value);
			}
		}
		return result;
	}

}

AbstractHttpMessageConverter

一个基础抽象实现,它也还是个泛型类。对于泛型的控制,有如下特点:

  • 最广的可以选择Object,不过Object并不都是可以序列化的,但是子类可以在覆盖的supports方法中进一步控制,因此选择Object是可以的
  • 最符合的是Serializable,既完美满足泛型定义,本身也是个Java序列化/反序列化的充要条件
  • 自定义的基类Bean,有些技术规范要求自己代码中的所有bean都继承自同一个自定义的基类BaseBean,这样可以在Serializable的基础上再进一步控制,满足自己的业务要求

若我们自己需要自定义一个消息转换器,大多数情况下也是继承抽象类再具体实现。

比如我们最熟悉的:FastJsonHttpMessageConverter它就是一个子类实现

public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> {

	// 它主要内部维护了这两个属性,可议构造器赋值,也可以set方法赋值~~
	private List<MediaType> supportedMediaTypes = Collections.emptyList();
	@Nullable
	private Charset defaultCharset;

	// supports是个抽象方法,交给子类自己去决定自己支持的转换类型~~~~
	// 而canRead(mediaType)表示MediaType也得在我支持的范畴了才行(入参MediaType若没有指定,就返回true的)
	@Override
	public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
		return supports(clazz) && canRead(mediaType);
	}

	// 原理基本同上,supports和上面是同一个抽象方法  所以我们发现并不能入参处理Map,出餐处理List等等
	@Override
	public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
		return supports(clazz) && canWrite(mediaType);
	}


	// 这是Spring的惯用套路:readInternal  虽然什么都没做,但我觉得还是挺有意义的。Spring后期也非常的好扩展了~~~~
	@Override
	public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException {
		return readInternal(clazz, inputMessage);
	}


	// 整体上就write方法做了一些事~~
	@Override
	public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {

		final HttpHeaders headers = outputMessage.getHeaders();
		// 设置一个headers.setContentType 和 headers.setContentLength
		addDefaultHeaders(headers, t, contentType);

		if (outputMessage instanceof StreamingHttpOutputMessage) {
			StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
			// StreamingHttpOutputMessage增加的setBody()方法,关于它下面会给一个使用案例~~~~
			streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {
				// 注意此处复写:返回的是outputStream ,它也是靠我们的writeInternal对它进行写入的~~~~
				@Override
				public OutputStream getBody() {
					return outputStream;
				}
				@Override
				public HttpHeaders getHeaders() {
					return headers;
				}
			}));
		}
		// 最后它执行了flush,这也就是为何我们自己一般不需要flush的原因
		else {
			writeInternal(t, outputMessage);
			outputMessage.getBody().flush();
		}
	}
	
	// 三个抽象方法
	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;
			
}

关于StreamingHttpOutputMessage的使用:

表示允许设置流正文的HTTP输出消息,需要注意的是,此类消息通常不支持getBody()访问

// @since 4.0
public interface StreamingHttpOutputMessage extends HttpOutputMessage {

	// 设置一个流的正文,提供回调
	void setBody(Body body);
	
	// 定义可直接写入@link outputstream的主体的协定。
	// 通过回调机制间接的访问HttpClient库很有作用
	@FunctionalInterface
	interface Body {
		// 把当前的这个body写进给定的OutputStream
		void writeTo(OutputStream outputStream) throws IOException;
	}

}

SourceHttpMessageConverter

处理一些和xml相关的资源,比如DOMSource、SAXSource、SAXSource等等

ResourceHttpMessageConverter

负责读取资源文件和写出资源文件数据

它来处理把Resource进行写出去。当然它也可以把body的内容写进到Resource里来。

public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<Resource> {
	// 是否支持读取流信息
	private final boolean supportsReadStreaming;
	// 默认支持所有的MediaType~~~~~   但是它有个类型匹配,所以值匹配入参/返回类型是Resource类型的
	public ResourceHttpMessageConverter() {
		super(MediaType.ALL);
		this.supportsReadStreaming = true;
	}
	@Override
	protected boolean supports(Class<?> clazz) {
		return Resource.class.isAssignableFrom(clazz);
	}

	// 直观感受:读的时候也只支持InputStreamResource和ByteArrayResource这两种resource的直接封装
	@Override
	protected Resource readInternal(Class<? extends Resource> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException {

		if (this.supportsReadStreaming && InputStreamResource.class == clazz) {
			return new InputStreamResource(inputMessage.getBody()) {
				@Override
				public String getFilename() {
					return inputMessage.getHeaders().getContentDisposition().getFilename();
				}
			};
		}
		// 若入参类型是Resource接口,也是当作ByteArrayResource处理的
		else if (Resource.class == clazz || ByteArrayResource.class.isAssignableFrom(clazz)) {
			// 把inputSteeam转换为byte[]数组~~~~~~
			byte[] body = StreamUtils.copyToByteArray(inputMessage.getBody());
			return new ByteArrayResource(body) {
				@Override
				@Nullable
				public String getFilename() {
					return inputMessage.getHeaders().getContentDisposition().getFilename();
				}
			};
		}
		else {
			throw new HttpMessageNotReadableException("Unsupported resource class: " + clazz, inputMessage);
		}
	}

	@Override
	protected void writeInternal(Resource resource, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {
		writeContent(resource, outputMessage);
	}
	
	// 写也非常的简单,就是把resource这个资源的内容写到body里面去,此处使用的StreamUtils.copy这个工具方法,专门处理流
	// 看到此处我们自己并不需要flush,但是需要自己关闭流
	protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {
		try {
			InputStream in = resource.getInputStream();
			try {
				StreamUtils.copy(in, outputMessage.getBody());
			}
			catch (NullPointerException ex) {
				// ignore, see SPR-13620
			}
			finally {
				try {
					in.close();
				}
				catch (Throwable ex) {
					// ignore, see SPR-12999
				}
			}
		}
		catch (FileNotFoundException ex) {
			// ignore, see SPR-12999
		}
	}
}

使用它模拟完成上传功能:

上传表单如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试FormHttpMessageConverter</title>
</head>
<body>
<!-- 表单的enctype一定要标注成multipart形式,否则是拿不到二进制流的 -->
<form action="http://localhost:8080/demo_war_war/upload" method="post" enctype="multipart/form-data">
    用户名 <input type="text" name="userName">
    头像 <input type="file" name="touxiang">
    <input type="submit">
</form>
</body>
</html>
    // 模拟使用Resource进行文件的上传~~~
    @ResponseBody
    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    public String upload(@RequestBody Resource resource) { //此处不能用接口Resource resource
        dumpStream(resource);
        return "success";
    }

    // 模拟写文件的操作(此处写到控制台)
    private static void dumpStream(Resource resource) {
        InputStream is = null;
        try {
            //1.获取文件资源
            is = resource.getInputStream();
            //2.读取资源
            byte[] descBytes = new byte[is.available()];
            is.read(descBytes);
            System.out.println(new String(descBytes, StandardCharsets.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                //3.关闭资源
                is.close();
            } catch (IOException e) {
            }
        }
    }

控制台结果

由此可见利用它是可以把客户端的资源信息都拿到的,从而间接的实现文件的上传的功能。

ByteArrayHttpMessageConverter

和上面类似

ObjectToStringHttpMessageConverter

它是对StringHttpMessageConverter的一个扩展。它在Spring内部并没有装配进去。若我们需要,可以自己装配到Spring MVC里面去

public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
	
	// 我们只需要自定定义这个转换器   让它实现String到Obj之间的互相转换~~~
	private final ConversionService conversionService;
	private final StringHttpMessageConverter stringHttpMessageConverter;
	... // 下面省略
	// 读的时候先用stringHttpMessageConverter读成String,再用转换器转为Object对象
	// 写的时候先用转换器转成String,再用stringHttpMessageConverter写进返回的body里
}
Json相关转换器

可以看到一个是谷歌阵营,一个是jackson阵营。

GsonHttpMessageConverter

利用谷歌的Gson进行json序列化的处理

// @since 4.1  课件它被Spring选中的时间还是比较晚的
public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter {
	private Gson gson;
	public GsonHttpMessageConverter() {
		this.gson = new Gson();
	}
	// @since 5.0  调用者可以自己指定一个Gson对象了
	public GsonHttpMessageConverter(Gson gson) {
		Assert.notNull(gson, "A Gson instance is required");
		this.gson = gson;
	}	

	// 因为肯定是文本,所以这里使用Reader 没有啥问题
	// 父类默认用UTF-8把inputStream转为了更友好的Reader
	@Override
	protected Object readInternal(Type resolvedType, Reader reader) throws Exception {
		return getGson().fromJson(reader, resolvedType);
	}

	@Override
	protected void writeInternal(Object o, @Nullable Type type, Writer writer) throws Exception {
		// 如果带泛型  这里也是特别的处理了兼容处理~~~~
		if (type instanceof ParameterizedType) {
			getGson().toJson(o, type, writer);
		} else {
			getGson().toJson(o, writer);
		}
	}

	// 父类定义了它支持的MediaType类型~
	public AbstractJsonHttpMessageConverter() {
		super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
		setDefaultCharset(DEFAULT_CHARSET);
	}
}

总体上看,jackson的实现是最为完善的

备注:Gson和Jackson转换器他俩都是支持jsonPrefix我们可以自定义Json前缀的

若你的返回值是Map、List等,只要MediaType对上了,这种json处理器都是可以处理的。因为他们泛型上都是Object表示入参、 返回值任意类型都可以处理

ProtobufHttpMessageConverter、ProtobufJsonFormatHttpMessageConverter

不常用,略

StringHttpMessageConverter

这个是使用得非常广泛的一个消息转换器,专门处理入参/出参字符串类型。

// @since 3.0  出生非常早
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
	// 这就是为何你return中文的时候会乱码的原因(若你不设置它的编码的话~)
	public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;
	@Nullable
	private volatile List<Charset> availableCharsets;
	// 标识是否输出 Response Headers:Accept-Charset(默认true表示输出)
	private boolean writeAcceptCharset = true;

	public StringHttpMessageConverter() {
		this(DEFAULT_CHARSET);
	}
	public StringHttpMessageConverter(Charset defaultCharset) {
		super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);
	}

	//Indicates whether the {@code Accept-Charset} should be written to any outgoing request.
	// Default is {@code true}.
	public void setWriteAcceptCharset(boolean writeAcceptCharset) {
		this.writeAcceptCharset = writeAcceptCharset;
	}

	// 只处理String类型~
	@Override
	public boolean supports(Class<?> clazz) {
		return String.class == clazz;
	}

	@Override
	protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
		// 哪编码的原则为:
		// 1、contentType自己指定了编码就以指定的为准
		// 2、没指定,但是类型是`application/json`,统一按照UTF_8处理
		// 3、否则使用默认编码:getDefaultCharset  ISO_8859_1
		Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
		// 按照此编码,转换为字符串~~~
		return StreamUtils.copyToString(inputMessage.getBody(), charset);
	}


	// 显然,ContentLength和编码也是有关的~~~
	@Override
	protected Long getContentLength(String str, @Nullable MediaType contentType) {
		Charset charset = getContentTypeCharset(contentType);
		return (long) str.getBytes(charset).length;
	}


	@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());
	}
	...
}

我们有可以这么来写,达到我们一定的目的:

	// 因为它支持MediaType.TEXT_PLAIN, MediaType.ALL所有类型,所以你的contentType无所谓~~~ 它都能够处理
    @ResponseBody
    @RequestMapping(value = "/test", method = RequestMethod.POST)
    public String upload(@RequestBody String body) {
        return "Hello World";
    }

这种书写方式它不管是入参,还是返回值处理的转换器,都是用到的StringHttpMessageConverter。用它来接收入参和上面例子Resource有点像,只是StringHttpMessageConverter它只能解析文本内容,而Resource可以处理所有。

需要注意的是:若你的项目中大量使用到了此转换器,请一定要注意编码问题。一般不建议直接使用StringHttpMessageConverter,而是我们配置好编码(UTF-8)后,再把它加入到Spring MVC里面,这样就不会有乱码问题了

另外我们或许看到过有的小伙伴竟这么来写:为了给前端返回一个json串

    @ResponseBody
    @RequestMapping(value = "/test")
    public String test() {
        return "{\"status\":0,\"errmsg\":null,\"data\":{\"query\":\"酒店查询\",\"num\":65544,\"url\":\"www.test.com\"}}";
    }

虽然这么做结果是没有问题的,但是非常非常的不优雅,属于低级的行为。
通过自己构造Json串的形式(虽然你可能直接借助Fastjson去转,但也很低级),现在看来这么做是低级的、愚蠢的,千万别这么去做

BufferedImageHttpMessageConverter

处理java.awt.image.BufferedImage,和awt相关

GenericHttpMessageConverter 子接口

GenericHttpMessageConverter接口继承自HttpMessageConverter接口,二者都是在org.springframework.http.converter包下。它的特点就是:它处理目标类型为泛型类型的类型

public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T> {
	//This method should perform the same checks than {@link HttpMessageConverter#canRead(Class, MediaType)} with additional ones related to the generic type.
	// 它的效果同父接口的canRead,但是它是加了一个泛型类型~~~来加以更加详细的判断
	boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType);
	// 一样也是加了泛型类型
	T read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException;

	//@since 4.2
	boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType);
	// @since 4.2
	void write(T t, @Nullable Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}

可以看出处理Json方面的转换器,都实现了此接口。此处主要以阿里巴巴的FastJson转换器为例加以说明:

FastJsonHttpMessageConverter

它和Gson和fastjson类似,只不过它内部引擎用的是Ali的FastJson库

// Fastjson for Spring MVC Converter. Compatible Spring MVC version 3.2+
// @since 1.2.10
public class FastJsonHttpMessageConverter extends AbstractHttpMessageConverter<Object>
        implements GenericHttpMessageConverter<Object> {

    public FastJsonHttpMessageConverter() {
        super(MediaType.ALL);
    }

	// 永远返回true,表示它想支持所有的类型,所有的MediaType,现在这算一个小Bug
    @Override
    protected boolean supports(Class<?> clazz) {
        return true;
    }

	// 它竟然对泛型Type都没有任何的实现,这也是一个小bug
	// 包括读写的时候  对泛型类型都没有做很好的处理~~~
    public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
        return super.canRead(contextClass, mediaType);
    }
    public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
        return super.canWrite(clazz, mediaType);
    }

	// 这是处理读的方法,主要依赖于JSON.parseObject这个方法解析成一个object
    private Object readType(Type type, HttpInputMessage inputMessage) {

        try {
            InputStream in = inputMessage.getBody();
            return JSON.parseObject(in,
                    fastJsonConfig.getCharset(),
                    type,
                    fastJsonConfig.getParserConfig(),
                    fastJsonConfig.getParseProcess(),
                    JSON.DEFAULT_PARSER_FEATURE,
                    fastJsonConfig.getFeatures());
        } catch (JSONException ex) {
            throw new HttpMessageNotReadableException("JSON parse error: " + ex.getMessage(), ex);
        } catch (IOException ex) {
            throw new HttpMessageNotReadableException("I/O error while reading input message", ex);
        }
    }
}

总体来说,如果你是FastJson的死忠粉,你可以替换掉默认的Jackson的实现方式。但是由于FastJson在效率在对标Jackson并没有多少优势,所以绝大多数情况下,我并不建议修改Spring MVC处理json的默认行为

ResourceRegionHttpMessageConverter

和org.springframework.core.io.support.ResourceRegion有关,它只能写为一个ResourceRegion或者一个它的List

只能写不能读,读方法都会抛异常

// 这个类很简单,就是对Resource的一个包装  所以它和`application/octet-stream`也是有关的
// @since 4.3
public class ResourceRegion {
	private final Resource resource;
	private final long position;
	private final long count;
	...
}

若你报错说ResourceRegionHttpMessageConverter类找不到,请检查你的Spring版本。因此此类@since 4.3

自定义消息转换器PropertiesHttpMessageConverter处理Properties类型数据

自定义的主要目的是加深对消息转换器的理解。此处我们仍然是通过继承AbstractHttpMessageConverter方式来扩展:

public class PropertiesHttpMessageConverter extends AbstractHttpMessageConverter<User> {


    // 用于仅仅只处理我自己自定义的指定的MediaType
    private static final MediaType DEFAULT_MEDIATYPE = MediaType.valueOf("application/properties");


    public PropertiesHttpMessageConverter() {
        super(DEFAULT_MEDIATYPE);
        setDefaultCharset(StandardCharsets.UTF_8);
    }

    // 要求入参、返回值必须是User类型我才处理
    @Override
    protected boolean supports(Class<?> clazz) {
        return clazz.isAssignableFrom(User.class);
    }

    @Override
    protected User readInternal(Class<? extends User> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        InputStream is = inputMessage.getBody();
        Properties props = new Properties();
        props.load(is);

        // user的三个属性
        String id = props.getProperty("id");
        String name = props.getProperty("name");
        String age = props.getProperty("age");
        return new User(Integer.valueOf(id), name, Integer.valueOf(age));
    }

    @Override
    protected void writeInternal(User user, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        OutputStream os = outputMessage.getBody();
        Properties properties = new Properties();

        // 属性判空此处我就不做了~~~
        properties.setProperty("id", user.getId().toString());
        properties.setProperty("name", user.getName());
        properties.setProperty("age", user.getAge().toString());
        properties.store(os, "user comments");
    }
}

其实发现,处理代码并不多。需要注意的是:此处我们只处理我们自定义的:application/properties-user这一种MediaType即可,职责范围放到最小。

接下来就是要注册进Spring MVC里:

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 因为此转换器职责已经足够单一,所以放在首位是木有问题的~
        converters.add(0, new PropertiesHttpMessageConverter());
        // 若放在末尾,将可能不会生效~~~~(比如如果Fastjson转换器 处理所有的类型的话,所以放在首位最为保险)
        //converters.add(0, new PropertiesHttpMessageConverter());
    }
}

这里需要注意的是,为了避免意外,一定要注意自定义消息转换器的注册顺序问题。

编写Handler处理器如下:

    @ResponseBody
    @RequestMapping(value = "/test/properties", method = RequestMethod.POST)
    public User upload(@RequestBody User user) {
        System.out.println(user);
        return user;
    }

下面可以用postman模拟访问了,就能看到如下效果

我们自定义的消息处理器,只处理我们我们指定的MediaType、指定的Class类型,可以帮助我们实现某些个性化逻辑

Spring MVC默认注册哪些HttpMessageConverter

此处情况完全以Spring MVC版本讲解,和Spring Boot无关

Spring 版本号为:5.1.6.RELEASE

不开启该注解:@EnableWebMvc

开启该注解:@EnableWebMvc

可以看到@EnableWebMvc注解的“威力”还是蛮大的,一下子让Spring MVC变强不少,所以一般情况下,我是建议开启它的。

当然如果是在Spring Boot环境下使用Spring MVC,到时候会再具体问题具体分析

在纯Spring环境下,我是无理由建议标注@EnableWebMvc上此注解的
而且从上面可以看出,若我们classpath下有Jackson的包,那装配的就是MappingJackson2HttpMessageConverter,若没有jackson包有gson包,那装配的就是gson转换器。

细节

  • 如果一个Controller类里面所有方法的返回值都需要经过消息转换器,那么可以在类上面加上@ResponseBody注解或者将@Controller注解修改为@RestController注解,这样做就相当于在每个方法都加上了@ResponseBody注解了(言外之意别的方式都是不会经历消息转换器的)
  • @ResponseBody和@RequestBody都可以处理Map类型的对象。如果不确定参数的具体字段,可以用Map接收。@ReqeustBody同样适用。(List也是没有问题的)
  • 方法上的和类上的@ResponseBody都可以被继承
  • 默认的xml转换器Jaxb2RootElementHttpMessageConverter需要类上有@XmlRootElement注解才能被转换(虽然很少使用但此处还是指出)
    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return (AnnotationUtils.findAnnotation(clazz, XmlRootElement.class) != null && canWrite(mediaType));
    }
  • 返回值类型可声明为基类的类型,不影响转换(比如我们返回值是Object都是木有关系的)。但参数的类型必需为特定的类型(最好不要用接口类型,当然有的时候也是可以的比如Map/List/Resource等等)。这是显而易见的

请求和响应都有对应的body,而这个body就是需要关注的主要数据。

请求体与请求的查询参数或者表单参数是不同的:

  • 请求体的表述一般就是一段字符串(当然也可能是二进制),而查询参数可以看作url的一部分,这两个是位于请求报文的不同地方
  • 表单参数可以按照一定格式放在请求体中,也可以放在url上作为查询参数。

响应体则是浏览器渲染页面的依据,对于一个普通html页面得响应,响应体就是这个html页面的源代码。

请求体和响应体都是需要配合Content-Type头部使用的,这个头部主要用于说明body中得字符串是什么格式的,比如:text,json,xml等。

  • 对于请求报文,只有通过此头部,服务器才能知道怎么解析请求体中的字符串
  • 对于响应报文,浏览器通过此头部才知道应该怎么渲染响应结果,是直接打印字符串还是根据代码渲染为一个网页

还有一个与body有关的头部是Accept,这个头部标识了客户端期望得到什么格式的响应体。服务器可根据此字段选择合适的结果表述。

对于HttpServletRequest和HttpServletResponse,可以分别调用getInputStream和getOutputStream来直接获取body,但是获取到的仅仅只是一段字符串。

而对于Java来说,处理一个对象肯定比处理一个字符串要方便得多,也好理解得多。

所以根据Content-Type头部,将body字符串转换为java对象是常有的事。反过来,根据Accept头部,将java对象转换客户端期望格式的字符串也是必不可少的工作。

消息转换器HttpMessageConverter就是专门来实现请求体/响应体到Java对象之间的转换的,具有非常重要的意义


匹配选择原理

介绍Spring MVC中消息转换器的关键作用,并且也知道Spring MVC其实是内置了非常非常多的转换器来处理各种各样的MediaType。绝大多数情况下我们并不需要自己去定义转换器,全都交给Spring MVC去处理就够了

但是Spring MVC既然帮我们内置了这么多的转换器,它默认都给我们加载进去了哪些了?若不是全部都加载进去,那我们遇到特殊的需求怎么自己往里放呢?

另外,我们一个请求request进来,Spring MVC到底是运用了怎么样的匹配规则,匹配到一个最适合的转换器进行消息转换的呢?

HTTP MediaType的基本知识

第一点

从上图可以看出Response的Content-Type为text/html,但是我们需要明白的是:决定Response的Content-Type的第一要素是Request请求头中的Accept属性的值,它也被称为MediaType。

这个Accept的值传给服务端,如果服务端支持这种MediaType,那么服务端就按照这个MediaType来返回对应的格式给Response,同时会把返回的的Content-Type设置成对应格式的MediaType

若服务端明确不支持请求头中Accept指定的任何值时,那么就应该返回Http状态码:406 Not Acceptable

比如上面截图例子:请求头中Accept支持多种MediaType,服务端最终返回的Content-Type为text/html显然是没有问题的

第二点

如果Accept指定了多个MediaType,并且服务端也支持多个MediaType,那么Accept应该同时指定各个MediaType的QualityValue(也就是如图中的q值),服务端根据q值的大小来决定这几个MediaType类型的优先级,一般是大的优先。q值不指定时,默认视为q=1.

上图的Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3为Chrome浏览器的默认请求头的值。

它的含义为:服务端在支持的情况下应该优先返回text/html,其次是application/xhtml+xml。前面几个都不支持时,服务器可以自行处理 /,返回一种服务器自己支持的格式。

第三点

一个HTTP请求没有指定Accept,默认视为指定 Accept: /;请求头里没有指定Content-Type,默认视为 null,就是没有。

第四点

Content-Type若指定了,必须是具体确定的类型,不能包含 *.

注意:上面属于Http规范的范畴,Spring MVC基本遵循上面这几点

Spring MVC默认加载的消息转换器有哪些

为了更好的理解Spring MVC对消息转换器的匹配规则,先弄清楚Spring MVC默认给我们加载了哪些HttpMessageConverter呢

首先我们从现象上直观的看一下:

(因为消息转换器都放在了RequestMappingHandlerAdapter里,所以我们只需要关注运行时它里面的这个属性值即可)

开启了@EnableWebMvc: 一共会有8个,只要我们classpath下有jackson的包,就会加载它进来。

理由如下:看代码吧(因为开启了@EnableWebMvc,所以看WebMvcConfigurationSupport它):

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
	...
	protected final List<HttpMessageConverter<?>> getMessageConverters() {
		if (this.messageConverters == null) {
			this.messageConverters = new ArrayList<>();
			// 调用者自己配置消息转换器
			// 若调用者自己没有配置,那就走系统默认的转换器们~~~~~
			configureMessageConverters(this.messageConverters);
			if (this.messageConverters.isEmpty()) {
				addDefaultHttpMessageConverters(this.messageConverters);
			}
	
			// 不管调用者配不配置,通过扩展接口进来的转换器都会添加进来
			// 因为复写此个protected方法也是我们最为常用的自定义消息转换器的一个手段~~~~~
			extendMessageConverters(this.messageConverters);
		}
		return this.messageConverters;
	}
	...
		
	// 大多数情况下,我们并不需要配置。因此看看系统默认的addDefaultHttpMessageConverters(this.messageConverters);
	protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
		StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
		stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316

		messageConverters.add(new ByteArrayHttpMessageConverter());
		messageConverters.add(stringHttpMessageConverter);
		messageConverters.add(new ResourceHttpMessageConverter());
		messageConverters.add(new ResourceRegionHttpMessageConverter());


		try {
			messageConverters.add(new SourceHttpMessageConverter<>());
		}
		catch (Throwable ex) {
			// Ignore when no TransformerFactory implementation is available...
		}
		messageConverters.add(new AllEncompassingFormHttpMessageConverter());

		if (romePresent) {
			messageConverters.add(new AtomFeedHttpMessageConverter());
			messageConverters.add(new RssChannelHttpMessageConverter());
		}

		if (jackson2XmlPresent) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
		}
		else if (jaxb2Present) {
			messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
		}

		if (jackson2Present) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
		}
		else if (gsonPresent) {
			messageConverters.add(new GsonHttpMessageConverter());
		}
		else if (jsonbPresent) {
			messageConverters.add(new JsonbHttpMessageConverter());
		}

		if (jackson2SmilePresent) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
		}
		if (jackson2CborPresent) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
		}
	}
}

这个逻辑走下来,最终能被添加进去就是我们截图的那8个(当然这里指的我们只导入jackson处理json的这个jar的情况下)

说明一点:jackson2SmilePresent用于处理application/x-jackson-smile,代表类为:com.fasterxml.jackson.dataformat.smile.SmileFactory
jackson2CborPresent用于处理application/cbor,代表类为com.fasterxml.jackson.dataformat.cbor.CBORFactory
(Smile和CBOR就是一种数据格式,只是jackson强大的都给与了支持)当下绝大多数情况下我们只需要处理Json数据,所以只需要导入如下一个包即可:

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.8</version>
        </dependency>

非常不建议导入jackson-all做这种全量导入,臃肿

Smile是二进制的JSON数据格式,等同于标准的JSON数据格式。Smile格式于2010年发布,于2010年9月Jackson 1.6版已开始支持

没有开启@EnableWebMvc: ,情况就不一样了:

我们发现仅仅只有4个,并且它并没有处理返回为Json的数据转换器。因此假如我们有如下两个Handler:

	// 返回值为string类型
    @ResponseBody
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String helloGet() throws Exception {
    	// 请注意:我这里又有中文  又有英文
        return "哈喽,world";
    }
	
	// 返回值是个对象,希望被转换为
    @ResponseBody
    @RequestMapping(value = "/hello/json", method = RequestMethod.GET)
    public Parent helloGetJson() throws Exception {
        return new Parent("fsx", 18);
    }

浏览器显示为:???world,可以看见String类型能够正常处理,但是若出现中文需要注意处理。

再看第二个请求:

浏览器会显示报错:

它原理就是初始化RequestMappingHandlerAdapter构造函数里默认加入的那4个:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
		implements BeanFactoryAware, InitializingBean {
	
	...
	public RequestMappingHandlerAdapter() {
		StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
		stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316

		this.messageConverters = new ArrayList<>(4);
		this.messageConverters.add(new ByteArrayHttpMessageConverter());
		this.messageConverters.add(stringHttpMessageConverter);
		try {
			this.messageConverters.add(new SourceHttpMessageConverter<>());
		}
		catch (Error err) {
			// Ignore when no TransformerFactory implementation is available
		}
		this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
	}
	...
}

由此可见,当我们使用Spring MVC的时候,强烈建议开启注解:@EnableWebMvc,否则功能是比较弱的。

Spring MVC的转换器匹配原理

涉及到转换器的匹配,其实就有对read的匹配和write的匹配。
因为上面我们已经主要接触到了写的过程(比如String、json转换到body里),所以此处我们下跟踪看看向body里write内容的时候是怎么匹配的。

Response返回向body里write时消息转换器的匹配

此处先以请求:http://localhost:8080/demo_war_war/hello为例

我们知道请求交给DispatcherServlet#doDispatch方法,最终会匹配到一个HandlerAdapter然后调用其ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)方法真正处理请求,然后最终都是返回一个ModelAndView

因为此处处理的是write过程,所以处理的是返回值。所以最终处理的是:RequestResponseBodyMethodProcessor#handleReturnValue():

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
	...
	@Override
	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
			throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

		mavContainer.setRequestHandled(true);
		ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
		ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

		// Try even with null return value. ResponseBodyAdvice could get involved.
		// 这里找到消息转换器,来把返回的结果写进response里面~~~
		// 该方法位于父类`AbstractMessageConverterMethodArgumentResolver`中,通用的利用转换器处理返回值的方法
		writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
	}
	...


}

关于返回值的匹配原理

SpringMVC—HandlerMethodReturnValueHandler返回值处理器

Request请求read请求参数、请求body时消息转换器的匹配(本文重点)

相应的,处理请求@RequestBody的处理器选择,也发生在RequestResponseBodyMethodProcessor里

此处以这个处理器为例进行讲解:

    @ResponseBody
    @RequestMapping(value = "/hello/post", method = RequestMethod.POST)
    public Person hello(@RequestBody Person person) {
        return person;
    }

若我们用postman发如下请求:

服务端日志也能收到如下的警告信息

收到的竟然是一个报错,what竟然不支持我的类型???

这就是为什么我要解释上面的Http基础原理的原因了。

如图其实根本原因是Postman给我们发送请求的时候,默认给我们发送了一个content-type,有点自作主张了,所以导致的这问题。(Chome是不会这样自作主张的~~~)

解决方案其实非常简单:我们自己指定一个Content-Type:application/json就木问题了

至于根本原因,看下面的源码分析

RequestResponseBodyMethodProcessor匹配入参的消息转换器

我们可以先看看RequestResponseBodyMethodProcessor这个处理器:

// @since 3.1
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.hasParameterAnnotation(RequestBody.class);
	}
	...
	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		// 入参是支持使用Optional包装一层的~~~
		parameter = parameter.nestedIfOptional();
		// 这个方法就特别重要了,实现就在下面,现在强烈要求吧目光先投入到下面这个方法实现上~~~~
		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
		// 拿到入参的形参的名字  比如此处为person
		String name = Conventions.getVariableNameForParameter(parameter);

		// 下面就是进行参数绑定、数据适配、转换的逻辑了  这个在Spring MVC处理请求参数这一章会详细讲解
		// 数据校验@Validated也是在此处生效的
		if (binderFactory != null) {
			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
			if (arg != null) {
				validateIfApplicable(binder, parameter);
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
				}
			}
			if (mavContainer != null) {
				mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
			}
		}

		return adaptArgumentIfNecessary(arg, parameter);
	}


	// 之前讲过writeWithMessageConverters,这个相当于读的时候进行一个消息转换器的匹配,实现逻辑大体一致~~~
	@Override
	protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
			Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

		HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
		Assert.state(servletRequest != null, "No HttpServletRequest");
		ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);

		// 核心是这个方法,它的实现逻辑在父类AbstractMessageConverterMethodArgumentResolver上~~~继续转移目光吧~~~
		Object arg = readWithMessageConverters(inputMessage, parameter, paramType);

		// body体为空,而且不是@RequestBody(required = false),那就抛错呗  请求的body是必须的  这个很好理解
		if (arg == null && checkRequired(parameter)) {
			throw new HttpMessageNotReadableException("Required request body is missing: " + parameter.getExecutable().toGenericString(), inputMessage);
		}
		return arg;
	}
}

上面已经分析到,读取的核心匹配逻辑,其实在父类AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters,因此我们继续看父类的实现源码:

// @since 3.1  可议看出这个实现是处理请求request的处理器~~~
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
	...
	@SuppressWarnings("unchecked")
	@Nullable
	protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

		MediaType contentType;
		boolean noContentType = false;
	
		// 如果已经制定了contentType,那就没什么好说的  以你的为准
		try {
			contentType = inputMessage.getHeaders().getContentType();
		}
		catch (InvalidMediaTypeException ex) {
			throw new HttpMediaTypeNotSupportedException(ex.getMessage());
		}
		// Content-Type默认值:application/octet-stream
		if (contentType == null) {
			noContentType = true;
			contentType = MediaType.APPLICATION_OCTET_STREAM;
		}

		// 这几句代码就是解析出入参的类型~
		Class<?> contextClass = parameter.getContainingClass();
		Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
		if (targetClass == null) {
			ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
			targetClass = (Class<T>) resolvableType.resolve();
		}

		HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
		Object body = NO_VALUE;

		EmptyBodyCheckingHttpInputMessage message;
		try {
			message = new EmptyBodyCheckingHttpInputMessage(inputMessage);

			// 这里就是匹配消息转换器的的逻辑~~~ 还是一样的  优先以GenericHttpMessageConverter这种类型的转换器为准
			for (HttpMessageConverter<?> converter : this.messageConverters) {
				Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
				GenericHttpMessageConverter<?> genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
				if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
						(targetClass != null && converter.canRead(targetClass, contentType))) {
				
					// 因为它是一个EmptyBodyCheckingHttpInputMessage,所以它有这个判断方法  body != null表示有body内容
					// 注意此处的判断,若有body的时候,会执行我们配置的`RequestBodyAdvice` 进行事前、时候进行处理~~~  后面我会示范
					if (message.hasBody()) {
						HttpInputMessage msgToUse = getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
		
						// 执行消息转换器的read方法,从inputStream里面读出一个body出来~
						body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
								((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
		
						// 读出body后的事后工作~~~
						body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
					}
			
					// 如果没有body体,就交给handleEmptyBody来处理~~~~  第一个参数永远为null
					// 这个处理器可以让我们给默认值,比如body为null时,我们返回一个默认的body之类的吧
					else {
						body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
					}
					break;
				}
			}
		}
		catch (IOException ex) {
			throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
		}


		// 到此处,如果body还没有被赋值过~~~~ 此处千万不能直接报错,因为很多请求它可以不用body体的
		// 比如post请求,body里有数据。但是Content-type确实text/html 就会走这里(因为匹配不到消息转换器)
		if (body == NO_VALUE) {
			if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) || (noContentType && !message.hasBody())) {
				return null;
			}
		
			// 比如你是post方法,并且还指定了ContentType 你有body但是却没有找到支持你这种contentType的消息转换器,那肯定就报错了~~~
			// 这个异常会被InvocableHandlerMethod catch住,虽然不会终止程序。但是会打印一个warn日志~~~
			// 并不是所有的类型都能read的,从上篇博文可议看出,消息转换器它支持的可以read的类型还是有限的
			throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
		}

		MediaType selectedContentType = contentType;
		Object theBody = body;
		LogFormatUtils.traceDebug(logger, traceOn -> {
			String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
			return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
		});

		return body;
	}
	...
}

又比如我们这样子写:使用MultiValueMap是能够接收多值入参的。注意请设值Content-Type=application/x-www-form-urlencoded,这样FormHttpMessageConverter这个消息转换器可以转换它

借助RequestBodyAdvice实现对请求参数进行干预

RequestBodyAdvice它能对入参body封装的前后、空进行处理。比如下面我们就可以很好的实现日志打打印:

public class LogRequestBodyAdvice implements RequestBodyAdvice {
    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }
 
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        return httpInputMessage;
    }
 
    @Override
    public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        Method method=methodParameter.getMethod();
        log.info("{}.{}:{}",method.getDeclaringClass().getSimpleName(),method.getName(),JSON.toJSONString(o));
        return o;
    }
 
    @Override
    public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        Method method=methodParameter.getMethod();
        log.info("{}.{}",method.getDeclaringClass().getSimpleName(),method.getName());
        return o;
    }
}

效率非常高,也不需要对request的body进行多次读取了。

使用@ControllerAdvice注解+ResponseBodyAdvice+ResponseBodyAdvice,可以对请求的输入输出进行处理,避免了在controller中对业务代码侵入。FastJson有提供一个JSONPResponseBodyAdvice可以直接使用的

它还有一个很不错的应用场景:就是对请求、返回数据进行加密、解密

Spring—@ControllerAdvice等异常处理方式或统一处理数据

自定义消息转换器HttpMessageConverter

虽然前面说了,Spring MVC已经为我们准备了好多个消息转换器了,能应付99.99%的使用场景了。

但是我们经常会遇到说不喜欢用它自带的Jackson来序列化,而想换成我们想要的国产的FastJson转换器。怎么弄呢?那么接下来就来实现这一波

其实FastJson还是非常友好的,早在@since 1.2.10版本就已经为我们写好了FastJsonHttpMessageConverter,我们直接使用即可。

Spring MVC内置支持了jackson,gson。但却没有内置支持我们国产的FastJson

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {


    // 请不要复写这个方法,否则你最终只有你配置的转换器,Spring MVC默认的并不会加载了~~
    //@Override
    //public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    //    FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
    //    fastJsonHttpMessageConverter.setCharset(StandardCharsets.UTF_8);
    //    converters.add(fastJsonHttpMessageConverter);
    //}

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
        fastJsonHttpMessageConverter.setCharset(StandardCharsets.UTF_8);
        converters.add(fastJsonHttpMessageConverter);
    }
}

可议看到我们的FastJsonHttpMessageConverter已经被配置进去了。

此处我们把Handler写成这样:

    @ResponseBody
    @RequestMapping(value = "/hello/post", method = RequestMethod.POST)
    public Object hello(@RequestBody(required = false) Person person) {
        return person;
    }

关于read: 关于writer:

总结一句:我们发现不管是读还是写,最终生效的都还是MappingJackson2HttpMessageConverter,我们自定义的FastJson转换器并没有生效。相信这个原因大家都知道了:FastJson转换器排在Jackson转换器的后面,所以处理json不会生效

找到原因,那就只要把 自定义的消息转换器【FastJsonJsonpHttpMessageConverter】添加到 MappingJackson2HttpMessageConverter 前面就可以

如果你是SpringBoot环境,你可以直接使用HttpMessageConverters很方便的把你自定义的转换器提到最高优先级,但是此处我们介绍一下Spring中的处理方式:

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
        fastJsonHttpMessageConverter.setCharset(StandardCharsets.UTF_8);

        // 把FastJsonHttpMessageConverter放到Jackson的前面去
        // 这个list是个ArrayList 所以我们要么就放在首位(不建议),而converters.indexOf() 因为人家是new的 所以肯定是找不到此对象的位置的 所以采用遍历的方式吧
        int jacksonIndex = 0;
        for (int i = 0; i < converters.size(); i++) {
            if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
                jacksonIndex = i;
                break;
            }
        }

        // 添加在前面
        converters.add(jacksonIndex, fastJsonHttpMessageConverter);
    }

效果

最终效果也是没有问题的,json数据的转换工作都会被我们的FastJson接管了,完美

看到说可以通过HttpMessageConverters这种方式配置自定义的消息转换器,那是不严谨的。因为HttpMessageConverters它属于SpringBoot的类,而不是属于Spring Framework的

FastJsonHttpMessageConverter

绝大多数情况下,直接使用FastJsonHttpMessageConverter是没有问题的。但是如果是这样的:

@ResponseBody
@RequestMapping(value = "/hello/post", method = RequestMethod.POST)
public Object hello(@RequestBody(required = false) MultiValueMap person) {
    return person;
}

报错

JSON parse error: unsupport type interface org.springframework.util.MultiValueMap; nested exception is com.alibaba.fastjson.JSONException: unsupport type interface org.springframework.util.MultiValueMap]

我们发现报错竟然找到了FastJson的头上。说明了什么:责任链模式下,fastjson接了这个活,最终发现自己干不了,然后还抛出异常,这是明显的甩锅行为嘛

这个根本原因在这,看它的源码:

	// 会发现FastJson这个转换器它接受所有类型它都表示可以处理~~~~
    @Override
    protected boolean supports(Class<?> clazz) {
        return true;
    }

显然它违反了“手伸得太长的”基本原则,是需要付一定的责任的

正确使用姿势(推荐使用姿势)

为了避免误伤,其实我们配置它的时候应该限制它的作用范围,如下:

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
        fastJsonHttpMessageConverter.setCharset(StandardCharsets.UTF_8);

        // 这步是很重要的,让它只处理application/json这种MediaType的就没有问题了~~~~
        List<MediaType> fastMediaTypes = new ArrayList<>();
        fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes);

        // 把FastJsonHttpMessageConverter放到Jackson的前面去
        // 这个list是个ArrayList 所以我们要么就放在首位(不建议),而converters.indexOf() 因为人家是new的 所以肯定是找不到此对象的位置的 所以采用遍历的方式吧
        int jacksonIndex = 0;
        for (int i = 0; i < converters.size(); i++) {
            if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
                jacksonIndex = i;
                break;
            }
        }

        // 添加在前面
        converters.add(jacksonIndex, fastJsonHttpMessageConverter);
    }

这样它就不会造成问题了,因为我们只让它处理MediaType.APPLICATION_JSON_UTF8这种类型,而这就是它最擅长的。

使用Spring MVC实现优雅的文件下载

传统的,我们要进行文件下载,可以直接操作HttpServletRequest和HttpServletResponse来处理下载。那基本上就与Spring MVC的关系不大了。 我们能看到形如下面的代码:

    //设置响应头和客户端保存文件名
    response.setCharacterEncoding("utf-8");
    response.setContentType("multipart/form-data");
    response.setHeader("Content-Disposition", "attachment;fileName=" + fileName);
	...
        //打开本地文件流
        InputStream inputStream = new FileInputStream(filePath);
        //激活下载操作
        OutputStream os = response.getOutputStream();

        //循环写入输出流
        byte[] b = new byte[2048];
        int length;
        while ((length = inputStream.read(b)) > 0) {
            os.write(b, 0, length);
            downloadedLength += b.length;
        }

        // 这里主要关闭。
        os.close();
        inputStream.close();
	...

显然这一大段处理起来还是比较麻烦的。本文另外一种方案:在Spring MVC环境下能让你优雅的处理文件下载:使用ResponseEntity方式

DEMO

    // 处理下载 get/post/put请求等等都是可以的  但一般都用get请求
    @RequestMapping(value = "/download", method = RequestMethod.GET)
    public ResponseEntity<Resource> downloadFile(@RequestParam String fileName) {
        // 构造下载对象  读取出一个Resource出来  此处以类路径下的logback.xml
        DownloadFileInfoDto downloadFile = new DownloadFileInfoDto(fileName, new ClassPathResource("logback.xml"));
        return downloadResponse(downloadFile);
    }

    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    private static class DownloadFileInfoDto {
        private String fileName; // 下载的文件
        private Resource resource; // 下载的具体文件资源
    }

    // 共用方法  兼容到了IE浏览器~~~
    private static ResponseEntity<Resource> downloadResponse(
            DownloadFileInfoDto fileInfo) {
        String fileName = fileInfo.getFileName();
        Resource body = fileInfo.getResource();

        // ========通过User-Agent来判断浏览器类型 做一定的兼容~========
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String header = request.getHeader("User-Agent").toUpperCase();
        HttpStatus status = HttpStatus.CREATED;
        try {
            if (header.contains("MSIE") || header.contains("TRIDENT") || header.contains("EDGE")) {
                fileName = URLEncoder.encode(fileName, "UTF-8");
                fileName = fileName.replace("+", "%20");    // IE下载文件名空格变+号问题
                status = HttpStatus.OK;
            } else { // 其它浏览器 比如谷歌浏览器等等~~~~
                fileName = new String(fileName.getBytes("UTF-8"), "ISO8859-1");
            }
        } catch (UnsupportedEncodingException e) {
        }

        // =====响应头需设置为MediaType.APPLICATION_OCTET_STREAM=====
        HttpHeaders headers = new HttpHeaders();
		// 注意:若这个响应头不是必须的,但是如果你确定要下载,建议写上这个响应头~
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        // 此处,如果你自己没有设置这个请求头,浏览器肯定就不会弹窗对话框了。它就会以body的形式直接显示在浏览器上
        headers.setContentDispositionFormData("attachment", fileName);
        return new ResponseEntity<>(body, headers, status);
    }

备注:使用此种方式最终处理返回值的处理器为:HttpEntityMethodProcessor,使用的消息转换器为:ResourceHttpMessageConverter

这样请求就能弹出下载框了。响应头如下:

可以看到这里不仅设置了Content-Disposition请求头,还是设置了Content-type为application/octet- stream那就意味着你不想让浏览器直接显示内容,而是弹出一个”文件下载”的对话框。

关于application/octet-stream等响应头的解释,请看如下例子形象解释:

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="picture.png"

它表示对浏览器说:“我不清楚代码内容,请把其保存为一个文件,最好命名为picture.png”。

Content-Type: image/png
Content-Disposition: attachment; filename="picture.png"

它表示对浏览器说:表示“这是一个PNG图像,请将其保存为一个文件,最好命名为picture.png”。

Content-Type: image/png
Content-Disposition: inline; filename="picture.png"

它表示对浏览器说:“这是一个PNG图像,除非你不知道如何显示PNG图像,否则请显示它,如果用户选择保存它,我们建议文件名保存为picture.png”。

在能够识别内联的浏览器中,可议使用这个方法(现在绝大多数浏览器都能识别这种方式),少数浏览器会对它进行保存~~~~

所以当你给客户端传递的不知道是文本、图片、还是其它的格式时,使用application/octet-stream最佳。(是否弹出下载框不是由它决定的,主要是Content-Disposition这个请求头来决定的)

ResponseEntity方式对比传统Java方式

单单从代码上看ResponseEntity方式秒杀传统的Java方式,但是否我们想都不想就采用优雅方式呢?但其实非也,有些东西还是要注意的。

  • 基于ResponseEntity的实现的局限性还是很大:这种下载方式是一种一次性读取的下载方式,在文件较大的时候会直接抛出内存溢出(所以适合小文件下载,不超过1G吧)。还有就是这种下载方式因为是一次性全部输出,所以无法统计已下载量、未下载量等扩展功能,所以也就不能实现断点续传
  • 传统Java通用实现在功能上能够更加的丰富:对下载文件的大小无限制((循环读取一定量的字节写入到输出流中,因此不会造成内存溢出)。 因为是这种实现方式是基于循环写入的方式进行下载,在每次将字节块写入到输出流中的时都会进行输出流的合法性检测,在因为用户取消或者网络原因造成socket断开的时候,系统会抛出SocketWriteException。 当然我们可以捕获到这个异常,记录下当前已经下载的数据量、下载状态等。这样我们就可以实现断点续传的功能了

ResponseEntity方式的优点就是简洁,所以在比较小的文件下载时,它绝对是首选。若是有大量的下载需求,其实一般都建议使用ftp服务器而不是http了。

当然还有一种使用ResponseEntity<byte[]>的方式,也挺优雅的,这里就只提供代码参考了:

@RequestMapping("/testResponseEntity")
public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
    byte[] body = null;
    ServletContext servletContext = session.getServletContext();
    InputStream in = servletContext.getResourceAsStream("/files/abc.txt");
    body = new byte[in.available()];
    in.read(body);

    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Disposition","attachment;filename=abc.txt");

    HttpStatus statusCode = HttpStatus.OK;

    ResponseEntity<byte[]> response = new ResponseEntity<byte[]>(body,headers,statusCode);
    return  response;
}

此处使用的返回值处理同上,消息转换器是:ByteArrayHttpMessageConverter

代码中有顺序遍历匹配这种逻辑,或者叫责任链模式时,功能越具体的节点越是应该放在前面,功能最广最泛的节点应该放在最后面

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

4 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 593968浏览

随机文章
Spring Boot 项目中的 parent
5年前
Java—事件驱动进行代码解耦(EventBus)
3年前
Vue—computed计算属性
5年前
SpringBoot—ActiveMQ简单使用
5年前
Kotlin-内置类型—集合框架(五)
4年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 593968 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付