浅聊Controller 请求映射

基于注解的 Spring MVC 的项目中,Controller 应该是我们接触最多的类了,这里提到的 Controller 并非是某一个具体的接口或类,而是一种概念,只要我们定义的类中包含了处理请求的方法,这个类就可以称为 Controller,而处理请求的方法被称为处理器方法。

由于 Controller 的内容较多,因此我打算将它拆成几块做讲解,相信看完这几篇文章之后,你会对 Controller 有更深入的认识,本篇要介绍的主要是 Controller 的定义及请求映射部分。

Spring MVC 请求处理流程中的 Controller

在这里插入图片描述

我们先来看下 Controller 在 Spring MVC 请求处理流程中所扮演的角色。

Servlet 容器将请求派发至 DispatcherServlet 之后,DispatchServlet 使用 HandlerMapping 查找处理请求的 Handler,这里的 HandlerMapping 是一个接口,不同的实现支持不同的 Handler 实现,Handler 不与具体的接口或者类耦合,最终由不同的 HandlerAdapter 适配后调用 Handler 处理请求。

处理器方法正是 Spring MVC 中 Handler 的一种,由 Controller 类中的方法定义,也就是说 Controller 是 Handler 的一个容器。

这里支持处理器方法的 HandlerMapping 实现是 RequestMappingHandlerMapping,适配处理器方法的 HandlerAdapter 实现是 RequestMappingHandlerAdapter。

Controller 定义

实际上 Spring MVC 内部确实有一个 Controller 接口,也是 Spring MVC 中的 Handler 之一,将自定义的类实现这个接口,然后再进行相关配置就可以处理请求了。不过这个接口并非本篇所介绍的重点,如不做特殊说明,本篇所指的 Controller 都是用户自定义的不限制必须实现某个接口的容纳处理器方法的类。

@Controller 注解方式定义 Controller

定义一个 Controller,通常具有两种方式。

第一种方式是在自定义的类上加上 @Controller 注解,这样我们定义的类就成为一个 Controller 类了,这个 Controller 也将自动注册为 bean。示例代码如下:

@Controller
public class CustomController {
    
}

@RestController 注解方式定义 Controller

第二种方式是在自定义的类上加上 @RestController 注解,这个注解是 @Controller 与 @ResponseBody 注解的结合,用于返回 json 或 xml 格式的响应体,而不是一个 html 页面。示例代码如下:

@RestController
public class CustomController {
    
}

@RequestMapping 注解方式定义 Controller

除了常规的方式,可能很多人没注意到,Spring 还有另一种方式定义 Controller,只需要在我们自定义的类上加上 @RequestMapping 注解或者 @RequestMapping 元标注的注解,然后注册为 bean,我们自定义的类也能成为一个 Controller。

直接标注 @RequestMapping 示例代码如下:

@Component
@RequestMapping("/custom")
public class CustomController {

}

@RequestMapping 元标注示例代码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping
public @interface CustomMapping {

    @AliasFor(annotation = RequestMapping.class)
    String name() default "";

    @AliasFor(annotation = RequestMapping.class)
    String[] value() default {};

    @AliasFor(annotation = RequestMapping.class)
    String[] path() default {};
}

@Component
@CustomMapping("/custom")
public class CustomController {

}

请求映射

处理器方法定义

Controller 本身并不是一个处理器,而是处理器方法的容器,如果想要处理请求还需要定义处理器方法,处理器方法的签名没有具体规定,Spring 会自动对方法参数和参数返回值进行处理。

假设我们想要做一个登陆功能,登陆成功后跳转到成功页面,我们可以定义处理器方法 login 如下:

@Controller
public class UserController {

    ModelAndView login(HttpServletRequest request) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("username", request.getParameter("username"));
        modelAndView.setViewName("success");
        return modelAndView;
    }
    
}

请求映射配置

那么 Spring 怎么知道访问哪个请求路径调用我们的 login 处理器方法呢?这就需要我们通过注解告诉 Spring 处理器方法映射的请求是哪个。最初的 Spring MVC 使用 @RequestMapping 进行请求映射。示例代码如下:

@Controller
@RequestMapping("/user")
public class UserController {
    
    @RequestMapping(value = "/login")
    ModelAndView login(HttpServletRequest request) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("username", request.getParameter("username"));
        modelAndView.setViewName("success");
        return modelAndView;
    }

}

