Files
rikako-note/spring/Spring core/SpringMVC.md
wu xiangkai 13f3a43ddc 日常提交
2023-01-04 13:00:19 +08:00

39 KiB
Raw Blame History

SpringMVC

SpringMVC简介

SpringMVC是一款基于Servlet API的web框架并基于前端控制器的模式被设计。前端控制器有一个中央的控制器DispatcherServlet通过一个共享的算法来集中分发请求请求实际是通过可配置的委托组件@Controller类来处理的。

Spring Boot遵循不同的初始化流程。相较于hooking到servlet容器的生命周期中Spring Boot使用Spring配置来启动其自身和内置servlet容器。filter和servlet声明在在spring配置中被找到并且被注入到容器中。

DispatcherServlet

Context层次结构

DispatcherServlet需要一个WebApplicationContextApplicationContext的拓展类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 chainpre/post/Controller)会被执行返回一个model用于渲染。
  6. 如果model被返回那么返回的model会被渲染。如果model没有返回那么没有view会被渲染。

在WebApplicationContext中声明的HandlerExceptionResolver会用于解析处理请求中抛出的异常。

路径匹配

Servlet API将完整的请求路径暴露为requestURI并且进一步划分requestURI为如下部分contextPathservletPathpathInfo。

contextPath, servletPath, pathInfo区别

  • contextPathcontextPath为应用被访问的路径是整个应用的根路径。默认情况下SpringBoot的contextPath为"/"。可以通过server.servlet.context-path="/demo"来改变应用的根路径。
  • servletPathservletPath代表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。

# 重定向实例如下
# 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会自动执行类型转换。

@ModelAttribute

可以为handler method的参数添加@ModelAttribute注解在添加该注解之后参数属性的值可以从Model中进行获取如果model中不存在则对该属性进行初始化。
Model属性会被http servlet param中的值覆盖如果request param name和field name相同。

@ModelAttribute注解其name的默认值为方法参数或者返回值类型的首字母小写
例如“orderAddress" for class "mypackage.OrderAddress"

@ModelAttribute的使用具体可如下所示

@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<Stirng,T>类型的bean对象用于类型转换当model attribute name和path variable或者request param name相匹配且Converter<String,T>对象存在时会调用该Converter进行类型转换

@Component
class StringPersonConverter implements Converter<String,Person> {
    @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。

@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的类型。
该注解使用如下所示:

@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如下所示

@RequestMapping("/")
public String handle(@SessionAttribute User user) { 
    // ...
}

Multipart

在MultipartResolver被启用之后Post请求体中为multipart/form-data格式的数据将被转化并可以作为handler method的参数访问。如下显示了文件上传的用法

@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<MultipartFile>来解析多个文件名
当@RequestParam没有指定name属性并且参数类型为Map<String,MultipartFile>或MultiValueMap<String,MultipartFile>类型时会根据name自动将所有匹配的MultipartFile注入到map中

@RequestBody

可以通过@RequestBody注解读取请求体内容并且通过HttpMessageConverter将内容反序列化为Object。具体使用方法如下

@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
    // ...
}

HttpEntity

将参数类型指定为HttpEntity与使用@RequestBody注解类似其作为一个容器包含了请求体转化后的对象account实例和请求头HttpHeaders。其使用如下

@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
    // ...
}

@ResponseBody

通过使用@ResponseBody注解可以将handler method的返回值序列化到响应体中通过HttpMessageConverter。具体使用如下所示

@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
    // ...
}

同样的,@ResponseBody注解也支持class-level级别的使用在使用@ResponseBody标注类后对所有controller method都会起作用。
通过@RestController可以起到同样作用。

ResponseEntity

ResponseEntity使用和@ResponseBody类似但是含有status和headers。ResponseEntity使用如下所示

@GetMapping("/something")
public ResponseEntity<String> 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

@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请求体相关的内容)。

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountRepository.findAccount(number));
    // add more ...
}

也可以通过如下方式向model中添加attribute

// 只向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如下图所示

// 指定attribute name为“myAccount”
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
    // ...
    return account;
}

DataBinder

对于@Controller和@ControllerAdvice类在类中可以包含@InitBinder方法该方法用来初始化WebDataBinder实例

  • 将请求参数绑定到model
  • 将基于String类型的请求值例如请求参数pathVariableheaderscookies或其他转化为controller method参数的类型
  • 在html渲染时将model object value转化为String的形式

@InitBinder可以针对特定的Controller注册java.beans.PropertyEditor或Spring Converter和Formatter 组件。
@InitBinder支持和@RequestMapping方法一样的参数形式但是参数不能使用@ModelAttribute注解。通常@InitBinder方法的参数为WebDataBinder且返回类型为void

@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实现例如

@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属性与表单中提交的参数相关联。

// 只会对如下两个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方法中抛出的异常使用如下所示

@Controller
public class SimpleController {

    // ...

    @ExceptionHandler
    public ResponseEntity<String> handle(IOException ex) {
        // ...
    }
}

该异常参数会匹配被抛出的顶层异常例如被直接抛出的IOException也会匹配被包装的内层cause例如被包装在IllegalStateException中的IOException该参数会匹配任一层级的cause exception并不是只有该异常的直接cause exception才会被处理。

只要@ExceptionHandler方法的异常参数类型匹配异常抛出stacktrace中任一层次的异常类型异常都会被捕获并且处理。

    @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的深度来进行排序。

   // 在调用/shiro接口时IOException离root exceptionRuntimeException更近
   // 故而会优先调用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为指定异常或异常链中包含指定异常的场景

@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
    // ...
}

// 甚至可以将参数的异常类型指定为一个非常宽泛的类型例如Exception
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> 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使用如下

// 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注解以允许跨域请求使用示例如下

@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注解支持在类上使用类上注解会被方法继承

@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注解可同时在类和方法上使用

@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

@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);
    }
}