SpringBoot-04-动态资源处理
RestFul 风格
装配原理
装配 SpringMVC 的时候就已经装配了 HiddenHttpMethodFilter,我们只需要在配置文件中开启使用即可。
1 |
|
我们就需要配置:
1 | spring: |
HidenHttpMethodFilter
本质是是一个包装器模式,通过重写了 HttpRequest 的 getMethod() 方法,检测如果有 _method=XXX
参数并且 XXX 为允许的 PUT DELETE BATCH 三种请求,就将 getMethod() 的返回值替换成对应的请求。
这是为了解决 HTML 只能发起 GET 和 POST 请求的弊端,对于非静态页面(JavaScript),则可以发送全部请求,所以这个功能是选开的。
注解更换
SpringMVC 有多了新的注解,用来代替 @RequestMapping
,分别是:
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
请求处理
详细的处理流程和 SpringMVC 是相同的,都是交给了 DispatcherServlet 来解决,所以我们可以完全按照 SpringMVC 的流程来进行请求处理:
基本注解
- @PathVariable 不指定名字的时候用 Map 接受所有的路径变量。
- @RequestHeader 不指定名字的时候用 Map 接受所有的请求头。
- @ModelAttribute 操作隐含模型,标注在属性获取数据,标注在方法上存入数据
- @RequestParam 不指定名字的时候用 Map 接受所有的请求参数。
- @RequestBody 获取请求体内所有数据。
- @CookieValue 加名字接收 Cookie 的 String 值或 Cookie 对象,不加名字没法用。
- @RequestAttribute 必须指定名字来获取指定的请求域对象的值,不支持 Map 全都拿走(为啥啊?)
- @MatrixVariable 获取矩阵变量的值。
矩阵变量
首先需要明确一个矩阵变量的概念,对于普通的变量来说只能 ?key1=value1&key2=value2
这样通过字符串传递参数,而这些参数是一维的,是只能一个 key 对应一个 value 的。
矩阵变量的意义是将字符串传递参数从一维变为二维,而多的一个维度就通过路径分隔实现的,也是通过 key=value 的形式表示一组参数,不同的是同一个维度(路径分割下)的参数通过前缀 ;
隔开,一个 key 的多个 value 可以用 ,
隔开。
http://localhost:8080/Matrix/Boy;name=Xorex;hobby=coding,eat,sleep/Girl;name=yukino;hobby=play,outgoing,love
需要注意的是,对于一个 /{}/
之间的内容来说,;
开头之后的一组 key=value
或 key=value1,value2
作为一个参数,可以有若干个以 ;
开头的参数,他们组成了一个维度的矩阵变量。而 ;
之前的字符串则是作为这个维度的路径变量,可以用 @PathVariable 来捕获。具体例子可以看下面的 Demo。
这就是矩阵变量,有两个维度一个是性别维度,一个是个人信息的维度。而 @MatrixVariable 的作用就是从矩阵变量中提取信息。
因为 SpringMVC 的 urlPathHelper 的属性 removeSemicolonContent 设置为了 true,也就是将 ;
给移除掉了,导致无法使用矩阵变量,我们首先需要设置为 false 才可以,方式就是将生成 urlPathHelper 对象的 WebMvcConfigurer 配置类的生成 urlPathHelper 方法自己实现。
WebMvcConfigurer 是一个有默认实现的接口(Java 8 新特性),我们只需要实现 WebMvcConfigurer 并单独重写 configurePathMath() 方法即可,然后扔到 IOC 容器中,会由于 @Conditional 系类注解导致不加载原来的,然后被使用的配置就是我们自己放入的 IOC 容器的这个:
1 | //在 IOC 容器中配置自定义 WebMvcConfigurer |
然后我们就可以这样捕获上面的 URL(http://localhost:8080/Matrix/Boy;name=Xorex;hobby=coding,eat,sleep/Girl;name=yukino;hobby=play,outgoing,love
) 了:
使用 pathVar 来区分维度,value 选定参数,两者确定了矩阵变量里面的一个参数:
1 |
|
结果:
1 | { |
方法参数/返回参数
源码分析
众所周知,SpringMVC 对于一个请求的处理通过 Adapter 来实现的,以 RequestMappingHandlerAdapter 来说,他就是请求和 @RequestMapping 标注方法之间的桥梁。
我们就以最常用的 RequestMappingHandlerAdapter 来分析一下,SpringMVC 是如何处理请求参数的:
首先调用 RequestMappingHandlerAdapter 的 handle() 来处理请求之后,会来到:
handleInternal() 执行完一些对于缓存和 Session 的判断之后,进入:
invokeHandlerMethod() 这里首先会获取用来处理方法参数的 HandlerMethodArgumentResolvers 和处理方法返回值的 HandlerMethodReturnValueHandlers。
我们来看看它们,都是用来处理每一个不同的参数的(看名字就知道了):
然后进入 invokeAndHandle() 方法,再进入 invokeForRequest() 方法,第一步是调用 getMethodArgumentValues() 来获取方法参数,这里就是对方法参数的处理:
具体过程还是那个样子,双重循环,遍历所有的参数,同时遍历所有的参数解析器,依次尝试,直到确定某个参数可以被某个解析器解析。
Servlet API
对于 WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId 这些 Servlet 的原生 API,作为处理方法的参数的时候,会被 ServletRequestMethodArgumentResolver
解析器处理。
复杂参数
Map Model ModelAndView
当 Map,Model,ModelAndView(本质上还是包含了 Model)作为参数,会分别被 MapMethodProcessor 和 ModelMethodProcessor 处理(没听错,参数用的不是 Resolver 而是 Processor,是因为这两个东西虽然在参数的位置,但实际上干的是返回值的事情)。
这两个处理器其实本质上都是一样的,他们里面保存的东西都会被放到同一个地方:ModelAndViewContainer 里面的 BingAwareModelMap 中。这个 ModelAndViewContainer 是保存模型和视图数据的对象。
重重重重点来了,经过了一系列的后续处理之后,最终回到页面渲染的部分,在渲染之前,有一句话:
1 | exposeModelAsReqeustAttributes(model,request); |
这就是我们在操作的 Model 和 Map 到底发生了什么了。
POJO 封装
这里指的是将大量的 key=value 封装到一个 POJO 中。
源码是首先判断枚举 Resolvers 来判断哪一个可以处理这个参数。其中 ServletModelAttributeMethodProcessor 可以处理。这个 Processor 判断可以处理的依据就是参数没有注解并且不是一个简单参数(也就是非基础类型+枚举类+数组+Date+URL+URI+Local等等)。
两者都满足之后就返回 ServletModelAttributeMethodProcessor 来处理封装 POJO,下面介绍一下封装流程。
首先创建一个空的 POJO 对象,然后调用 createBinder() 并传入 POJO 对象和请求数据,来获取一个 WebDataBindaer 这个数据绑定器的作用就是将请求的参数绑定到 POJO 里面的属性中。
剩下的过程也就很熟悉了,依次枚举 POJO 需要封装的属性,然后枚举 Converters 转换器,直到可以完成请求的 String 到对应的属性类型,然后调用 Converters 进行转化并赋值。
大概流程就是这些,因为里面内容过于复杂,所以就不自己粘贴源码之类的了。
对于 POJO 里面包含自定义类型的,就需要自己实现 Converter<S,T>
接口了,具体接口实现流程可以参考 SpringMVC 里面的相关章节,这里只介绍实现之后的配置:
因为 converters 可以通过 WebMvcConfigurer 类里面的 addFormatters() 方法里面使用 FormatterRegistry.addConverter() 方法将自定义的 Converter<S,T>
添加到配置中的 converters 即可。
1 | // 这就等价于在 applicationContext.xml 中配置 IOC 容器内容了 |
需要注意的是,在封装过程中,ModelAndView 也会保存一个被封装 POJO 的引用。
返回内容
理论上的方法返回处理都比较清楚了,也是枚举返回类型的 Processor,找到一个合适的去处理它,看名字就知道是处理哪种类型的返回值了:
从这些 Processor 可以看到 SpringMVC 支持的范围值类型为:
1 | //认识的 |
这里我们主要讨论的是几个常用的 Processor 内部的一些细节,对于我们最常用的 Json 数据返回来说,通过 @RequestBody 来返回 POJO 转化为 Json 数据的 Processor 就是 RequestResponseBodyMethodProcessor 。
而这个处理类的核心就是在 handleReturnValue() 方法中的 writeWithMessageConverters() 。在这里干了什么事情呢,就是找 Converters,要将信息作为 ResponseBody 写出去,就要找到将返回值转化为 String 的 Converter。而除了找到能处理的 Converter,还要找到浏览器想要的 Converter。
这就要说说内容协商了:
首先浏览器会发请求的时候,会有一个请求头 Accept:
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=0.9),服务器接收到之后,会解析出来所有的内容,然后一个二重循环遍历所有的 MessageConverters,找到能转化处理并且转化内容优先级更高的 MassageConverter。
而我们在前后端分离开发的时候,一般都是 MappingJackson2HttpMessageConverter 这个转换器,将 POJO 转化为 JSON。
基于请求参数的内容协商
不在 accept 找协商,而是请求参数 format=XXX
中确定数据想要的响应内容,只需要开启配置:
1 | Spring: |
然后就会自动注册一个 strategie (协商策略),优先级高于原本的哪个(从 Accept 请求头解析协商内容)。这个 strategie 会从请求参数中找 format=XXX
然后就是返回要接受的请求类型了。经过 MessageConverters 遍历之后,找到合适的并处理。
其中 format=XXX
接收两个参数:json 或者 xml。