可以看到,我们的 Controller 类和处理器方法上都添加了 @RequestMapping 注解,并且指定了请求的路径。Spring 会自动将 Controller 类上的路径和处理器方法上的路径进行拼接,当浏览器向我们的服务发起 /user/login 的请求,就会使用我们定义的处理器方法处理请求。

Servlet 只支持对路径进行匹配,而 Spring MVC 中的处理器方法除了支持路径匹配,还支持其他的映射方式,下面分别介绍。

路径匹配

处理器方法请求映射中,路径匹配的设置方式很简单,直接指定 @RequestMapping 的 value 或 path 属性就可以了,如上面的示例。

Servlet 路径匹配方式

路径匹配是最基本的匹配方式,对于 Servlet 而言,主要有4种路径匹配方式:

  • 精确匹配,如 /user/login,仅匹配 /user/login 路径的请求。
  • 前缀模糊匹配,如 /user/*,匹配 /user 开头的所有请求,如 /user、/user/login。
  • 后缀模糊匹配,如 *.html,匹配所有以 .html 结尾的请求,如 /home.html。
  • 默认匹配 /,/ 支持所有的请求路径,也是容器处理静态资源的 Servlet 所使用的路径,可以在 web.xml 中自定义映射 / 路径的 Servlet 覆盖容器的默认行为。

Spring MVC 路径匹配方式

通常情况下,我们为 DispatchServlet 配置映射的请求路径是 / 或者 /*,这样所有的请求都会派发到 DispatchServlet。那么 DispatchServlet 又如何派发请求到 Handler 呢?

DispatchServlet 支持的 ant 风格的路径匹配方式,具体如下:

1、精确匹配

如 /user/login。

2、模糊匹配

  • 如 /user/*,* 表示任意不包含 / 的路径名称,可以接受的请求包括 /user/login、/user/logout 等等。
  • 如 /user/**/login,** 表示任意层次的路径,可以接受的请求包括 /user/login、/user/aaa/login、/user/aaa/bbb/login 等等。
  • 如 /use?/login,? 表示单个字符,可接受的请求包括 /user/login、/usee/login 等等。
  • 如 /user/{name}/login,name 表示某一段请求路径,Spring 可以将 name 解析为变量,可接受的请求包括 /user/aaa/login、/user/bbb/login、/user/ 等等,此时 name的值为 aaa、bbb。
  • 如 /user{name}/login,name 表示矩阵变量,可接受的请求包括 /user;name=zhangsan/login、/user;name=lisi/login 等等。

