# 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请求 1. 首先,会找到WebApplicationContext并且将其绑定到请求中,此后controller和其他元素在请求处理的过程中可以使用该WebApplicationContext。**默认情况下,WebApplicationContext被绑定到DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE属性中** 2. locale resolver被绑定到请求中,在请求处理过程中可以被其他元素使用 3. theme resolver被绑定到请求中,在请求处理过程中可以被其他元素使用 4. 如果指定了multipart file resolver,请求会被检查是否存在multipart。如果multipart被找到,该请求会被包装在一个MultipartHttpServletRequest中并等待对其进行进一步的处理 5. 一个合适的handler将会被找到,并且和该handler相关联的execution chain(pre/post/Controller)会被执行,返回一个model用于渲染。 6. 如果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实现类: 1. SimpleMappingExceptionResolver:一个从exception class name到error view name的映射,用于渲染错误页面 2. DefaultHandlerExceptionResolver:解析Spring MVC抛出的异常,并且将它们映射为Http状态码 3. ResponseStatusExceptionResolver:通过@ResponseStatus注解来指定异常的状态码,并且将异常映射为Http状态码,状态码的值为@ResponseStatus注解指定的值 4. 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。 ```shell # 重定向实例如下 # 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变量中,例如: ```java @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类型来缩小请求映射范围 ```java @PostMapping(path = "/pets", consumes = "application/json") public void addPet(@RequestBody Pet pet) { // ... } ``` consumes属性还支持“非”的操作,如下所示: ```java // 其映射除text/plain之外的content-type @PostMapping(path = "/pets", consumes = "!text/plain") ``` #### 产生media-type 可以在@RequestMapping中指定produces属性来根据http请求header中的Accept属性来缩小映射范围 ```java // 该方法会映射Accept属性为application/json的http请求 @GetMapping(path = "/pets/{petId}", produces = "application/json") @ResponseBody public Pet getPet(@PathVariable String petId) { // ... } ``` #### Parameters & Headers 可以通过请求参数或header来缩小@RequestMapping的映射。 支持过滤条件为某参数是否存在、某参数值是否为预期值,header中某值是否存在、header中某值是否等于预期值。 ```java // 参数是否存在 : 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对,其中变量之间通过分号分隔,值如果存在多个,多个值之间通过逗号分隔。 ```shell /cars;color=red,green;year=2012 ``` 当URL中预期含有Matrix变量时,被映射方法的URI中必须含有一个URI Variable({var})来覆盖该Matrix变量(即通过{var}来捕获URL中的Matrix变量),以此确保URL能被正确的映射。例子如下所示 ```java // 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,可以通过如下方式进行区分: ```java // 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参数也可以被设置为可选的,并且也能为其指派一个默认值: ```java // GET /pets/42 @GetMapping("/pets/{petId}") public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) { // q == 1 } ``` 如果要获取所有的Matrix Variable并且将其缓存到一个map中,可以通过如下方式 ```java // GET /owners/42;q=11;r=12/pets/21;q=22;s=23 @GetMapping("/owners/{ownerId}/pets/{petId}") public void findPet( @MatrixVariable MultiValueMap matrixVars, @MatrixVariable(pathVar="petId") MultiValueMap petMatrixVars) { // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23] // petMatrixVars: ["q" : 22, "s" : 23] } ``` #### @RequestParam @RequestParam注解用于将http request中的param赋值给handler method的参数,使用方法如下: ```java @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或MultiValValue类型,不要指定@RequestParam属性,此时,map将会被自动注入。 #### @RequestHeader 可以通过@RequestHeader注解来将http header值赋值给handler method参数。 ```shell # 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中的值: ```java @GetMapping("/demo") public void handle( @RequestHeader("Accept-Encoding") String encoding, @RequestHeader("Keep-Alive") long keepAlive) { //... } ``` 同样的,如果被注解参数的类型不是String,类型转换将会被自动执行。 同样,也可以用@RequestHeader注解来标注Map或MultiValueMap或HttpHeaders类型参数,map会自动被header name和header value注入。 #### @CookieValue 可以通过@CookieValue注解将Http Cookie赋值给handler method参数,如果存在如下Cookie: ```cookie JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84 ``` 可以通过如下方式来获取cookie的值 ```java @GetMapping("/demo") public void handle(@CookieValue("JSESSIONID") String cookie) { //... } ``` 如果注解标注参数的类型不是String,会自动执行类型转换。 #### @ModelAttribute 可以为handler method的参数添加@ModelAttribute注解,在添加该注解之后,参数属性的值可以从Model中进行获取,如果model中不存在则对该属性进行初始化。 **Model属性会被http servlet param中的值覆盖,如果request param name和field name相同。** > @ModelAttribute注解,其name的默认值为方法参数或者返回值类型的首字母小写: > 例如,“orderAddress" for class "mypackage.OrderAddress" @ModelAttribute的使用具体可如下所示: ```java @PostMapping("/owners/{ownerId}/pets/{petId}/edit") public String processSubmit(@ModelAttribute Pet pet) { // method logic... } ``` 参数pet可以通过如下方式进行获取: 1. pet参数已经加上了@ModelAttribute属性,故而可以从model中进行获取 2. 如果该model attribute已经出现在类级别的@SessionAttribute中,则可以在session中进行获取 3. 如果model attribute name和request param name或者path variable name相匹配,那么可以通过converter来进行获取 4. 通过默认的构造器进行初始化 5. 通过primary constructor进行构造,主构造器参数和servlet request param相匹配 在使用@ModelAttribute时,可以创建一个Converter类型的bean对象用于类型转换,当model attribute name和path variable或者request param name相匹配,且Converter对象存在时,会调用该Converter进行类型转换: ```java @Component class StringPersonConverter implements Converter { @Override public Person convert(String source) { return new Person("kazusa",true,21); } } @RestController public class ModelAttributeController { @PostMapping("/hello") public Person sayHello(@ModelAttribute(name="name",binding=true) Person person, Model model) { System.out.println(model); return person; } } ``` 在model attribute被获取之后,会执行data binding。WebDataBinder会将request param name和目标对象的field进行匹配,匹配的field会通过类型转换进行注入。 如果想要访问Model Attribute但是不想使用data binding,可以直接在handler method参数中使用Model,或者将@ModelAttribute注解的binding属性设置为false。 ```java @PostMapping("update") public String update(@Valid AccountForm form, BindingResult result, @ModelAttribute(binding=false) Account account) { // ... } ``` #### @SessionAttributes @SessionAttributes注解用于将model attribute存储在http session中,从而在不同的请求间都可以访问该attribute。 @SessionAttributes为class-level的注解,注解的value应该列出model attribute name或model attribute的类型。 该注解使用如下所示: ```java @Controller @SessionAttributes("pet") public class EditPetForm { // ... @PostMapping("/pets/{id}") public String handle(Pet pet, BindingResult errors, SessionStatus status) { if (errors.hasErrors) { // ... } status.setComplete(); // ... } } ``` 当第一个请求到来时,name为pet的Model Attribute被添加到model中,其会被自动提升并保存到http session中,知道另一个controller method通过SessionStatus方法来清除存储。 #### @SessionAttribute 如果想要访问已经存在的Session Attribute,可以在handler method的参数上添加@SessionAttribute,如下所示: ```java @RequestMapping("/") public String handle(@SessionAttribute User user) { // ... } ``` #### Multipart 在MultipartResolver被启用之后,Post请求体中为multipart/form-data格式的数据将被转化并可以作为handler method的参数访问。如下显示了文件上传的用法: ```java @Controller public class FileUploadController { @PostMapping("/form") public String handleFormUpload(@RequestParam("name") String name, @RequestParam("file") MultipartFile file) { if (!file.isEmpty()) { byte[] bytes = file.getBytes(); // store the bytes somewhere return "redirect:uploadSuccess"; } return "redirect:uploadFailure"; } } ``` > 在上述代码中,可以将MultipartFile改为List\来解析多个文件名 > **当@RequestParam没有指定name属性并且参数类型为Map或MultiValueMap类型时,会根据name自动将所有匹配的MultipartFile注入到map中** #### @RequestBody 可以通过@RequestBody注解读取请求体内容,并且通过HttpMessageConverter将内容反序列化为Object。具体使用方法如下: ```java @PostMapping("/accounts") public void handle(@RequestBody Account account) { // ... } ``` #### HttpEntity 将参数类型指定为HttpEntity与使用@RequestBody注解类似,其作为一个容器包含了请求体转化后的对象(account实例)和请求头(HttpHeaders)。其使用如下: ```java @PostMapping("/accounts") public void handle(HttpEntity entity) { // ... } ``` #### @ResponseBody 通过使用@ResponseBody注解,可以将handler method的返回值序列化到响应体中,通过HttpMessageConverter。具体使用如下所示: ```java @GetMapping("/accounts/{id}") @ResponseBody public Account handle() { // ... } ``` 同样的,@ResponseBody注解也支持class-level级别的使用,在使用@ResponseBody标注类后对所有controller method都会起作用。 通过@RestController可以起到同样作用。 #### ResponseEntity ResponseEntity使用和@ResponseBody类似,但是含有status和headers。ResponseEntity使用如下所示: ```java @GetMapping("/something") public ResponseEntity handle() { String body = ... ; String etag = ... ; return ResponseEntity.ok().eTag(etag).body(body); } ``` #### Jackson JSON Spring MVC为Jackson序列化view提供了内置的支持,可以渲染对象中的字段子集。 如果想要在序列化返回值时仅仅序列化某些字段,可以通过@JsonView注解来指明序列化哪些字段。 ##### @JsonView @JsonView的使用如下所示, - 通过interface来指定渲染的视图 - 在字段的getter方法上通过@JsonView来标注视图类名 - interface支持继承,如WithPasswordView继承WithoutPasswordView,故而WithPasswordView在序列化时不仅会包含其自身的password字段,还会包含从WithoutPasswordView中继承而来的name字段 > 对于一个handler method,只能够通过@JsonView指定view class,如果想要激活多个视图,可以使用合成的view class ```java @RestController public class UserController { @GetMapping("/user") @JsonView(User.WithoutPasswordView.class) public User getUser() { return new User("eric", "7!jd#h23"); } } public class User { public interface WithoutPasswordView {}; public interface WithPasswordView extends WithoutPasswordView {}; private String username; private String password; public User() { } public User(String username, String password) { this.username = username; this.password = password; } @JsonView(WithoutPasswordView.class) public String getUsername() { return this.username; } @JsonView(WithPasswordView.class) public String getPassword() { return this.password; } } ``` ### Model #### @ModelAttribute注解用法 - 将@ModelAttribute注解标记在handler method的参数上,用于创建或访问一个对象,该对象从model中获取,并且该对象通过WebDataBinder与http request绑定在一起。 - 将@ModelAttribute注解绑定在位于@Controller或@ControllerAdvice类中的方法上,用于在任何handler method调用之前初始化model - 将@ModelAttribute注解绑定在@RequestMapping方法上,用于标注该方法的返回值是一个model attribute #### @ModelAttribute作用于Controller类中普通方法上 对于上述的第二种使用,一个controller类中可以含有任意数量个@ModelAttribute方法,所有的这些方法都会在@RequestMapping方法调用之前被调用。(**同一个@ModelAttribute方法,也可以通过@ControllerAdvice在多个controllers之间进行共享**)。 @ModelAttribute方法支持可变的参数形式,其参数形式可以和@RequestMapping方法中的参数形式一样。(**但是,@ModelAttribute方法的参数中不能含有@ModelAttribute注解本身,参数也不能含有和http请求体相关的内容**)。 ```java @ModelAttribute public void populateModel(@RequestParam String number, Model model) { model.addAttribute(accountRepository.findAccount(number)); // add more ... } ``` 也可以通过如下方式向model中添加attribute: ```java // 只向model中添加一个属性 @ModelAttribute public Account addAccount(@RequestParam String number) { return accountRepository.findAccount(number); } ``` > 在向model中添加属性时,如果attribute name没有显式指定,那么attribute name将会基于attribute value的类型来决定。可以通过model.addAttribute(attributeName,attributeValue)来指定attribute name,或者通过指定@ModelAttribute的name属性来指定attribute name #### @ModelAttribute作用于@RequestMapping方法上 对于第三种使用,可以将@ModelAttribute注解标注在@RequestMapping方法之上,在这种情况下方法的返回值将会被解释为model attribute。**在这种情况下@ModelAttribute注解不是必须的,应为该行为是html controller的默认行为,除非返回值是String类型,此时返回值会被解释为view name。** 可以通过如下方式来自定义返回model attribute的attribute name,如下图所示: ```java // 指定attribute name为“myAccount” @GetMapping("/accounts/{id}") @ModelAttribute("myAccount") public Account handle() { // ... return account; } ``` ### DataBinder 对于@Controller和@ControllerAdvice类,在类中可以包含@InitBinder方法,该方法用来初始化WebDataBinder实例: - 将请求参数绑定到model - 将基于String类型的请求值(例如请求参数,pathVariable,headers,cookies或其他)转化为controller method参数的类型 - 在html渲染时,将model object value转化为String的形式 @InitBinder可以针对特定的Controller注册java.beans.PropertyEditor或Spring Converter和Formatter 组件。 @InitBinder支持和@RequestMapping方法一样的参数形式,但是参数不能使用@ModelAttribute注解。通常,@InitBinder方法的参数为WebDataBinder,且返回类型为void: ```java @Controller public class FormController { // WebDataBinder参数用于注册 @InitBinder public void initBinder(WebDataBinder binder) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setLenient(false); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); } // ... } ``` 同样,当使用一个共享的FormattingConversionService来设置格式时,可以注册针对特定Controller的Formatter实现,例如: ```java @Controller public class FormController { @InitBinder protected void initBinder(WebDataBinder binder) { binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); } // ... } ``` ### Model Design 在web应用的上下文中,data binding涉及将http请求中的参数绑定到model object及其内层嵌套对象中。 默认情况下,所有spring允许绑到model object中所有的公共属性(有public的getter和setter)。 通常情况下,会自定义一个特定的model object类,并且该类中的public属性与表单中提交的参数相关联。 ```java // 只会对如下两个public属性进行data binding public class ChangeEmailForm { private String oldEmailAddress; private String newEmailAddress; public void setOldEmailAddress(String oldEmailAddress) { this.oldEmailAddress = oldEmailAddress; } public String getOldEmailAddress() { return this.oldEmailAddress; } public void setNewEmailAddress(String newEmailAddress) { this.newEmailAddress = newEmailAddress; } public String getNewEmailAddress() { return this.newEmailAddress; } } ``` ### Exception 在@Controller和@ControllerAdvice类中,可以含有@ExceptionHandler方法,该方法用于处理controller方法中抛出的异常,使用如下所示: ```java @Controller public class SimpleController { // ... @ExceptionHandler public ResponseEntity handle(IOException ex) { // ... } } ``` 该异常参数会匹配被抛出的顶层异常(例如,被直接抛出的IOException),也会匹配被包装的内层cause(例如,被包装在IllegalStateException中的IOException)。**该参数会匹配任一层级的cause exception,并不是只有该异常的直接cause exception才会被处理。** > 只要@ExceptionHandler方法的异常参数类型匹配异常抛出stacktrace中任一层次的异常类型,异常都会被捕获并且处理。 ```java @PostMapping("/shiro") public String shiro() { throw new RuntimeException(new JarException("fuck")); } @ExceptionHandler public String handleJarException(JarException e) { return e.getMessage() + ", Jar"; } ``` > 如果有多个@ExceptionHandler方法匹配抛出的异常链,那么root exception匹配会优先于cause exception匹配。 > ExceptionDepthComparator会根据各个异常类型相对于抛出异常类型(root exception)的深度来进行排序。 > ```java > // 在调用/shiro接口时,IOException离root exception(RuntimeException)更近, > // 故而会优先调用handleIOException方法 > @PostMapping("/shiro") > public String shiro() throws IOException { > throw new RuntimeException(new IOException(new SQLException("fuck"))); > } > > @ExceptionHandler > public String handleIOException(IOException e) { > return e.getMessage() + ", IO"; > } > > @ExceptionHandler > public String handleSQLException(SQLException e) { > return e.getMessage() + ",Exception"; > } > ``` 对于@ExceptionHandler,可以指定该方法处理的异常类型来缩小范围,如下方法都只会匹配root exception为指定异常或异常链中包含指定异常的场景: ```java @ExceptionHandler({FileSystemException.class, RemoteException.class}) public ResponseEntity handle(IOException ex) { // ... } // 甚至,可以将参数的异常类型指定为一个非常宽泛的类型,例如Exception @ExceptionHandler({FileSystemException.class, RemoteException.class}) public ResponseEntity handle(Exception ex) { // ... } ``` 上述两种写法的区别是: 1. 如果参数类型为IOException,那么当cause exception为FileSystemException或RemoteException,且root exception为IOException时,实际的cause exception要通过ex.getCause来获取 2. 如果参数类型为Exception,那么当cause exception为FileSystemException或RemoteException且root exception不为指定类型异常时,指定类型异常统一都通过ex.getCause来获取 > 通常情况下,@ExceptionHandler方法的参数类型应该尽可能的具体,而且推荐将一个宽泛的@ExceptionHandler拆分成多个独立的@ExceptionHandler方法,每个方法负责处理特定类型的异常。 **对于@ExceptionHandler方法,在捕获异常之后可以对异常进行rethrow操作,重新抛出之后的异常会在剩余的resolution chain中传播,就好像给定的@ExceptionHandler在开始就没有匹配** ### Controller 对于@ModelAttribute、@InitBinder、@ExceptionHandler注解,若其在@Controller类中使用,那么该类仅对其所在的@Controller类有效。 **如果这些注解定义在@ControllerAdvice或@RestControllerAdvice类中,那么它们对所有的@Controller类都有效。** **位于@ControllerAdvice类中的@ExceptionHandler方法,其可以用于处理任何@Controller类中抛出的异常或任何其他exception handler中抛出的异常。** > 对于@ControllerAdvice中的@ExceptionHandler方法,其会在其他所有位于@Controller类中的@ExceptionHandler方法执行完**之后**才会执行;而@InitBinder和@ModelAttribute方法,则是位于@ControllerAdive方法中的**优先于**位于@Controller类中的执行 > @ControllerAdive使用如下: ```java // Target all Controllers annotated with @RestController @ControllerAdvice(annotations = RestController.class) public class ExampleAdvice1 {} // Target all Controllers within specific packages @ControllerAdvice("org.example.controllers") public class ExampleAdvice2 {} // Target all Controllers assignable to specific classes @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) public class ExampleAdvice3 {} ``` ## CORS Spring MVC允许处理跨域问题。 > ### 跨域问题 > 处于对安全的考虑,浏览器禁止ajax访问非当前origin的资源。 > 对于简单请求,浏览器会直接通过请求和响应中特定header来判断当前资源是否能够被访问;对于非简单请求,则是在请求之前会发送一个预检请求(OPTIONS请求,判断当前资源能否被访问)。 在Spring MVC中,HandlerMapping实现提供了对CORS的支持。在实际将一个请求映射到handler后,HandlerMapping实现将会检查请求和handler的CORS配置并且做进一步的处理。 如果想要启用CORS,需要显式声明CORS配置,如果匹配的CORS配置未被找到,那么预检请求将会被拒绝。(CORS header将不会添加到简单请求和实际CORS请求的响应中,浏览器将会拒绝没有CORS header的响应)。 ### @CrossOrigin 可以对handler method使用@CrossOrigin注解以允许跨域请求,使用示例如下: ```java @RestController @RequestMapping("/account") public class AccountController { @CrossOrigin @GetMapping("/{id}") public Account retrieve(@PathVariable Long id) { // ... } @DeleteMapping("/{id}") public void remove(@PathVariable Long id) { // ... } } ``` 默认情况下,@CrossOrigin会允许来自所有origin、含有任意header和所有http method。 - allowCredentials默认情况下没有启用,除非allowOrigins或allowOriginPatterns被指定了一个非"*"的值。 - maxAge默认会设置为30min @CrossOrigin注解支持在类上使用,类上注解会被方法继承 ```java @CrossOrigin(origins = "https://domain2.com", maxAge = 3600) @RestController @RequestMapping("/account") public class AccountController { @GetMapping("/{id}") public Account retrieve(@PathVariable Long id) { // ... } @DeleteMapping("/{id}") public void remove(@PathVariable Long id) { // ... } } ``` @CrossOrigin注解可同时在类和方法上使用 ```java @CrossOrigin(maxAge = 3600) @RestController @RequestMapping("/account") public class AccountController { @CrossOrigin("https://domain2.com") @GetMapping("/{id}") public Account retrieve(@PathVariable Long id) { // ... } @DeleteMapping("/{id}") public void remove(@PathVariable Long id) { // ... } } ``` ### spring boot全局配置CORS 在spring boot中,可以通过如下方式全局配置CORS ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:4200") .allowedMethods("*") .allowedHeaders("*") .allowCredentials(false) .maxAge(3600); } } ```