Spring framework: MVC

Spring Web MVC

MVC架构模式与前端控制器设计模式

  • MVC(Model-View-Controller,模型-视图-控制器)是网页应用中最常见的架构
    • 模型用于存储数据与负责业务逻辑
    • 视图用于显示与渲染数据
    • 控制器是模型与视图的中介,用于接收并过滤来自视图的输入、格式化并输出来自模型的数据
  • 在传统的应用中,视图渲染同时也在服务器端进行,即是一个前后端不分离的应用,此时控制器处理请求的返回值是一个视图,在Spring中由ViewResolver解析器保证开发者只需要返回String而不是具体的View
  • 在前后端分离的现代Web应用中,前端交给一个前端框架开发,也许你已经听说过,此时后端应该提供RESTfulAPI,控制器返回的不再是View,而是数据
  • 前端控制器设计模式旨在将Controller部分再次拆分,用一个单一的处理程序(前端控制器)来处理所有的请求,包括过滤、认证与授权、处理、记录日志等 这个前端控制器可以根据请求的类型不同,通过一个调度器/分发器,将请求分发到下属的不同的处理程序 不同的处理程序会有对应的响应数据,视图用于呈现这些响应数据
  • 传统的Servlet开发有如下问题:
    • Servlet的单个doXxx()处理多个不同URL的请求,管理映射困难,需要用分支判断 而如果对每一个URL都写一个单独的Servlet服务,虽然能解决上述问题,但相似业务逻辑的代码就会被分散在不同的文件中
    • 需要手动地通过分支判断决定重定向以及内部转发,视图和服务紧耦合,难以扩展
    • 需要通过getParameter()获取参数以及手动类型转换和校验
  • Spring Web MVC是基于Jakarta EE Servlet APIWeb框架,通常简称Spring MVC

配置相关

DispatcherServlet的配置

  • Spring MVC提供了大部分实现,以使开发者只需要专心于编写控制器或处理器程序,然而稍微了解其内部构造仍是有用的

  • DispatcherServlet是一个前端控制器,它实现了jakarta.servlet.http.HttpServlet 它的作用就是识别项目里被@Controller等注解的处理器以及内部的方法映射,将来自不同URL的请求分发给它们

  • 这个前端控制器同时应该作为一个Bean被纳入IoC容器管理,而作为一个Servlet应用,应用启动的流程是:由Servlet容器创建并注册所有的Servlet服务类,然后启动,最后创建ApplicationContext实例,因此无法使用自动注入

  • 为了解决这个问题,DispatcherServlet的构造方法有两种:默认构造方法会查询ServletConfigInitParameter,使用contextConfigLocation配置;另一个构造方法要求一个WebApplicationContext参数,从而引导Servlet容器去创建IoC容器的上下文 这两种方法对应两种配置: 使用web.xml以及IoC容器所需的XML文件注册,部署在外部Servlet容器中 Servlet容器在创建DispatcherServlet时,检测到<init-param/>标签,转而读取对应的IoC容器的XML文件,在创建DispatcherServlet之前创建ClassPathXmlWebApplication实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!-- web.xml -->
    <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <init-param>
    <!-- 参数名需要记 -->
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml</param-value>
    </init-param>
    </servlet>

    <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/ctx_path</url-pattern>
    </servlet-mapping>

    <!-- applicationContext.xml -->
    <!-- IoC容器的XML配置 -->
    <beans ...>
    </beans>

    • 使用WebApplicationInitializer接口,覆写onStartup(ServletContext)方法来编程式地创建DispatcherServlet,实际上等价于用Java代码替代XML配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // 基于注解
      public class MyWebApplicationInitializer implements WebApplicationInitializer {
      @Override
      public void onStartup(ServletContext servletContext) {
      WebApplicationContext ctx = new AnnotationConfigWebApplicationContext(...);

      DispatcherServlet dispatcher = new DispatcherServlet(ctx);
      var registration = servletContext.addServlet("dispatcher", dispatcher);
      registration.addMapping("/api/*");
      }
      }
    • 如果使用基于Java的注解,更建议继承AbstractAnnotationConfigDispatcherServletInitializer

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

      @Override
      protected Class<?>[] getRootConfigClasses() {
      return new Class<?>[] { RootConfig.class }; // 返回null可能不是最佳实践
      }

      @Override
      protected Class<?>[] getServletConfigClasses() {
      return new Class<?>[] { MyWebConfig.class };
      }

      @Override
      protected String[] getServletMappings() {
      return new String[] { "/api/**" };
      }

      }
  • DispatcherServlet默认有若干内置的对象,重要的接口如下:

    • HandlerMapping接口:将处理器对应的类映射到某个具体的URL上 不需要调用或实现它,它的默认实现是RequestMappingHandlerMapping,它会自动解析@RequestMapping@GetMapping等注解映射
    • HandlerAdapter接口:(适配器)将处理器的不同方法统一为一个方法,使前端控制器不需要关注其细节 不需要调用或实现它,它的默认实现是ResquestMappingHandlerAdapter,它替代前端控制器来执行HandlerMapping找到的方法
    • HandlerExceptionResolver接口:将不同地方的各种异常映射到统一的异常处理器类上 不需要调用或实现它,它有若干实现类用于处理不同类别的异常源
    • ViewResolver接口:负责解析后端处理器返回的字符串,变为View对象,如JSPThymeleaf模板 不需要调用或实现它,它的默认实现是InternalResourceViewResolver 因为现代Web应用一般使用前后端分离,所以ViewResolver也不常用了
    • LocaleResolver接口:负责i18n 不需要调用或实现它,它有若干实现类
    • ThemeResolver接口:负责解析UI主题,即前端的工作 前端的工作一般不用Java来做,所以一般不需要使用它
    • MultipartResolver接口:负责解析multipart类型的数据 没有默认配置,需要自己配置,有现成的实现类

