17 KiB
SpringMVC
SpringMVC简介
SpringMVC是一款基于Servlet API的web框架,并基于前端控制器的模式被设计。前端控制器有一个中央的控制器(DispatcherServlet),通过一个共享的算法来集中分发请求,请求实际是通过可配置的委托组件(@Controller类)来处理的。
Spring Boot遵循不同的初始化流程。相较于hooking到servlet容器的生命周期中,Spring Boot使用Spring配置来启动其自身和内置servlet容器。filter和servlet声明在在spring配置中被找到并且被注入到容器中。
DispatcherServlet
Context层次结构
DispatcherServlet需要一个WebApplicationContext(ApplicationContext的拓展类,Spirng容器)来进行配置。WebApplicationContext拥有一个指向ServletContext和与ServletContext关联Servlet的链接。
同样的,也可以通过ServeltContext来获取关联的ApplicationContext,可以通过RequestContextUtils中的静态方法来获取ServletContext关联的ApplicationContext。
WebApplicationContext的继承关系
对大多数应用,含有一个WebApplicationContext就足够了。也可以存在一个Root WebApplicationContext在多个DispatcherServlet实例之间共享,同时DispatcherServlet实例也含有自己的WebApplicationContext。
通常,被共享的root WebApplicationContext含有repository或service的bean对象,这些bean对象可以被多个DispatcherServlet实例的子WebApplicationContext共享。同时,子WebApplicationContext在继承root WebApplicationContext中bean对象的同时,还能对root WebApplicationContext中的bean对象进行覆盖。
WebApplicationContext继承机制
只有当Servlet私有的子WebApplicationContext中没有找到bean对象时,才会从root WebApplicationContext中查找bean对象,此行为即是对root WebApplicationContext的继承
Spring MVC中特殊的bean类型
DispatcherServlet将处理请求和渲染返回的工作委托给特定的bean对象。Spring MVC中核心的bean类型如下。
HandlerMapping
将请求映射到handler和一系列用于pre/post处理的拦截器。
HandlerAdapter
HandlerAdapter主要是用于帮助DispatcherServlet调用request请求映射到的handler对象。
通常,在调用含有注解的controller时需要对注解进行解析,而HandlerAdapter可以向DispatcherServlet隐藏这些细节,DispatcherServlet不必关心handler是如何被调用的。
HandlerExceptionResolver
解析异常的策略,可能将异常映射到某个handler,或是映射到html error页面。
ViewResolver
将handler返回的基于字符串的view名称映射到一个特定的view,并通过view来渲染返回的响应。
Web MVC Config
在应用中可以定义上小节中包含的特殊类型bean对象。DispatcherServlet会检查WebApplicationContext中存在的特殊类型bean对象,如果某特殊类型的bean对象在容器中不存在,那么会存在一个回滚机制,使用DispatcherServlet.properties中默认的bean类型来创造bean对象(例如,DispatcherServlet.properties中指定的默认HandlerMapping类型是 org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping和org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.)。
Servlet Config
在Servlet环境中,可以选择通过java代码的方式或者web.xml的方式来配置servlet容器。
配置Servlet的详细方式,参照Spring MVC文档。
请求处理过程
DispatcherServlet按照如下方式来处理Http请求
- 首先,会找到WebApplicationContext并且将其绑定到请求中,此后controller和其他元素在请求处理的过程中可以使用该WebApplicationContext。默认情况下,WebApplicationContext被绑定到DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE属性中
- locale resolver被绑定到请求中,在请求处理过程中可以被其他元素使用
- theme resolver被绑定到请求中,在请求处理过程中可以被其他元素使用
- 如果指定了multipart file resolver,请求会被检查是否存在multipart。如果multipart被找到,该请求会被包装在一个MultipartHttpServletRequest中并等待对其进行进一步的处理
- 一个合适的handler将会被找到,并且和该handler相关联的execution chain(pre/post/Controller)会被执行,返回一个model用于渲染。
- 如果model被返回,那么返回的model会被渲染。如果model没有返回,那么没有view会被渲染。
在WebApplicationContext中声明的HandlerExceptionResolver会用于解析处理请求中抛出的异常。
路径匹配
Servlet API将完整的请求路径暴露为requestURI,并且进一步划分requestURI为如下部分:contextPath,servletPath,pathInfo。
contextPath, servletPath, pathInfo区别
- contextPath:contextPath为应用被访问的路径,是整个应用的根路径。默认情况下,SpringBoot的contextPath为"/"。可以通过server.servlet.context-path="/demo"来改变应用的根路径。
- servletPath:servletPath代表main DispatcherServlet的路径。默认情况下,servletPath的值仍为"/"。可以通过spring.mvc.servlet.path来自定义该值。
Interception
所有HandlerMapping的实现类都支持handler interceptor,当想要为特定请求执行指定逻辑时会相当有用。拦截器必须要实现HandlerInterceptor,该接口提供了三个方法:
- preHandle:在实际handler处理之前
- postHandle:在 实际handler处理之后
- afterCompletion:在请求完成之后
preHandle
preHandle方法会返回一个boolean值,可以通过指定该方法的返回值来阻止执行链继续执行。当preHandle的返回值是true时,后续执行链会继续执行,当返回值是false时,DispatcherServlet会假设该拦截器本身已经处理了请求,并不会继续执行execution chain中的其他拦截器和实际handler。
postHandle
对于@ResponseBody或返回值为ResponseEntity的方法,postHandle不起作用,这些方法在HandlerAdapter中已经写入且提交了返回响应,时间位于postHandle之前。到postHandle方法执行时,已经无法再对响应做任何修改,如添加header也不再被允许。对于这些场景,可以实现ResponseBodyAdvice或者声明一个ControllerAdvice。
afterCompletion
调用时机位于DispaterServlet渲染view之后。
Exceptions
如果异常在request mapping过程中被抛出或者从request handler中被抛出,DispatcherServlet会将改异常委托给HandlerExceptionResolver chain来处理,通常是返回一个异常响应。
如下是可选的HandlerExceptionResolver实现类:
- SimpleMappingExceptionResolver:一个从exception class name到error view name的映射,用于渲染错误页面
- DefaultHandlerExceptionResolver:解析Spring MVC抛出的异常,并且将它们映射为Http状态码
- ResponseStatusExceptionResolver:通过@ResponseStatus注解来指定异常的状态码,并且将异常映射为Http状态码,状态码的值为@ResponseStatus注解指定的值
- ExceptionHandlerExceptionResolver:通过@Controller类内@ExceptionHandler方法或@ControllerAdvice类来解析异常
Resolver Chain
可以声明多个HandlerExceptionResolver bean对象来声明一个Exception Resolver链,并可以通过指定order属性来指定resolver chain中的顺序,order越大,resolver在链中的顺序越靠后。
exception resolver的返回规范
HandlerExceptionResolver返回值可以按照如下规范返回:
- 一个指向ModelAndView的error view
- 一个空的ModelAndView,代表异常已经在该Resolver中被处理
- null,代表该exception仍未被解析,resolver chain中后续的resolver会继续尝试处理该exception,如果exception在chain的末尾仍然存在,该异常会被冒泡到servlet container中
container error page
如果exception直到resolver chain的最后都没有被解析,或者,response code被设置为错误的状态码(如4xx,5xx),servlet container会渲染一个默认的error html page。
视图解析
处理
类似于Exception Resolver,也可以声明一个view resolver chain,并且可以通过设置order属性来设置view resolver在resolver chain中的顺序。
view resolver返回null时,代表该view无法被找到。
重定向
以“redirect:”前缀开头的view name允许执行重定向,redirect:之后指定的是重定向的url。
# 重定向实例如下
# 1. 基于servlet context重定向
redirect:/myapp/some/resource
# 2. 基于绝对路径进行重定向
redirect:https://myhost.com/some/arbitrary/path
如果controller的方法用@ResponseStatus注解标识,该注解值的优先级高于RedirectView返回的response status。
转发
以“forward:”前缀开头的view name会被转发。
Controller
AOP代理
在某些时候,可能需要AOP代理来装饰Controller,对于Controller AOP,推荐使用基于class的aop。
例如想要为@Controller注解的类添加@Transactional注解,此时需要手动指定@Transactional注解的proxyTargetClass=true来启用基于class的动态代理。
当为@Transactional注解指定了proxyTargetClass=true之后,其不光会将@Transactional的代理变为基于cglib的,还会将整个context中所有的autoproxy bean代理方式都变为基于cglib类代理的
Request Mapping
可以通过@RequestMapping来指定请求对应的url、http请求种类、请求参数、header和media type。
还可以使用如下注解来映射特定的http method:
- @GetMapping
- @PostMapping
- @DeleteMapping
- @PutMapping
- @PatchMapping
相对于上述的注解,@RequestMapping映射所有的http method。
URI Pattern
Spring MVC支持如下URI Pattern:
- /resources/ima?e.png : ?匹配一个字符
- /resources/*.png : *匹配0个或多个字符,但是位于同一个path segment内
- /resource/** : **可以匹配多个path segment(但是**只能用于末尾)
- /projects/{project}/versions : 匹配一个path segment,并且将该path segment捕获到一个变量中,变量可以通过@PathVariable访问
- /projects/{project:[a-z]+}/versions : 匹配一个path segment,并且该path segment需要满足指定的regex
{varName:regex}可以将符合regex的path segment捕获到varName变量中,例如:
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
// ...
}
Pattern Comparasion
如果多个pattern都匹配当前URI,那么最佳匹配将会被采用。
多个pattern中,更加具体的pattern会被采用。URI的计分方式如下:
- 每个URI变量计1分
- *符号计1分
- **符号计两分
- 如果分数相同,那么更长的pattern会被选择
- 如果分数和pattern length都相同,那么拥有比wildchar更多的的URI变量的模式会被选择
分数越高,那么pattern将会是更佳的匹配
消费media-type
可以在@RequestMapping中指定请求中的Content-Type类型来缩小请求映射范围
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
// ...
}
consumes属性还支持“非”的操作,如下所示:
// 其映射除text/plain之外的content-type
@PostMapping(path = "/pets", consumes = "!text/plain")
产生media-type
可以在@RequestMapping中指定produces属性来根据http请求header中的Accept属性来缩小映射范围
// 该方法会映射Accept属性为application/json的http请求
@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
Parameters & Headers
可以通过请求参数或header来缩小@RequestMapping的映射。
支持过滤条件为某参数是否存在、某参数值是否为预期值,header中某值是否存在、header中某值是否等于预期值。
// 参数是否存在 : myParam
// 参数是否不存在 : !myParam
// 参数是否为预期值 : myParam=myValue
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
public void findPet(@PathVariable String petId) {
// ...
}
// headers校验同样类似于params
@GetMapping(path = "/pets", headers = "myHeader=myValue")
public void findPet(@PathVariable String petId) {
// ...
}
handler method
类型转换
当handler方法的参数为非String类型时,需要进行类型转换。在此种场景下,类型转换是自动执行的。默认情况下,简单类型(int,long,Date,others)是被支持的。
可以通过WebDataBinder来进行自定义类型转换。
在执行类型转换时,空字符串""在转换为其他类型时可能被转化为null(如转化为long,int,Date).如果允许null被注入给参数,需要将参数注解的required属性指定为false,或者为参数指定@Nullable属性。
Matrix Variable
在Matrix Variable中,允许在uri中指定key-value对。path segment中允许包含key-value对,其中变量之间通过分号分隔,值如果存在多个,多个值之间通过逗号分隔。
/cars;color=red,green;year=2012
当URL中预期含有Matrix变量时,被映射方法的URI中必须含有一个URI Variable({var})来覆盖该Matrix变量(即通过{var}来捕获URL中的Matrix变量),以此确保URL能被正确的映射。例子如下所示
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
在所有的path segment中都有可能含有matrix variable,如果在不同path segment中含有相同名称的matrix variable,可以通过如下方式进行区分:
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable(name="q", pathVar="ownerId") int q1,
@MatrixVariable(name="q", pathVar="petId") int q2) {
// q1 == 11
// q2 == 22
}
matrix variable参数也可以被设置为可选的,并且也能为其指派一个默认值:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
如果要获取所有的Matrix Variable并且将其缓存到一个map中,可以通过如下方式
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable MultiValueMap<String, String> matrixVars,
@MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
@RequestParam
@RequestParam注解用于将http request中的param赋值给handler method的参数,使用方法如下:
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) {
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
当@RequestParam注解的参数类型不是String时,类型转换会被自动的执行。
将@RequestParam注解的参数类型指定为list或array时,会将具有相同param name的多个param value解析到其中。
可以用@RequestParam来注解一个Map<String,String>或MultiValValue<String,String>类型,不要指定@RequestParam属性,此时,map将会被自动注入。
@RequestHeader
可以通过@RequestHeader注解来将http header值赋值给handler method参数。
# http header
Host localhost:8080
Accept text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding gzip,deflate
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive 300
可以通过如下方式来获取header中的值:
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding,
@RequestHeader("Keep-Alive") long keepAlive) {
//...
}
同样的,如果被注解参数的类型不是String,类型转换将会被自动执行。
同样,也可以用@RequestHeader注解来标注Map<String,String>或MultiValueMap<String,String>或HttpHeaders类型参数,map会自动被header name和header value注入。
@CookieValue
可以通过@CookieValue注解将Http Cookie赋值给handler method参数,如果存在如下Cookie:
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
可以通过如下方式来获取cookie的值
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) {
//...
}
如果注解标注参数的类型不是String,会自动执行类型转换。