目录
一、ReturnValueHandler返回值处理器原理
二、利用MessageConverter消息转换器进行处理,将数据写为json
三、HttpMessageConverter原理
四、内容协商原理
五、自定义MessageConverter
六、自定义内容协商管理器(自定义内容协商策略)
众所周知,SpringBoot框架为我们的开发工作提供了便捷,大大减少了之前使用SSM(Spring、SpringMVC、MyBatis)框架进行开发时的繁琐配置。
那是因为SpringBoot框架在底层为我们自动运行了许多AutoConfiguration自动配置类,其中就包括Web场景的自动配置类 WebMvcAutoConfiguration ,这个自动配置类为我们自动注册了许多我们在开发Web场景式需要使用的组件到IOC容器中。
在众多组件中,MessageConverter消息转换器为我们实现了将Java数据类型和json/xml等媒体数据类型的互相转换,接下来我们就从Controller方法标注 @ResponseBody 注解——>获得ReturnValueHandler返回值处理器——>获得MessageConverter消息转换器进行内容协商——>MessageConverter消息转换器如何将数据响应出去进行详细分析......
public class WebMvcAutoConfiguration {
......
@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
......
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
this.messageConvertersProvider.ifAvailable((customConverters) -> {
converters.addAll(customConverters.getConverters());
});
}
......
}
......
}
一、ReturnValueHandler返回值处理器原理
1、先创建一个普通的Controller类,返回自定义的Person数据,并在其中的方法上标注 @ResponseBody 注解
@Controller
public class PersonController {
@ResponseBody
@GetMapping("/test/person")
public Person test(){
Person person = new Person();
person.setName("xiaoming");
person.setAge(20);
return person;
}
}
2、以debug模式启动程序,来到我们真正执行目标方法的代码处
3、来到方法内部,SpringMVC底层先遍历所有的返回值解析器returnValueHandlers
放到一个可执行的方法 invocableMethod 中
4、接下来SpringMVC底层会利用反射机制执行目标方法,当目标方法执行完以后会得到我们自己的返回值也就是我们的Person对象,得到返回值以后,就会利用之前保存好的返回值解析器来帮我们处理返回值
5、处理返回值的第一步就是先循环遍历所有的返回值处理器,看看哪个返回值处理器可以处理我们当前的返回值,最终遍历得到 RequestResponseBodyMethodProcessor 可以处理我们当前标注了 @ResponseBody 注解的方法。
返回值处理器其实是一个接口,它定义了两个方法。
(1) supportsReturnType :判断当前返回值处理器是否支持此类型的返回值
(2) handleReturnValue :如果支持,就会调用此方法真正进行处理
6、得到哪个返回值处理器可以处理当前返回值后,就会执行返回值处理器的 handleReturnValue 方法
进入方法后,就会经过一系列设置得到请求、响应等一系列数据,最终调用writeWithMessageConverters()传入我们的返回值内容、返回值类型以及请求和响应来使用消息转换器进行写出操作。
二、利用MessageConverter消息转换器进行处理,将数据写为json
Accept(翻译 : 接受),也就是浏览器或者客户端在发请求时会以请求头的方式告诉服务器它可以接収什么样的类型参数,优先接受HTML页面,再不济也可以接収图片等所有类型。
HTML页面权重最大,为0.9
*/* 即所有类型数据都可接受,权重为0.8
1、在SpringBoot底层的内容协商源码中,首先确定本次请求是否被提前处理过(即没有确定媒体类型,如果有就用之前确定好的媒体类型),然后进入else分支,先获取到本次请求的原生request,然后调用getAcceptableMediaTypes()方法得到浏览器可以接収的媒体类型。
接着服务器调用getProducibleMediaTypes()方法得到服务器可以响应的数据类型
getProducibleMediaTypes()方法内部其实就是循环遍历SpringMVC底层的每一个消息转换器,判断哪个消息转换器可以处理本次响应数据并存放在result中。
2、得到浏览器或客户端想要接収的类型以及服务器可以响应的所有数据类型后,通过循环,浏览器可以接収的类型和服务器可以响应的类型开始匹配,最终会在mediaTypesToUse里面存入我们到底可以写什么样的类型数据
3、一旦我们决定要使用什么样的类型数据,SpringMVC底层就会遍历所有的消息转换器,看谁能处理这种类型数据(这里假设要响应到浏览器的数据类型为json)。
4、SpringMVC底层循环判断每一个Converter能不能处理json类型,等循环遍历到MappingJackson2HttpMessageConverter,就进入这个消息转换器的canWrite()方法,最终得到结果为true,即MappingJackson2HttpMessageConverter可以处理json类型的数据。
5、得到可以处理本次要响应数据的消息转换器后,源码走到真正的write方法
在write()方法中得到此次响应的响应头,响应头中的"Content-Type"为json类型
6、最终在write()方法中得到一系列处理后,把Person数据类型转为json类型并把数据以json格式写到了outputMessage中
三、HttpMessageConverter原理
SpringMVC底层的每一个消息转换器其实都实现了一个HttpMessageConverter接口,它们有以下几种方法,也都有自己相应的处理能力
HttpMessageConverter:看是否支持将此Class类型的对象转换为MediaType类型的数据
方法说明:
canRead():如果浏览器发送一个请求,此消息转换器能否将json等媒体数据类型的数据转换为Person类型
canWrite():能不能把服务器返回的Person对象写成一个json数据等媒体数据类型响应给浏览器
getSupportedMediaTypes():SpringMVC底层要通过getSupportedMediaTypes()方法来统计所有的MessageConverter都能写出哪些内容
read():具体的读入操作
wirte():具体的写出操作
如果是响应就是要写出去,如果是请求就是要转回来
四、内容协商原理
当Controller中的方法标注了@ResponseBody注解时,因为在SpringBoot开发场景中,Web场景就自动为我们导入了json依赖,所以服务器默认给浏览器响应json类型的数据,但如果想要返回xml类型数据,只要在POM文件中引入相关依赖即可
<!--引入XML依赖-->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
1、先判断当前响应头中是否已经有确定的媒体类型,因为可能拦截器会提前处理过,写死了数据类型,如果有的话就用之前确定过的数据类型。
2、然后判断客户端或浏览器能够接收什么样的响应数据类型
(1)在getAcceptableMediaTypes()方法中获取客户端或浏览器能够接收什么样的响应数据类型的方法中,有一个contentNegotiationManger(内容协商管理器),默认使用基于请求头的内容协商策略
如果是 HeaderContentNegotiationStrategy 基于请求头的内容协商策略,它会使用原生request请求得到请求头的Accept字段的值,确定客户端(Postman)或浏览器可以接収的媒体类型
(2)但是浏览器有时不能随意改动请求头的Accept字段(除非发送Ajax请求),为了方便内容协商,我们可以在全局配置文件(application.properties/yml)中添加以下配置,这时只要我们在浏览器发送请求时带上format=json/xml的参数,就开启了基于参数的内容协商功能
spring.mvc.contentnegotiation.favor-parameter=true
如果开启了基于参数的内容协商功能,此时contentNegotiation(内容协商管理器)中就会有基于参数的内容协商策略
基于参数的内容协商策略底层会为我们获取请求头中format的值,得到这个值以后就确定了浏览器要接収的媒体数据类型
接着我们就可以在浏览器发送以下带format参数的请求
http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
3、得到客户端或浏览器可以接収什么样的数据类型后,进入getProducibleMediaTypes()方法确定服务端可以响应生产什么样的数据类型(方法内部也就是循环遍历SpringMVC底层所有的MessageConverter,看谁支持操作这种数据类型,并且把统计结果存放到result中)
4、确定 浏览器想要的数据类型 和 服务器可以响应的数据类型 后,进行内容协商的最佳匹配得到最佳匹配媒体类型存放到mediaTypesToUse里。
5、最后经过一系列筛选,最终确定此次响应的数据类型selectMediaType,然后再一次循环遍历所有的MessageConverter看看谁能支持此次处理(将java对象转为最佳匹配媒体类型json/xml,此时在SpringMVC底层MessageConverter循环遍历了两次,一次是在第3步确定当前服务器的所有MessageConverter合起来可以支持哪些数据,再一次就是现在看看谁能支持处理这个最佳匹配媒体类型)
6、找到可以处理最佳匹配媒体类型的MessageConverter后,调用它的write方法进行转换。
五、自定义MessageConverter
如果我们想要自定义MessageConverter,只需要在自定义的WebMvcConfig配置类里配置
1、自定义WebMvcConfig的两种方式
(1)自定义一个配置类,在配置类里创建一个webMvcConfigurer的组件并放到IOC容器中
@Configuration(proxyBeanMethods = false)
public class WebConfig {
//WebMvcConfigurer定制化SpringMvc的功能
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer(){
*****实现WebMvcConfigurer中的方法实现扩展功能*****
}
}
}
(2)创建一个配置类直接实现WebMvcConfigurer,并在配置类中实现WebMvcConfigurer中的方法实现扩展功能
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
*****实现WebMvcConfigurer中的方法实现扩展功能*****
}
(3)自定义WebMvcConfig不要使用用@EnableWebMvc注解。我们这里使用 @Configuration
+ 实现WebMvcConfigurer
自定义规则,因为使用 @EnableWebMvc + @Configuration + DelegatingWebMvcConfiguration 将全面接管SpringMVC,SpringMVC底层为我们调整好的所有默认配置都会失效。
2、将自定义 MessageConverter 配置到自定义的WebMvcConfig配置类里
(1)编写自定义的 MessageConverter
public class PengpengMessageConverter implements HttpMessageConverter<Person> {
/**
* 支不支持把Person类型的数据读成某种媒体数据类型
* 也就是浏览器传来json或xml类型的数据,Controller中的方法的入参位置标注@RequestBody注解时自动注入时转为Person类型
* @param aClass
* @param mediaType
* @return
*/
@Override
public boolean canRead(Class<?> aClass, MediaType mediaType) {
return false;
}
/**
* 只要响应的是Person类型就能自动转换成json或xml类型
* @param aClass
* @param mediaType
* @return
*/
@Override
public boolean canWrite(Class<?> aClass, MediaType mediaType) {
return aClass.isAssignableFrom(Person.class);
}
/**
* 服务器要通过getSupportedMediaTypes()来统计所有的MessageConverter都能写出哪些内容
* @return 这里要告诉SpringMvc我们要操作"application/x-pengpeng"自定义类型
*/
@Override
public List<MediaType> getSupportedMediaTypes() { //获取所有支持的媒体类型
return MediaType.parseMediaTypes("application/x-pp");
}
@Override
public Person read(Class<? extends Person> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
//
@Override
public void write(Person person, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
//自定义协议数据的写出
String data = person.getName() + ";" + person.getAge();
//获取一个输出流
OutputStream body = httpOutputMessage.getBody();
//将自定义数据放到输出流中写出去
body.write(data.getBytes());
}
}
(2)编写自定义的WebMvcConfig
@Configuration(proxyBeanMethods = false)
public class WebConfig {
//WebMvcConfigurer定制化SpringMvc的功能
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer(){
//扩展自定义的MessageConverter
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new PengpengMessageConverter());
}
};
}
}
此时SpringMVC底层就会存在我们自定义的 MessageConverter ,当客户端发送过来的请求的请求头中的Accept字段的值为"application/x-pp",SpringMVC在底层进行内容协商时,就会得到此时客户端想要接収的数据类型为"application/x-pp",接着进行内容协商的最佳匹配得到我们自定义的消息转换器可以处理这种类型的数据,然后就会调用我们自定义消息转换器的write方法将数据响应出去。
六、自定义内容协商管理器(自定义内容协商策略)
上面自定义的 MessageConverter 默认只能支持基于请求头的内容协商策略,此时如果我们想要服务器支持我们自定义的基于参数的内容协商策略,就需要在自定义配置类中进行配置。
@Configuration(proxyBeanMethods = false)
public class WebConfig {
//WebMvcConfigurer定制化SpringMvc的功能
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer(){
/**
* 自定义内容协商管理器(自定义内容协商策略)
* @param configurer
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Map<String, MediaType> map = new HashMap<>();
map.put("xml",MediaType.APPLICATION_XML);
map.put("json",MediaType.APPLICATION_JSON);
//如果参数中是“pp”,对应自定义媒体类型
map.put("pp",MediaType.parseMediaType("application/x-pp"));
//指定支持解析哪些参数对应的哪些媒体类型,这里的有参构造放的是一个map集合
ParameterContentNegotiationStrategy paramStrategy = new ParameterContentNegotiationStrategy(map);
//基于请求头的处理策略
HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();
//设置内容协商策略
configurer.strategies(Arrays.asList(paramStrategy,headerStrategy));
}
//添加自定义的MessageConverter
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new PengpengMessageConverter());
}
};
}
}
此时我们自定义的内容协商管理器就会覆盖SpringMVC底层默认的内容协商管理器,只要我们在浏览器发送http://localhost:8080/test/person?format=pp请求,SpringMVC底层就会知道浏览器想要接収"application/x-pp"媒体类型的数据,然后接着根据内容协商的最佳匹配得到我们自定义的消息转换器可以处理这种类型的数据,然后就会调用我们自定义消息转换器的write方法将数据响应出去。
如果我们在自定义(覆盖默认)内容协商管理器时,没有添加基于请求头的内容协商策略,此时如果我们发送的是正常请求没有基于参数的内容协商,那么不管我们在客户端发送的请求中请求头的Accept字段的值为任何值,SpringMVC底层没有基于请求头的内容协商策略就会默认认为此次请求浏览器想要接収的媒体数据类型为 */*(也就是任何类型),那么内容协商的最佳匹配就会根据权重匹配到权重最高的消息转换器响应相应的数据类型。
以上就是我对 MessageConverter 消息转换器底层原理的分析,感谢您的观看,如果有不同的见解,欢迎与我共同学习讨论~~~