默认情况下,我们定义的映射路径应该是 DispatcherServlet 映射路径中模糊匹配的部分,如果我们定义 DispatcherServlet 映射路径为 /user/*,那么我们只需要为处理器方法指定 /login 映射路径就可以匹配 /user/login 请求。

DispatcherServlet 路径匹配实现

DispatchServlet 映射路径模糊匹配部分获取的思路是 请求 URI - 上下文路径 - DispatcherServlet url-pattern 精确匹配部分,如请求 URI 是 /context/user/login,上下文路径是 /context,DispatcherServlet url-pattern 是 /user/*,此时 映射路径 = /context/user/login - /context - /user = /login,也就是说我们为处理器方法配置 /login 映射路径就可以了。

各个部分获取方式如下:

  • 请求 URI:request.getRequestURI(),获取到的是未解码的字符串,和 HTTP 协议中的一致,例如请求路径是 /context/根路径/user/login,这个方法获取到的实际是编码后的 /context/%E6%A0%B9%E8%B7%AF%E5%BE%84/user/login。
  • 上下文路径:request.getContextPath(),获取到的同样是未解码的字符串,例如上下文配置为 /上下文,这个方法获取到的实际是编码后的 /%E4%B8%8A%E4%B8%8B%E6%96%87。
  • url-pattern 精确匹配部分:request.getServletPath(),这个方法获取到的是解码后的字符串,例如 url-pattern 配置为 /登录/*,获取到的值是 /登录。

路径匹配是请求映射最复杂的部分,Spring 考虑了路径匹配遇到的各种特殊情况,Spring 5.2 版本默认的行为如下:

  • 由于项目中定义的上下文路径、请求路径可能包含非 ASCII 字符,通过代码获取到的可能和定义的不一致,因此 Spring 会对获取到的上下文路径、请求路径先进行解码处理。
  • 由于请求路径中可能包含矩阵变量,因此 Spring 默认会将获取到的请求路径去除矩阵变量部分,例如请求路径是 /user;name=zhangsan/login,处理后的路径是 /user/login。
  • 由于请求路径中可能包含用户误输入的连续的 /,因此 Spring 会将请求路径中连续的 / 替换为单个 /,例如请求路径是 /user//login,处理后的路径是 /user/login。
  • 由于请求可能来自 request.getRequestDispatcher("/include").include(request, response),因此 Spring 会优先从 request attribute 中获取各种路径信息。
  • 如果 DispatchServlet 配置是应用的默认页面路径,如 /index.html,访问 / 时容器也会将请求派发到 DispatchServlet,此时请求路径与 url-pattern 不匹配,Spring 也会做额外处理。
  • 此外 Spring 还会根据请求 URI 与配置的映射路径解析出路径变量,如果设置查找路径时不去除矩阵变量,Spring 还能解析出矩阵变量,之后便会将路径变量和矩阵变量存至 request 的属性中,便于后续获取。

方法匹配

HTTP 请求方式常用的有 4 种,分别是 GET、POST、PUT、DELETE,可以在 @RequestMapping 的 method 属性中指定处理器方法支持的请求方式,示例代码如下:

@RequestMapping(value = "/login",method = RequestMethod.POST)

对于这几个常用的方法,如果每次都使用 method 属性指定方法,会增加很多手工编写代码的工作量,Spring 提供了与请求方法对应的几个注解来替代 @RequestMapping,包括 @GetMapping、@PostMapping、@DeleteMapping、@PutMapping,使用方式与 @RequestMapping 相同,原理是 Spring 的注解编程模型,Spring 会自动将注解和元注解组合为我们需要的注解。

查询参数匹配

Spring 还支持查询字符串中的参数的匹配方式,如果不匹配查询字符串中的参数,处理器方法也不会对请求进行处理。参数匹配设置方式如下:

@PostMapping(value = "/login", params = {"name", "age=20", "!sex", "address!=中国"})

其中,name 表示查询字符串中必须包含 name 参数,age=20 表示查询字符串中的 age 参数值必须是 20,!sex 表示查询字符串中不能包含 sex 参数,address!=中国 表示查询字符串中存在 address 参数且值不能等于 中国。

请求头匹配

和查询参数相似,Spring 支持匹配某些请求头,匹配后处理器方法才会处理请求。

请求头匹配设置方式如下:

@PostMapping(value = "/login", headers = {"name", "age=20", "!sex", "address!=中国"})

headers 参数值的含义与 params 参数值的含义相同,只是从请求头中获取参数值。

请求内容类型匹配

Spring 支持请求内容类型的匹配,如果只想让处理器方法处理 application/json 内容类型的请求,可以做如下的配置:

@PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE)

响应内容类型匹配

通常可以指定响应的内容类型,如果响应的内容类型请求不接受,那么处理器方法同样不会处理请求。如果只想让处理器方法产生 application/json 类型的响应内容,可以做如下的配置:

@PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE)

自定义注解

假如我们想要指定接收和处理的内容类型都是 application/json,我们需要做如下的配置:

@PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)

如果处理的请求比较少还能接受,如果处理的请求比较多,我们要写很多遍 consumes/produces 参数,增加了我们的工作量。利用 Spring 的注解编程模型,我们可以定义自己的 @RequestMapping 注解,如果我们只想接收和产生 application/json 内容类型,我们可以如下自定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public @interface JsonRequestMapping {

    @AliasFor(annotation = RequestMapping.class)
    String name() default "";
    
    @AliasFor(annotation = RequestMapping.class)
    String[] value() default {};
    
    @AliasFor(annotation = RequestMapping.class)
    String[] path() default {};
    
    @AliasFor(annotation = RequestMapping.class)
    String[] params() default {};
    
    @AliasFor(annotation = RequestMapping.class)
    String[] headers() default {};
}

然后将这个注解添加到处理器方法上即可,Spring 会自动处理我们的注解。

@Controller
@RequestMapping("/user")
public class UserController {

    @JsonRequestMapping(value = "/login")
    ModelAndView login(HttpServletRequest request) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("username", request.getParameter("username"));
        modelAndView.setViewName("success");
        return modelAndView;
    }

}

 

参考:

 

posted @ 2022-01-11 23:10  残城碎梦  阅读(556)  评论(0编辑  收藏  举报