RestFul 风格

装配原理

装配 SpringMVC 的时候就已经装配了 HiddenHttpMethodFilter,我们只需要在配置文件中开启使用即可。

1
2
3
4
5
6
7
8
9
@Bean
@ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
@ConditionalOnProperty(
prefix = "spring.mvc.hiddenmethod.filter",
name = {"enabled"}
)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}

我们就需要配置:

1
2
3
4
5
spring:
mvc:
hiddenmethod:
filter:
enabled: true #开启页面表单的Rest功能

HidenHttpMethodFilter

本质是是一个包装器模式,通过重写了 HttpRequest 的 getMethod() 方法,检测如果有 _method=XXX 参数并且 XXX 为允许的 PUT DELETE BATCH 三种请求,就将 getMethod() 的返回值替换成对应的请求。

这是为了解决 HTML 只能发起 GET 和 POST 请求的弊端,对于非静态页面(JavaScript),则可以发送全部请求,所以这个功能是选开的。

注解更换

SpringMVC 有多了新的注解,用来代替 @RequestMapping,分别是:

  1. @GetMapping
  2. @PostMapping
  3. @PutMapping
  4. @DeleteMapping

请求处理

详细的处理流程和 SpringMVC 是相同的,都是交给了 DispatcherServlet 来解决,所以我们可以完全按照 SpringMVC 的流程来进行请求处理:

基本注解

  1. @PathVariable 不指定名字的时候用 Map 接受所有的路径变量。
  2. @RequestHeader 不指定名字的时候用 Map 接受所有的请求头。
  3. @ModelAttribute 操作隐含模型,标注在属性获取数据,标注在方法上存入数据
  4. @RequestParam 不指定名字的时候用 Map 接受所有的请求参数。
  5. @RequestBody 获取请求体内所有数据。
  6. @CookieValue 加名字接收 Cookie 的 String 值或 Cookie 对象,不加名字没法用。
  7. @RequestAttribute 必须指定名字来获取指定的请求域对象的值,不支持 Map 全都拿走(为啥啊?)
  8. @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=valuekey=value1,value2 作为一个参数,可以有若干个以 ; 开头的参数,他们组成了一个维度的矩阵变量。而 ; 之前的字符串则是作为这个维度的路径变量,可以用 @PathVariable 来捕获。具体例子可以看下面的 Demo。

这就是矩阵变量,有两个维度一个是性别维度,一个是个人信息的维度。而 @MatrixVariable 的作用就是从矩阵变量中提取信息。

因为 SpringMVC 的 urlPathHelper 的属性 removeSemicolonContent 设置为了 true,也就是将 ; 给移除掉了,导致无法使用矩阵变量,我们首先需要设置为 false 才可以,方式就是将生成 urlPathHelper 对象的 WebMvcConfigurer 配置类的生成 urlPathHelper 方法自己实现。

WebMvcConfigurer 是一个有默认实现的接口(Java 8 新特性),我们只需要实现 WebMvcConfigurer 并单独重写 configurePathMath() 方法即可,然后扔到 IOC 容器中,会由于 @Conditional 系类注解导致不加载原来的,然后被使用的配置就是我们自己放入的 IOC 容器的这个:

1
2
3
4
5
6
7
8
9
10
11
@Bean //在 IOC 容器中配置自定义 WebMvcConfigurer 
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override //因为默认实现机制,我们只需要重写一个方法就能实现接口
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}

然后我们就可以这样捕获上面的 URL(http://localhost:8080/Matrix/Boy;name=Xorex;hobby=coding,eat,sleep/Girl;name=yukino;hobby=play,outgoing,love) 了:

使用 pathVar 来区分维度,value 选定参数,两者确定了矩阵变量里面的一个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RequestMapping("/Matrix/{Boy}/{Girl}")
public Map<String, Object> getMatrix(@MatrixVariable(pathVar = "Boy",value = "name") String boyName,
@MatrixVariable(pathVar = "Boy",value = "hobby") List<String> boyHobbys,
@MatrixVariable(pathVar = "Girl",value = "name") String girlName,
@MatrixVariable(pathVar = "Girl",value = "hobby") List<String> girlHobbys,
@PathVariable Map<String,String> paths) {
HashMap<String, Object> map = new HashMap<>();
map.put("boyName", boyName);
map.put("boyHobbys", boyHobbys);
map.put("girlName", girlName);
map.put("girlHobbys", girlHobbys);
map.put("pathMap", paths);
return map;
}

结果:

1
2
3
4
5
6
7
8
9
10
{
"girlName": "yukino",
"girlHobbys": ["play", "outgoing", "love"],
"boyHobbys": ["coding", "eat", "sleep"],
"pathMap": {
"Boy": "Boy",
"Girl": "Girl"
},
"boyName": "Xorex"
}

方法参数/返回参数

源码分析

众所周知,SpringMVC 对于一个请求的处理通过 Adapter 来实现的,以 RequestMappingHandlerAdapter 来说,他就是请求和 @RequestMapping 标注方法之间的桥梁。

我们就以最常用的 RequestMappingHandlerAdapter 来分析一下,SpringMVC 是如何处理请求参数的:

首先调用 RequestMappingHandlerAdapter 的 handle() 来处理请求之后,会来到:

handleInternal() 执行完一些对于缓存和 Session 的判断之后,进入:

invokeHandlerMethod() 这里首先会获取用来处理方法参数的 HandlerMethodArgumentResolvers 和处理方法返回值的 HandlerMethodReturnValueHandlers。

我们来看看它们,都是用来处理每一个不同的参数的(看名字就知道了):

323.jpg

然后进入 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
2
3
4
5
6
7
8
9
10
11
12
exposeModelAsReqeustAttributes(model,request);

//这个方法也非常简单,就是单纯的将 Model 放入了 Request 中:
protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception {
model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
} else {
request.removeAttribute(name);
}
});
}

这就是我们在操作的 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
2
3
4
5
6
7
8
9
10
11
12
@Configuration // 这就等价于在 applicationContext.xml 中配置 IOC 容器内容了
public class Config {
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new MyConvertor<String,Address>());
}
};
}
}

需要注意的是,在封装过程中,ModelAndView 也会保存一个被封装 POJO 的引用。

返回内容

理论上的方法返回处理都比较清楚了,也是枚举返回类型的 Processor,找到一个合适的去处理它,看名字就知道是处理哪种类型的返回值了:

324.jpg

从这些 Processor 可以看到 SpringMVC 支持的范围值类型为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//认识的
ModelAndView
Model
String
Map
View
ResponseEntity
HttpEntity
HttpHeaders
@ModelAttribute 标注的类

//不认识的
ResponseBodyEmitter
StreamingResponseBody
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask

这里我们主要讨论的是几个常用的 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
2
3
4
Spring:
mvc:
contentnegotiation:
favor-parameter: true

然后就会自动注册一个 strategie (协商策略),优先级高于原本的哪个(从 Accept 请求头解析协商内容)。这个 strategie 会从请求参数中找 format=XXX 然后就是返回要接受的请求类型了。经过 MessageConverters 遍历之后,找到合适的并处理。

其中 format=XXX 接收两个参数:json 或者 xml。