拦截链

  • 就像Jakarta EEFilter那样,DispatcherServlet在找到对应的处理器后,调用之前会先调用拦截器链

  • 拦截器需要实现HandlerInterceptor接口,包含三个方法,它们会作为回调函数被观察者调用

    • preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler):调用处理器之前的拦截点,通常进行日志记录、身份验证
    • postHandle(HttpServletRequest req, HttpServletResponse resp, Object handler, ModelAndView modelAndView):成功调用处理器之后,视图渲染之前的拦截点,通常用于添加视图的全局配置
    • afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex)):完成整个请求后的回调,通常用于资源关闭、性能统计等,就像finally块那样,即使之前的处理链有异常发生,也会调用该方法
  • 注册拦截器:

    • 基于Java的配置:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      // 需实现WebMvcConfigurer接口(全部都有default实现, 因此可按需实现)
      @Configuration
      @EnableWebMvc
      public class WebMvcConfig implements WebMvcConfigurer {
      private MyHandlerInterceptor interceptor;

      public WebMvcConfig(MyHandlerInterceptor interceptor) {
      this.interceptor = interceptor;
      }

      @Override
      public void addInterceptors(InterceptorRegistry registry) {
      // 注册interceptor, 作用于/api/**, 忽略/api/noIntercept
      // 执行顺序为最高(0为最高优先级)
      // addInterceptor()返回InterceptorRegistration, 然后允许链式调用配置方法
      registry.addInterceptor(interceptor)
      .addPathPatterns("/api/**")
      .exludePathPatterns("/api/noIntercept")
      .order(0);
      }
      }
    • 基于XML配置:待补

  • CORS配置:Spring MVC的默认CORS策略是同源策略,即默认情况下,不允许被不同域访问(包括localhost的不同端口),浏览器确认的具体做法是先发送预请求获取对方的CORS配置,然后限定只能发送或不能发送特定的请求

  • @CrossOrigin类/方法级注解就是用于配置CORS的,针对一个类,它会作用于该类的所有方法

    • origin属性包括它允许的来源域
    • originPatterns属性包括它允许被获取的URI
    • methods属性包括它允许的请求方法
    • allowedHeadersexposedHeaders:允许/不允许请求所包含的请求头
    • maxAge:秒为单位的CORS配置有效时间
  • 除了注解配置,还可以基于Java配置,仍然是WebMvcConfigurer的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Configuration
    @EnableWebMvc
    public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    registry.allowedOrigins("localhost")
    .addMappings("/api/**")
    .allowedMethods("GET", "POST")
    .allowedHeaders(HttpHeaders.LOCATION)
    .exposedHeaders(HttpHeaders.ACCEPT)
    .maxAge(3600);
    }
    }

