SpringMVC处理请求源码分析——part2 场景分析
上篇中我们已经讲了SpringMVC处理HTTP请求的整体流程,其间我们讲到了很多接口,如参数解析器,返回结果处理器等,但我们都没有深入的进去查看这些解析器或处理器是如何处理的。本章就带大家进入真实的案例场景,看看这些接口在具体场景下是如何发挥作用的
0. 参数解析器
为了方便后面具体案例的分析,我们先来回顾下之前在上一篇SpringMVC处理请求源码分析——part1 整体流程 - coderZoe的博客中讲到的参数解析器。
当我们发送HTTP请求 时,根据之前的框架分析,我们知道这个请求会由RequestMappingHandlerAdapter
来处理,在RequestMappingHandlerAdapter
来处理的时候,会将自己的很多信息封装到ServletInvocableHandlerMethod
中,包括自己的参数解析器和返回值处理器。ServletInvocableHandlerMethod
做处理的代码我们之前看过了,这里再拿过来:
其中invokeForRequest()
内容如下:
getMethodArgumentValues()
内容如下:
这里的supportsParameter()
源码如下:
而resolveArgument()
源码如下:
我们之前虽然大体上走完了每个流程,但其实并没有深入到每个接口的实现类里去看,这一主要原因是SpringMVC要处理的情况太多了,只能结合实际的场景来说源码。因此下面我们将会一个一个场景的讲源码处理,首先从参数解析器的源码场景说起。
1. @PathVariable
@PathVariable
注解是Web开发中十分常见的一个注解,我们可以从路径中获取信息作为参数。
举个例子如下:
当我们执行HTTP请求 GET /user/1
的时候,上面的参数id值就被映射为了1。根据上面的源码我们知道,要想解析出参数id,必然有一个参数解析器做了这个工作。
首先打断点到supportsParameter()
,查看到底是哪个参数解析器支持这样的处理。通过打断点可以很容易知道是PathVariableMethodArgumentResolver
参数解析器支持解析这个参数,那我们就需要问两个问题了:
- 为什么
PathVariableMethodArgumentResolver
可以支持这个参数的解析,它是如何判定的 PathVariableMethodArgumentResolver
是如何解析参数,从HTTP请求中将信息抠出赋给id字段的呢?
这两个问题的答案其实也是PathVariableMethodArgumentResolver
的两个实现方法:supportsParameter()
和resolveArgument()
我们先来看第一个方法的源码:
这个函数的判断逻辑比较简单,首先是判断如果参数上不带@PathVariable
注解直接返回false。其次带了注解,判断我们的参数类型是不是Map类型的,如果是的话就获取@PathVariable
注解信息并要求其value内容不能为空。最后如果不是Map但是有@PathVariable
注解就直接返回true。我们的参数是long型,且标了@PathVariable
,因此会直接返回true。
再看第二个源码(其实是PathVariableMethodArgumentResolver
的父类AbstractNamedValueMethodArgumentResolver
实现的):
首先我们先看下参数解析器是如何从请求信息中拿到参数值,也即:resolveName(resolvedName.toString(), nestedParameter, webRequest);
的实现:
可以看到思路很简单,在走到当前步骤之前SpringMVC就做了一个处理,将我们Controller上写的URL与实际请求的URL做了一个映射处理,比如 Controller层为:/user/{id}/{age}
,实际请求为:/user/1/27
。这时SpringMVC会根据路径匹配得到K,V,分别是id ->1和age ->27。并将这个Map存储在Request的请求域中,且将它的key值设为HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE
。这样我们走到这里的时候再从请求域中根据key值就可以拿到这个信息,信息本身是个Map,再传入resolvedName就可以拿到value,因此很容易通过传入id得到1这个信息。(关心这个Map是何时构建的可以查阅源码的RequestMappingInfoHandlerMapping#extractMatchDetails()
函数,它会在RequestMappingInfoHandlerMapping#getHandlerInternal()
里被调用,只不过这里面的内容有些多,需要断点打的深一点)
其次我们又看到了数据绑定器,我们前一篇中已经见过它了,当时我们说从HTTP请求中解析出数据后,得能将这些数据绑定到我们的参数上的,这个工作就是数据绑定器干的工作。由于我们的当前例子的参数比较简单(只是一个long id),所以很难发挥数据绑定器的作用,不过我们这里可以先简单的说一下:
我们知道HTTP协议是文本协议,这代表从HTTP请求中解析出的东西都是文本(二进制除外),所谓文本也即字符串,因此上面的resolveName()
函数得到的其实是字符串"1",但我们的HandlerMethod参数是long类型,这就需要一个字符串向long类型的转化,而这其实就是数据绑定器做的工作。
如果大家源码打的比较深的话会发现,实际进行转化的核心代码如下:
而获取转化器的代码如下:
是不是又想到了策略模式?自己持有多个转化器,在需要转化的时候就找到适合的转化器来转化。
在SpringBoot2.7.2版本中,默认情况下的转化器共有124个,部分内容如下:
可以看到这些参数解析器都是将一种数据类型转为另一种数据类型,针对于我们的情况就需要一个将String类型转为Long类型的转化器。
另外需要提一嘴的是,这里的策略模式与之前参数解析器或返回值处理器的设计不同,之前策略接口都都有个类似于support()
的功能,主类会挨个问每个策略是否支持解析,支持了再调用它来解析。但是这里是用HashMap来做的。也即我们将情况设计为key值,解决方案设计为value值。这样直接输入key值就能得到策略,无需遍历询问。通过源码也很容易看到:
converters
是一个Map。其Key是源类型+目标类型
另外,SpringMVC支持让我们自定义一些类型转化器的,可以按照自己的规则来做类型转化,我们下一章就会说到。
2. 表单提交
2.1. 源码分析
举例如下:
其中我们的POJO类,User和Pet信息如下:
当我们点击输入如下信息:
点击提交给后端,我们就能通过前端的参数将这些信息赋值到User对象上
很明显,这是SpringMVC帮我们做的,依据之前的源码经验,我们知道必然有一个参数解析器帮我们做了这个工作,同样通过源码
很快可以定位到帮我们解析参数的参数解析器是ServletModelAttributeMethodProcessor
,因此这个参数解析器就是处理表单提交的,老规矩我们先看下它为什么能够处理表单请求再看下它是如何解析表单参数的:
其中parameter.hasParameterAnnotation(ModelAttribute.class)
是指当前参数上包含注解@ModelAttribute
,我们这里没有,因此是false。this.annotationNotRequired
在当前对象中恒为true(构造方法中构造时就写死的true),最后一个BeanUtils.isSimpleProperty(parameter.getParameterType())
是判断当前参数是否是基本类型,我们的参数是User对象,不是基本类型,取反后为true,因此整体返回true。
下面我们再看下ServletModelAttributeMethodProcessor
是如何解析参数的:
源码中的流程会比较长,这里只截取了主要的部分,其思路比较清晰:
由于我们知道当前参数不是一个简单类型,因此需要先构造出来,通过
就构造出了一个空的对象(如果点进去源码的话会发现其实就是反射拿到默认构造方法,然后调用默认的构造方法创建对象),比如如果是我们的User对象,执行完这句后就会得到:
我们不妨叫一个壳对象,然后构造数据绑定器,数据绑定器会解析HTTP请求,将HTTP请求的信息绑定到我们的壳对象上,这样我们就得到了一个有意义的对象,其中数据的绑定操作对应源码:
执行完成后,我们的User对象就变成了
因此bindRequestParameters()
方法是解析出来参数。
bindRequestParameters()
源码如下:
而servletBinder.bind()
源码如下:
这里十分关键的是MutablePropertyValues
对象,这里我们已经将form表单提交的参数进行了初步解析,解析为了一个MutablePropertyValues
对象,MutablePropertyValues
内部有个List,装着解析后的每个参数信息:
这个解析其实不难,我们在提交form表单的时候,HTTP的参数原始数据如下:
name=tom&age=18&pet.name=myDog&pet.age=2
,因此我们可以很容易的按&
和=
进行拆分,得到上述MutablePropertyValues
信息。
这个信息会非常的关键,我们在后面的数据绑定中就是以这个信息作为信息源来与我们的User对象进行绑定的。
多次源码深入后会走到 DataBinder#applyPropertyValues()
方法,其源码如下:
可以看到就是拿到属性访问器,设置属性。所谓属性访问器,大家不用想的多高大上,其实就是一个对象包装器,通过它可以反射的将一些属性设置值。这里的属性访问器就是BeanWrapperImpl
,了解Spring的同学肯定对它很熟悉(其实我们三期网管的协议解析器就用了这个对象,我们可以简单将它理解为一个工具类,这个工具类可以反射设置对象里的属性值)。因此SpringMVC就是通过BeanWrapperImpl
将HTTP请求信息绑定到User
对象上的。
BeanWrapperImpl#setPropertyValues()
函数源码如下:
后面一层层的源码会很深,我们可以讲一些比较核心的部分:
在进行setPropertyValue()
设置时会走进AbstractNestablePropertyAccessor#processLocalProperty()
函数(BeanWrapperImpl
继承自AbstractNestablePropertyAccessor
):
AbstractNestablePropertyAccessor#processLocalProperty()
函数中比较重要的一行信息是:
其中convertForProperty()
内容如下:
走到这里的时候就可以看到与@PathVariable
的一些共同之处,都需要使用类型转化器来转化数据。
这其实也容易理解,HTTP请求提交age=18,解出来的是字符串"18",自然就需要转为int类型。
因此大体的流程为:通过HTTP请求解析form提交的信息,根据KV值解析出来多条,如:
接着调用默认构造方法,new出空壳的参数对象,再借用BeanWrapperImpl
,将解析出的多条KV信息绑定到参数对象上。在绑定的过程中可能需要类型转化,比如字符串转整型,这时就需要借助类型转化器来转化数据,将转化后的数据再绑定到属性上。
另外,BeanWrapperImpl
其实也是持有124个类型转化器的:
2.2 自定义类型转化器
可以看到,类型转化器会将HTTP请求信息转化为我们参数上对应的数据类型,我们之前也说了SpringBoot2.7.2版本中默认包含124个类型转化器,这些类型转化器大都是基本类型转化器,如String转Integer等。我们可以自定义类型转化器,来扩展Spring自带的转化器功能,举例如下:
上例中,我们将pet
信息写为"狗子,18",也即我们想将"狗子,18"这一信息转为Pet
对象,再或者说,我们想将字符串类型转为Pet
对象。我们知道SpringBoot默认的数据转化器是没有这种功能的,此时就需要自定义类型转化器:
自定义类型转化器需要继承自Convert
接口,我们这里的转化做的比较粗糙,大家明白就好。
接着我们就需要将自己的自定义转化器注册到SpringBoot中,目前对于SpringMvc功能的增强可以通过自定义一个WebMvcConfigure Bean 或者继承WebMvcConfigure接口实现自己的对象注册到Bean中。
这样SpringMVC在进行类型转化的时候,根据form表单提交的信息pet=myDog,2
,SpringMVC根据key值pet找到User对象中属性是pet的属性,发现类型是Pet类型,然后就是将myDog,2
字符串转为Pet类型,此时转化的源是String,目的是Pet类型,根据这一信息作为key值从converts这个map中获取转化器,很自然的就拿到了我们自己写的转化器,然后调用我们自己写的转化器将字符串转为Pet对象。
2.3 一些补充
我在B站看这块视频的时候看到很多弹幕会提一个问题,我们在2.1节举的例子中,为什么Controller层的方法没加@RequestBody
注解?
也即
这段代码中的参数User对象,为什么没有标@RequestBody注解。很多同学会认为只要是Post请求提交的对象,后端都应该是加@RequestBody
注解的。
首先@RequestBody
注解使用的场景是请求参数在请求体中并且是JSON格式(如果不了解你需要先学习基本的Spring MVC应用知识),而表单提交提交的数据虽然在请求体中,但不是JSON格式的数据,而是param1=value1¶m2=value2格式的数据,因此此处如果加@RequestBody
注解会无法进来,从这也可以看出,这两种情况是使用不同的参数解析器来解析的。
注:上面说的不是很贴切,@RequestBody
注解虽然拿的是请求体中的数据,但并不一定是JSON,你完全可以这样写:
这代表直接将请求体中所有的数据拿到作为一个字符串,这时请求体中到底传过来的是不是JSON都无所谓了。
但如果写出
则请求体中一定得是JSON格式的。
3. @RequestBody
3.1 源码分析
上面我们已经提了一嘴@RequestBody
注解,这个注解主要是将HTTP请求协议体中的JSON数据转为我们的Java对象,举例如下:
我们使用postman模拟发送请求如下:
很明显又是SpringMVC将HTTP请求体中的数据取出来,转为了我们的User对象并设置赋给了我们的参数,那么就来看看是哪个参数解析器做的工作:
打断点定位是哪个参数解析器的工作我们不再重复了,实际上是RequestResponseBodyMethodProcessor
参数解析器,我们还是看两个内容,为什么它支持解析这种情况,以及它是如何解析的:
supportsParameter()
源码如下:
可以看到很简单,就是判断参数上是否有@RequestBody
注解,凡是有这个注解的就支持,我们的User对象上有这个注解,所以很明显,当前参数解析器能解析这种情况。
下面我们就看下RequestResponseBodyMethodProcessor
是如何解析参数的:
readWithMessageConverters()
函数会将HTTP请求体中的JSON转为我们的Java对象,其源码如下:
能够看到,这里依然是策略模式的使用,策略的接口是HttpMessageConverter
,翻译过来即Http消息转化器。RequestResponseBodyMethodProcessor
根据请求的content-type和我们的参数以及Controller对象类型等信息挨个询问每个消息转化器是否支持解析当前HTTP消息。在这里我们的content-type是application/json。
RequestResponseBodyMethodProcessor
对象持有多个HttpMessageConverter
,其中属性messageConverters
是个List<HttpMessageConverter>
。SpringBoot2.7.2版本默认情况下messageConverters
内有10个HttpMessageConverter
实现对象
通过名字不难看出ByteArrayHttpMessageConverter
是用来解析byte数组的,StringHttpMessageConverter
是用来解析字符串的,MappingJackson2HttpMessageConverter
对象是用来转化JSON数据的,Jaxb2RootElementHttpMessageConverter
是用来解析xml的。
打断点不难发现,MappingJackson2HttpMessageConverter
解析器可以解析我们的请求,其判断自己是否能解析的代码如下:
MappingJackson2HttpMessageConverter
支持两种mediaType:application/json和application/*+json
我们发的HTTP请求content-type是application/json,同时我们的Java对象还支持序列化,因此自然返回true。
知道了MappingJackson2HttpMessageConverter
为什么能够解析,再看MappingJackson2HttpMessageConverter
是如何解析的:
其实就是拿到ObjectMappper
然后调用ObjectMappper
来解析JSON。
3.2 一些总结
这样其实我们就基本分析完了参数解析器RequestResponseBodyMethodProcessor
处理流程的源码,首先RequestResponseBodyMethodProcessor
会解析所有标注了@RequestBody
注解的参数,其次在解析的时候,RequestResponseBodyMethodProcessor
内部持有多个HttpMessageConverter
,RequestResponseBodyMethodProcessor
会挨个遍历每个HttpMessageConverter
询问其是否能够解析,HttpMessageConverter
一般会根据请求的content-type和要转化的Java对象来判断自己是否能解析,如我们的MappingJackson2HttpMessageConverter
只能解析请求的content-type是application/json和application/*+json的。拿到HttpMessageConverter
就可以直接进行解析了。可以看到我们之前对于@RequestBody
注解的理解比较片面,认为前端必须要传入JSON,然后它就会被解析为Java对象,现在看了源码会发现前端能传很多格式可以被@RequestBody
解析,比如xml。
3.3 自定义HttpMessageConverter
与自定义类型转化器一样,我们也可以自定义消息转化器。一个消息转化器往往是解析一种(或多种)mediaType类型下的HTTP请求,不同的mediaType的HTTP请求内容格式也不相同,比如application/json格式就是JSON类型,application-xml格式就是xml类型。
既然要自定义HttpMessageConverter
,就使用自定义的media-type:application-coderZoe
同时要求这种media-type下前端传过来的参数只有value,没有key,且value之间逗号隔开,比如前端传过来的请求体是:
我们主要实现了canRead()
和read()
方法,实现的源码比较简单,这里不再解释。
然后我们将这个自定义的消息转化器加入到Spring中:
根据前面的postman截图发送消息,打断点可以看到RequestResponseBodyMethodProcessor
的messageConverters
已经有了11个实现类,其中就包含我们自定义的消息转化器。
同样,我们的Controller层依然可以正常接收到参数:
4. @ResponseBody
4.1 源码分析
前面我们看的都是参数解析器的源码,现在我们看下返回值处理器的源码,从我们使用最频繁的@ResponseBody
说起,很多同学知道的是@ResponseBody
会将我们返回给请求的对象转为JSON写出到HTTP响应。现在我们来看这一功能是如何实现的。
通过前一篇SpringMVC处理请求源码分析——part1 整体流程 - coderZoe的博客,我们知道在执行完HandlerMethod拿到返回值的时候,SpringMVC会使用返回值处理器来处理返回值:
而处理返回的源码为:
这些内容我们之前都看过,策略模式,选择一个能处理的HandlerMethodReturnValueHandler
进行处理。
为了源码分析的顺序,我们依然举个例子:
编写后端:
通过PostMan发送请求:
可以看到我们后端返回的是一个Java对象,前端拿到的是一个JSON。这肯定是SpringMVC帮我们做了处理,根据前面的源码,很容易打断点定位到能处理这个返回的是RequestResponseBodyMethodProcessor
返回值处理器(是的,又是它,我们在看@RequestBody
注解源码的时候也是它,它既是参数解析器也是返回值处理器)。
4.1.1 RequestResponseBodyMethodProcessor
同样,我们先看它为什么能处理这个返回,再看它是如何处理返回的:
supportsReturnType()
源码如下:
可以看到就是判断方法所在的类上是否包含@ResponseBody
注解和方法本身上是否包含@ResponseBody
注解,由于@RestController
是个复合注解,由@Controller
与@ResponseBody
组成,因此我们的返回值可以被RequestResponseBodyMethodProcessor
处理。下面我们就看下RequestResponseBodyMethodProcessor
是如何处理返回值的:
这里会走进writeWithMessageConverters()
函数:
writeWithMessageConverters()
的源码会相对比较多且复杂,在讲解源码前我们需要先讲一个东西叫内容协商。
4.1.2 内容协商
我们之前在HTTP请求头中已经看到了content-type信息,这代表请求方会告诉服务端自己发来的数据是什么类型的数据,除此以外,HTTP还有另一个重要信息accept。
在发送HTTP请求的时候,浏览器会告诉服务器自己支持哪种数据的返回,这一信息会放在HTTP请求头的Accept字段。比如我们上面的Post表单提交,其Accept信息是:
上述代表当前浏览器可以接收(逗号分隔)
- text/html
- application/xhtml+xml
- application/xml;q=0.9
- image/webp
- image/apng
- */*;q=0.8
- application/signed-exchange;v=b3;q=0.9
这些类型的返回,其中q代表权重(关于权重我们一会再说)。
但是我们的服务器往往也需要判断自己能够返回哪些类型,然后对服务器能返回的且浏览器能接收的这两个类型集合做交集,得到的结果就是服务器可以返回给浏览器的数据类型,这就是内容协商。
4.1.3 writeWithMessageConverters()
下面我们看下writeWithMessageConverters()
的部分源码:
可以看到,得到selectedMediaType,也即得到了确定的Http输出类型(MediaType)后,就该将我们的输出值转为这个对应的类型数据了,但现在问题来了,要怎样转化呢?
AbstractMessageConverterMethodArgumentResolver
(RequestResponseBodyMethodProcessor
的父类)内部有一个属性叫
HttpMessageConverter
的集合,这个接口我们上面讲过了HTTP消息转化器,但上面说的是将HTTP消息转为我们的Java对象,这里的作用是将Java对象转为我们的HTTP消息。对于HttpMessageConverter
而言,HTTP转Java属于read,Java转HTTP属于write。
HttpMessageConverter
接口源码如下:
其中canRead
和canWrite()
代表是否支持读取和写入。read()
和write()
代表进行读取和写入。
在默认情况下,messageConverters
内有10个已经初始化的HttpMessageConverter
(我们之前已经看到过了):
它们的功能通过名字也很容易看出来,比如ByteArrayHttpMessageConvert
是将byte数组转为Http数据输出,MappingJackson2HttpMessageConverter
是将Java对象转为JSON然后通过HTTP输出。
我们的返回处理器会挨个遍历这10个消息转化器,询问它们是否支持写入,如果支持写入,那么就用这个消息处理器来写入,对应的源码便是刚才的下半部分:
通过上面说的内容协商,我们已经知道selectedMediaType
是application/json。然后打断点会得到MappingJackson2HttpMessageConverter
消息转化器支持处理我们的返回结果。我们这里自然需要看两点内容了,为什么它支持写出,它又是如何写出的。
4.1.4 MappingJackson2HttpMessageConverter
canWrite()
源码如下:
可以看到与canRead()
代码基本相同,就是判断要写出的mediaType自己是否支持,以及要写出的对象是否可以序列化。
write()
源码如下:
核心思想还是拿到jackson的ObjectMapper将Java对象转为JSON再写出到HTTP输出流。
4.2 XML
看源码的过程中我们其实是看到SpringMVC是包含有xml消息转化器的,但xml消息转化器要想生效,还需要导入一个依赖包。
此时我们的Controller层方法不变
但这时的Http响应体返回为:
可以看到是一个xml类型的数据。
我们之前说过,内容协商是根据浏览器能处理的请求和我们服务器能返回的请求共同再根据权重排序共同得出来的结果。上面的浏览器请求中,我们返回的是application/json
格式的数据,这是匹配到的浏览器的*/*
类型,但这种类型只有0.8的权重,如果我们的服务器支持xml类型的返回结果,application/xml;q=0.9
是0.9的权重,此时就会按xml转化返回。
并且由于我们支持xml类型的输出了,因此在内容协商的时候得到了服务端更多可支持的输出类型。
getProducibleMediaTypes()
获得服务端支持的输出类型由之前的4种:
变为了7种(虽然是10个,但有3个重复的)
多出来的application/xml
优先级肯定高于之前的application/json
这种,因此SpringBoot会按xml去解析。
这时我们就要问一句,为什么多出来了几种解析方式,我们点进getProducibleMediaTypes()
函数源码:
其源码思路比较简单,就是遍历每个HttpMessageConverter
,判断其是否支持写入,如果支持写入的话,顺便也将其支持的mediaType拿到,所有支持写入的HttpMessageConverter
对应的mediaType集合就是服务器支持的medisType。
不同的是由于xml解析依赖的导入,现在SpringBoot的messageConverters
集合多了两种类型(同时也少了一个):
这俩是同一个类,都是MappingJackson2XmlHttpMessageConverter
,它们是用于支持xml写出的,它们支持的mediaType为:
共三个mediaType:
application/xml;charset=UTF-8
text/xml;charset=UTF-8
application/*+xml;charset=UTF-8
也就是我们上面截图多出来的那三个。
根据排序后,由于xml优先级高于json,自然selectMedia
就是application/xhtml+xml
能处理application/xhtml+xml
类型的消息解析器自然是新加进来的MappingJackson2XmlHttpMessageConverter
。
在这里就可以看到虽然后端业务代码同样返回的是User对象,但由于HTTP请求Accept字段的不同,就可以解析为不同的格式,有人想要json就将Accept写为application/json,有人想要xml就将Accept写为application/xhtml+xml
这一做法的应用场景还是非常多的,比如不同的客户端想要不同的数据结构,浏览器想要json格式,而app想要xml格式,客户端只需要在自己的请求头的accept字段修改接收的数据类型或给要接收的数据类型排较高权重即可实现自适应返回数据。
还有类似的应用场景,比如我们自己写一个消息转化器,将返回的结果转为excel表格作为导出。同一个查询接口,前端可以根据请求accepet的不同,将json设置最高,可以查回来json结构直接展示。也可以将Accept设置为excel文件(自定义的mediaType),此时后端就会使用自定义消息转化器按文件输出。
刚才我们说了请求端可以修改accept的属性来决定返回类型,但有时修改accept会比较麻烦,比如对于表单提交。SpringBoot针对这种情况给出了可以通过请求参数来获得客户端想返回的数据类型,通过属性spring.mvc.contentnegotiation.favorParameter
来开启这一功能
此时我们只需要在请求参数中加上format参数,即可指定客户端想要的数据类型,如:
ip:port/user?format=xml
代表以xml形式返回
ip:port/user?format=json
代表以json形式返回
4.3 为什么导入XML依赖就多了xml的消息转化器
这里再补充一个细节,我们之前看到,在项目启动的时候,messageConverters
内就已经加载了很多实现好的消息转化器,并且当我们导入jackson-dataformat-xml
依赖时,又会自动增加xml的消息转化器,这是怎么做到的?
首先根据SpringBoot的自动装配我们知道,有关Spring Mvc的所有装配都在WebMvcAutoConfiguration
下,在这个类下。在这个类下有一个继承自WebMvcConfigurer
的方法
在这里SpringBoot装配进了一些消息转化器,其中customConverters.getConverters()
源码为:
这里的converters
属性是在构造方法中传进来的,也即一开始new的时候构造好的,构造方法如下:
可以看到构造方法会执行一个叫getDefaultConverters()
方法,这个方法会获得默认的HttpMessageConverter
其中getDefaultConverters()
又会调用super.getMessageConverters();
上面的代码又会调用addDefaultHttpMessageConverters(this.messageConverters);
从上面的代码可以看到默认情况下导入了ByteArrayHttpMessageConverter
、StringHttpMessageConverter
、ResourceHttpMessageConverter
、ResourceRegionHttpMessageConverter
、AllEncompassingFormHttpMessageConverter
,这些我们都在之前见到了,还有一些需要根据条件判断是否应该导入的,比如MappingJackson2HttpMessageConverter
、MappingJackson2XmlHttpMessageConverter
。
我们之前是导入了xml解析包,就自动添加了MappingJackson2XmlHttpMessageConverter
消息转化器,这是因为此时jackson2XmlPresent
属性为true,而jackson2XmlPresent
属性的判断逻辑是:
很简单,就是判断类com.fasterxml.jackson.dataformat.xml.XmlMapper
是否存在,如果存在jackson2XmlPresent
就为true,从而MappingJackson2XmlHttpMessageConverter
就会被创建和加载。
4.4 自定义HttpMessageConverter
我们在上一章@RequestBody
中已经讲了自定义HttpMessageConverter,那时我们自定义了一个消息转化器,只不过它是用来解析HTTP请求的,现在我们需要自定义一个解析器是处理返回HTTP响应的。我们可以看下之前定的自定义消息转化器:
上面的canWrite()
、write()
和getSupportedMediaTypes()
都是写出的时候需要实现的,我们现在就来实现:
同样我们自定义一种mediaType叫application/coderzoe
,然后自定义消息转化器,将Java对象按application/coderzoe
的格式写出:
同理,加这个convert加入到HttpMessageConverters
中:
我们依然使用application/coderZoe
的content-type方式提交,但同时将Accept也设置为application/coderZoe
:
此时,我们的返回结果为:
可以看到数据确实按照我们想要的结果类型返回了,也就是根据我们能处理的MedisType走到了我们自定义的消息转化器。
如果不想修改Http头的Accept信息,而是像我们之前那样,从参数中的format来决定Media类型,之前通过yml配置开启的方式在这种情况下已经不适用,因为那种方式只支持format=json和format=xml两种类型,我们现在想要类似于format=coderZoe这种类型,此时就需要自定义内容协商策略。
这种情况下,我们就可以通过ip:port/user?format=coderzoe
的形式走到自定义消息转化器了
但这种方式有个问题,就是我们自定义的消息转化器会覆盖了SpringBoot自带的消息转化器,那么此时在协议头中的Accept等信息都无法处理了,这肯定不是我们想看到的,一种比较危险的办法是我们自己再把它new出来加进去:
还一种比较简单,没有心里负担的做法:
这种情况也是可以的,不过需要我们开启spring.mvc.contentnegotiation.favorParameter
这种就是在默认的ParameterContentNegotiationStrategy
中添加支持的mediaType,在原来支持的xml和json里又加入coderZoe。
还有一种更简单的方案,ContentNegotiationConfigurer
中的mediaTypes
支持yml配置,也即我们只需要:
即可。
4.5 源码优化
在前面的源码分析中,其实是可以看到一个SpringMVC的优化点的,在说如何优化前我们先说回内容协商:
内容协商的时候,会执行
语句,这条语句会得到当前服务器端支持返回的mediaType。其源码如下:
可以看到就是遍历所有的HttpMessageConverter
实例,判断其是否支持将当前返回值写出,如果支持就将这个HttpMessageConverter
对应的mediaType记录起来,然后汇总返回,这个汇总结果就是当前服务器端支持的返回类型。
在内容协商后,我们拿到了要输出的mediaType理论上就该使用HttpMessageConverter
将信息写出,此时SpringMVC的做法如下:
依然是遍历所有的HttpMessageConverter
,判断其是否支持写出,支持再调用写出函数转化和写出结果。
其实这里很多人已经看明白了,上一步内容协商的时候已经遍历过所有的HttpMessageConverter
了,这其中有些是支持写出,有些是不支持的,将支持写出的HttpMessageConverter
对应的mediaType汇总起来。再从这些汇总后候选的mediaType中选出一个合适的mediaType作为写出类型,再选择一个能处理这个mediaType的HttpMessageConverter
写出。这时第二遍就不需要再遍历所有的mediaType,直接遍历第一遍支持写出的HttpMessageConverter
结果就可以了,相当于第一遍做个初筛选,第二遍做可以在初筛选的结果上再遍历得到具体的那个HttpMessageConverter
做转化,而无需在第二次时再遍历所有的mediaType。甚至如果mediaType与HttpMessageConverter
具有映射关系,可以将第一步的初筛结果转为map,key是支持的mediaType,value是HttpMessageConverter
,内容协商后可以直接通过key拿到HttpMessageConverter
,时间复杂度O(1),无需二次遍历。
8 条评论
虽然也跟着视频老师敲了,但是敲完一回想,发现还是有很多细节不懂,博主这里的总结真的很好,文字版本的加上博主自己的很多见解很方便我们去理解阅读,泰裤辣!!!
感谢肯定,能帮到你就好
看了两三天了,这最后有没看懂
可以照着文章的案例,在源码里一点点打断看。多打几遍断点走几遍就很容易看懂了。
真的牛,已经完全看不懂了
是的,这篇文章要比上一篇更琐碎和麻烦些,我写的时候默认读者对第一篇的内容已经掌握的非常熟练了,如果掌握的没那么好,可能这篇文章看起来会吃力 一些。
真的牛,萌新被镇住了
感谢肯定