深入理解 Spring MVC Controller —— 请求映射

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

前言


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


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


Spring MVC 请求处理流程中的 Controller

在 Spring MVC 系列的开篇文章中,就已经介绍了 Spring MVC 处理请求的流程,后面的文章基本上都围绕着这个流程,本篇也不例外。


26.png


我们先来看下 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 的问题,欢迎留言交流。


目录
相关文章
|
6天前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
30天前
|
Java 数据库连接 Spring
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
文章是关于Spring、SpringMVC、Mybatis三个后端框架的超详细入门教程,包括基础知识讲解、代码案例及SSM框架整合的实战应用,旨在帮助读者全面理解并掌握这些框架的使用。
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
|
1月前
|
XML JSON 数据库
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
这篇文章详细介绍了RESTful的概念、实现方式,以及如何在SpringMVC中使用HiddenHttpMethodFilter来处理PUT和DELETE请求,并通过具体代码案例分析了RESTful的使用。
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
|
21天前
|
安全 Java 应用服务中间件
如何在 Spring Boot 3.3 中实现请求 IP 白名单拦截功能
【8月更文挑战第30天】在构建Web应用时,确保应用的安全性是至关重要的。其中,对访问者的IP地址进行限制是一种常见的安全措施,特别是通过实施IP白名单策略,可以只允许特定的IP地址或IP段访问应用,从而有效防止未授权的访问。在Spring Boot 3.3中,我们可以通过多种方式实现这一功能,下面将详细介绍几种实用的方法。
31 1
|
22天前
|
Java API UED
【实战秘籍】Spring Boot开发者的福音:掌握网络防抖动,告别无效请求,提升用户体验!
【8月更文挑战第29天】网络防抖动技术能有效处理频繁触发的事件或请求,避免资源浪费,提升系统响应速度与用户体验。本文介绍如何在Spring Boot中实现防抖动,并提供代码示例。通过使用ScheduledExecutorService,可轻松实现延迟执行功能,确保仅在用户停止输入后才触发操作,大幅减少服务器负载。此外,还可利用`@Async`注解简化异步处理逻辑。防抖动是优化应用性能的关键策略,有助于打造高效稳定的软件系统。
31 2
|
1月前
|
前端开发 应用服务中间件 数据库
SpringMVC入门到实战------八、RESTful案例。SpringMVC+thymeleaf+BootStrap+RestFul实现员工信息的增删改查
这篇文章通过一个具体的项目案例,详细讲解了如何使用SpringMVC、Thymeleaf、Bootstrap以及RESTful风格接口来实现员工信息的增删改查功能。文章提供了项目结构、配置文件、控制器、数据访问对象、实体类和前端页面的完整源码,并展示了实现效果的截图。项目的目的是锻炼使用RESTful风格的接口开发,虽然数据是假数据并未连接数据库,但提供了一个很好的实践机会。文章最后强调了这一章节主要是为了练习RESTful,其他方面暂不考虑。
SpringMVC入门到实战------八、RESTful案例。SpringMVC+thymeleaf+BootStrap+RestFul实现员工信息的增删改查
|
1月前
|
JSON 前端开发 Java
Spring MVC返回JSON数据
综上所述,Spring MVC提供了灵活、强大的方式来支持返回JSON数据,从直接使用 `@ResponseBody`及 `@RestController`注解,到通过配置消息转换器和异常处理器,开发人员可以根据具体需求选择合适的实现方式。
87 4
|
1月前
|
XML 前端开发 Java
Spring MVC接收param参数(直接接收、注解接收、集合接收、实体接收)
Spring MVC提供了灵活多样的参数接收方式,可以满足各种不同场景下的需求。了解并熟练运用这些基本的参数接收技巧,可以使得Web应用的开发更加方便、高效。同时,也是提高代码的可读性和维护性的关键所在。在实际开发过程中,根据具体需求选择最合适的参数接收方式,能够有效提升开发效率和应用性能。
72 3
|
1月前
|
XML 前端开发 Java
Spring MVC接收param参数(直接接收、注解接收、集合接收、实体接收)
Spring MVC提供了灵活多样的参数接收方式,可以满足各种不同场景下的需求。了解并熟练运用这些基本的参数接收技巧,可以使得Web应用的开发更加方便、高效。同时,也是提高代码的可读性和维护性的关键所在。在实际开发过程中,根据具体需求选择最合适的参数接收方式,能够有效提升开发效率和应用性能。
67 2
|
2月前
|
前端开发 Java 应用服务中间件
我以为我对Spring MVC很了解,直到我遇到了...
所有人都知道Spring MVC是是开发的,却鲜有人知道Spring MVC的理论基础来自于1978 年提出MVC模式的一个老头子,他就是Trygve Mikkjel Heyerdahl Reenskaug,挪威计算机科学家,名誉教授。Trygve Reenskaug的MVC架构思想早期用于图形用户界面(GUI) 的软件设计,他对MVC是这样解释的。MVC 被认为是解决用户控制大型复杂数据集问题的通用解决方案。最困难的部分是为不同的架构组件想出好的名字。模型-视图-编辑器是第一个。
108 1
我以为我对Spring MVC很了解,直到我遇到了...