模型与视图

Model接口

  • Model类似ServletRequest的属性在Servlet应用中的作用,用于管理单个请求范围内的由后端程序设置的属性
  • Model提供addAttribute(String, Object)getAttribute(String, Object)containsAttribute(String)三个常用的属性相关方法 它不像ServletRequest那样提供删除属性的方法,因为Model的生命周期较短,设计者认为不需要删除属性
  • Model实例通常不需要开发者自己创建,而是由Spring容器自动创建并注入为参数

ModelAndView

  • ModelAndView类通常作为一个返回视图的方法的返回值,需要开发者自己创建,是Model结合返回值String的一个替代选择,它包揽了Model以及返回视图的功能,算是一种风格上的不同 即使在前后端结合的应用中,它也不常用,因为它不止返回视图,还返回其中添加的属性
  • 它提供addObject(String, Object)setViewName(String)

注解式声明

Controller声明

  • @ResponseBody类/方法级注解,注解类时表示该类的所有方法的返回值不应经过ViewResolver,而是作为响应体返回;注解方法时只表示该特定方法的返回值不经过ViewResolver
  • @Controller类级注解:标识该类是传统的控制器类,通常作为页面控制器而存在,虽然返回值允许多个类型,但期望返回String,表示视图类型,然后ViewResolver会解析它为视图
  • @RestController类级注解:标识该类是现代的前后端分离的控制器类,通常作为数据接口,即API控制器而存在,期望返回ResponseEntity<T>或其它类型,表示数据 @RestController本质是@Controller加上@ResponseBody注解,后者表示该类的方法返回的是数据本身而不是一个视图,不应经过ViewResolver解析
  • @ControllerAdvice类级注解:表示该类中定义的ExceptionHandler方法会应用于全局的Controller
  • @RestControllerAdvice类级注解:等价于@ControllerAdvice加上@ResponseBody注解

URL映射

  • @RequestMapping:通常作为类级注解,value表示映射到的URL路径,method表示请求方法

  • 针对方法,在@RequestMapping的基础上有若干的扩展注解,即@GetMapping@PostMapping@PutMapping等,它们规定了@RequestMappingmethod属性,会继承来自类级@RequestMapping的映射路径,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Controller
    @RequestMapping("/api")
    public class ApiController {
    // 访问/api/obj时会重定向到此
    @GetMapping("/obj")
    public String getObj() {
    return "index";
    }
    }
  • @RequestMappingvalue(URL路径)以外,还有若干属性用于缩小一个方法对应的URL范围

    • consumes属性:指定该方法能处理的请求的数据类型,例如consumes={"application/json"},它会自动解析Content-Type请求头
    • produces属性:指定该方法响应的数据类型,例如produces={"application/json"},它会自动解析Accept请求头
    • params属性:限定该方法接受请求所带的参数,例如params={"a", "!a", "a=b"}"a"表示检测请求是否带a参数,!a表示检测请求是否不带a参数,a=b表示检测请求里的a参数是否等于b
    • headers属性:限定该方法接受请求所带的请求头,格式和params类似
  • 除了*以及**通配符以外,Spring提供?以及{}捕获功能

    • ?匹配任意单个字符
    • {variable}:将该部分路径信息捕获,赋给variable模板参数,可以通过@PathVariable获取
    • {variable:regex}:仅将能成功匹配regex正则表达式的URL交给该方法处理,并将该部分路径赋给variable 因为在java中,使用regexPCRE不同,例如\需要\\转义
    • 类型转换由Spring提供,需要目标参数的类型能通过单个String对象构造

