前言
基于注解的 Spring MVC 的项目中,Controller 应该是我们接触最多的类了,这里提到的 Controller 并非是某一个具体的接口或类,而是一种概念,只要我们我们定义的类中包含了处理请求的方法,这个类就可以称为 Controller,而处理请求的方法被称为处理器方法。
由于 Controller 的内容较多,因此我打算将它拆成几块做讲解,相信看完这几篇文章之后,你会对 Controller 有更深入的认识,本篇要介绍的主要是 Controller 的定义及请求映射部分。
Spring MVC 请求处理流程中的 Controller
在 Spring MVC 系列的开篇文章中,就已经介绍了 Spring MVC 处理请求的流程,后面的文章基本上都围绕着这个流程,本篇也不例外。
我们先来看下 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", 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; } }
总结
本篇主要介绍了 Controller 的定义与处理器方法的请求映射,其中路径匹配是 Spring 最复杂的部分。了解请求映射之后,下一步我们要关注的就是处理器方法对请求参数的接收,Spring 同样提供了各种各样的接收方式,下篇将进行介绍。如果你有 Spring 的问题,欢迎留言交流。