允许自动注入的方法参数

  • 虽然允许直接注入ServletRequestServletResponseServlet API,但通常没有必要
  • InputStreamReaderOutputStreamWriter:从Servlet API获取的流对象
  • Model:获取当前请求的整个模型实例
  • Spring核心提供的类型转换十分强大,以下注解获取的数据只要能进行转换(包括json数据),就能自动注入为各种非String类型的参数值 甚至可以自定义Converter并注册,以实现自定义转换的功能
  • @PathVariable:获取对应的从URL中捕获的参数
  • @MatrixVariable:获取从URL最后由;分割的名值对 例如/api/a;name1=value1,value2;name2=value3,不同名值对由;分割、同一名值对的多个值由,分割
  • @RequestParam("paramName"):获取对应的请求参数,若为POST则只能获取表单或multipart参数
  • @RequestHeader("headerName"):获取对应的请求头的值
  • @CookieValue("cookieName"):获取对应的Cookie
  • @RequestBody:获取对应的请求体
  • @RequestPart:获取multipart请求体的一个part
  • @ModelAttribute("attr"):获取模型中,属性名为attr的值
  • @SessionAttribute("attr"):获取当前会话的属性attr的值
  • @RequestAttribute("attr"):获取当前请求中存的属性attr的值
  • 其它方法参数,若不带任意注解且不是上述的任意类型,若为简单类型则解析为@RequestParam,否则解析为@ModelAttribute

返回值类型

  • ResponseEntity<T>:完整的由开发者决定的响应体
    • 调用一个指定了状态码的静态方法,创建一个ResponseEntity.BodyBuilderResponseEntity.HeadersBuilder创建者: status(HttpStatusCode):指定任意状态码,创建一个响应报文 ok():等价于status(200),服务器成功返回网页 accept():等价于status(202),服务器接受但尚未处理 basRequest():等价于status(400) notFound():创建一个仅含响应头的响应报文Builder internalServerError():等价于status(500)
    • HeadersBuilder接口包含以下方法: build():创建ResponseEntity header(String, String)以及headers(HttpHeaders):添加响应头
    • BodyBuilder继承HeaderBuilder接口,此外包含以下方法: contentType(String):设置响应体数据类型 contentLength(long):设置响应体大小 body(T):设置响应体,会自动由Converter序列化
  • void:方法参数必须包含输出流或ServletResponse,此时表示Spring认为请求在该方法内部完成了处理,返回值为void
  • @ModelAttribute:它在修饰参数时表示获取,而在修饰方法时表示方法的返回值会添加到该请求的Model里而不是返回
  • StringViewModelAndView:返回字符串表示的视图、自己创建的视图、绑定了一些属性的视图

异常处理

  • @ExceptionHandler注解修饰的方法用于处理来自Controller类的异常,它不需要@RequestMapping
  • 其接受的方法参数在大多数情况应该是某一个异常类型
  • 其方法的返回值类型规则与一般的@RequestMapping方法一致
  • @ExceptionHandler允许设置一系列异常类的class属性,表示只会接受这些类型的异常
  • @Controller类、@RestController类中定义的异常处理器只作用于其所在类的其它方法抛出的异常
  • @ControllerAdvice类、@RestControllerAdvice类中定义的异常处理器作用于所有的Controller

其它工具类

  • org.springframework.http.HttpStatus:包含一系列状态码的枚举
  • org.springframework.http.MediaType:包含一系列数据类型常量,包括可以用parseMediaType(String)将其中没有提供的数据类型显式转换为MediaType
  • org.springframework.http.HttpHeaders:包含一系列请求头、响应头的常量以及设置方法