📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍

博主所在公司早期后端采用 SSM(Spring + SpringMVC + MyBatis),2020年进行了微服务拆分,顺带技术栈升级,后端调整为 SpringCloud 和 SpringBoot,深刻体会到了SpringBoot为我们带来的遍历。
经常会听到新人之间在议论,“还是新框架好,旧框架已过时了,不要浪费时间学习旧框架”,那么 SpringBoot 是否完全取代了 SSM 技术栈呢?
那么,有了 SpringBoot,是否还需要学习 SpringMVC?
毋庸置疑,SpringBoot 的出现确实简化了 SpringMVC 的配置,但 SpringMVC 作为 Spring 生态中重要的组成部分,其底层原理和核心概念依然值得深入学习。
总结一下:
SpringBoot 和 SpringMVC 并不是对立的关系,而是相辅相成的。初学者建议优先学习 SpringBoot,可以让你快速上手,提高开发效率。接着深入学习 SpringMVC 的底层原理,更深入得使用其定制化能力,从而写出更高质量的代码。
接下来的本系列文章,将从 SpringMVC 的搭建、常见用法、源码分析、扩展点分析、企业实战等方面展开。
好,先开始基础篇的介绍!
开发环境:
IDE:IDEA 2022
构建工具:Maven 3.x
服务器:Tomcat 8
Spring版本:5.3.15
创建 Maven 项目:
使用 IDEA 创建一个Maven 子模块, 可以使用模板方式创建剩下不少功夫,具体如下图:
先展示Demo全貌:
圈出来的文件也是下面几个步骤需要交互的,后续就不一一截图了。
org.springframework spring-webmvc 5.3.15 ch.qos.logback logback-classic 1.2.3 javax.servlet javax.servlet-api 3.1.0 provided org.thymeleaf thymeleaf-spring5 3.0.12.RELEASE 引入后查看 Maven 依赖:
【知识扫盲】
web.xml 是一个传统的 Java Web 应用的部署描述文件,它用于配置 Web 应用的一些基本信息,比如:监听器、过滤器、过滤器、Servlet、欢迎页等。
在 Spring MVC 项目中,web.xml 主要负责:
1、加载 Spring 容器,通过 ContextLoaderListener 监听器加载 Spring 配置文件(applicationContext.xml 等),初始化 Spring 容器。
2、配置前端控制器 DispatcherServlet,DispatcherServlet 是 Spring MVC 的核心,负责拦截请求,分发给相应的 Controller 处理。
Tomcat 容器启动的时候,会加载 web.xml,这是一个复杂的过程,这边先不展开。
web.xml 内部元素的加载顺序通常是:context-param -> listener -> filter -> servlet。
值得一提的是,Spring Boot 内嵌了 Tomcat,不需要额外的部署描述文件。
【关键代码】
springMVC org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:spring-mvc.xml 1 springMVC / web.xml里面加载的配置文件,当然也可以通过添加配置类的形式实现功能,这里就不展开了。
如下所示,一个方法返回视图,一个方法返回数据。
Tips:index.jsp是Maven模板自带的,如果要显示中文,注意加上编码设置。
Tips:目前基本都是前后端分离模式,因此主要关注返回数据的场景。
@Controller public class HelloController { @RequestMapping("/") public String index() { return "index"; } @ResponseBody @RequestMapping(value = {"/data", "/test"}) public String data() { return "12345"; } } <%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> Hello 战神!
这步比较简单,直接看图:

点击启动后,会自动访问:http://localhost:8085/study_mvc/
修改访问地址:http://localhost:8085/study_mvc/data
也可以正常输出数据,搞定收工!
接下来,测试一下JSON类型数据的传递。
先定义一个实体类,并修改控制层代码,@RequestBody 接收参数,@ResponseBody 响应结果,如下所示。
public class Student implements Serializable { private static final long serialVersionUID = 3227472127782930834L; private Integer id; private String name; private String email; private Integer age; ... } @Controller public class HelloController { @ResponseBody @RequestMapping(value = "stu") public Student stu(@RequestBody Student student) { student.setName("战神"); return student; } } 启动代码试试看,Postman测试如下:
解决方案也简单,引入一下依赖:
com.fasterxml.jackson.core jackson-databind 2.9.0 重启一下服务,再试试:
Tips:很多文章介绍需要额外的步骤,其实引入依赖才是核心。
背景前提:
如果你的 DispatcherServlet 拦截的是 *.do 这样的URL,就不存在访问不到静态资源的问题,但要多书写.do。
如果你的 DispatcherServlet 拦截的是 /,代表拦截了所有的请求,同时对 .js、.jpg 的访问也就被拦截了。
但 DispatcherServlet 不具备静态文件的处理能力,所以需要额外的配置。
映射规则参考一下知识延伸 - url-pattern 映射规则
目前有三种方案,那么如何选择,主要需求是可以访问静态文件,不要报404,当然效率也要注意。
方案一:激活Tomcat的defaultServlet来处理静态文件(推荐)
default *.jpg *.gif *.png *.js *.css *.html 说明:要配置多个,每种文件配置一个,并且要写在 DispatcherServlet 的前面,让 defaultServlet 先拦截,这样就不会进入Spring了,我想性能是最好的吧。
补充说明:
这里的 default,其实是在$tomcat/conf/web.xml文件中配置的:
该 web.xml 的执行优先级低于项目自带的 web.xml,但如果项目的没注册这个Servlet,则以 Tomcat 的为准。
default org.apache.catalina.servlets.DefaultServlet debug 0 listings false 1 default / 如果某个 Servlet 的映射路径仅仅是一个/,那么这个Servlet就成为当前web应用的默认Servlet它可以处理其它所有Servlet都不处理的请求。开发时最好不要出现这种情况,否则web应用的静态资源无法被访问,从而被此Servlet 拦截处理。
SpringMVC org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath*:mvc-config.xml 1 SpringMVC / 这种情况会覆盖由 tomcat 提供的默认的 Servlet,该Serlvet是为静态资源提供访问服务的。
所有要额外配置其他高优先级处理器,也就是按方案一的设置,指定类型的文件,访问的时候,先使用默认Servlet处理,找不到才使用 MVC 的。
方案二: 在spring3.0.4以后版本提供了mvc:resources **
mvc:resources 的使用方法:
/images/**映射到 ResourceHttpRequestHandler进行处理,location指定静态资源的位置,可以是web application根目录下、jar包里面,这样可以把静态资源压缩到jar包中。cache-period 可以使得静态资源进行web cache
如果出现下面的错误,可能是没有配置
报错WARNING: No mapping found for HTTP request with URI [/mvc/user/findUser/lisi/770] in DispatcherServlet with name ‘springMVC’
使用mvc:resources/元素,把mapping的URI注册到SimpleUrlHandlerMapping的urlMap中,key为mapping的URI pattern值,而value为ResourceHttpRequestHandler,
这样就巧妙的把对静态资源的访问由HandlerMapping转到ResourceHttpRequestHandler处理并返回,所以就支持classpath目录,jar包内静态资源的访问。
另外需要注意的一点是,不要对SimpleUrlHandlerMapping设置defaultHandler.因为对static uri的defaultHandler就是ResourceHttpRequestHandler,
否则无法处理static resources request。
方案三,使用mvc:default-servlet-handler/
Xml代码 :mvc:default-servlet-handler/
会把"/**" url 注册到SimpleUrlHandlerMapping的urlMap中,把对静态资源的访问由HandlerMapping转到 org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler 处理并返回。
DefaultServletHttpRequestHandler使用就是各个Servlet容器自己的默认Servlet。
需要搭配: mvc:annotation-driven/
补充说明1:
多个HandlerMapping的执行顺序问题:
DefaultAnnotationHandlerMapping的order属性值是:0
mvc:default-servlet-handler/自动注册的SimpleUrlHandlerMapping的order属性值是:2147483647
spring会先执行order值比较小的。当访问一个a.jpg图片文件时,先通过DefaultAnnotationHandlerMapping 来找处理器,一定是找不到的,我们没有叫a.jpg的Action。再按order值升序找,由于最后一个SimpleUrlHandlerMapping 是匹配"/**"的,所以一定会匹配上,再响应图片。
补充说明2:
访问一个图片,还要走层层匹配。真不知性能如何?改天做一下压力测试,与Apache比一比。
最后再说明一下,如何你的DispatcherServlet拦截 *.do这样的URL,就不存上述问题了。
玩过 SpringBoot 的人,看到上面步骤,一定感觉十分繁琐(对熟练的人另说)。
就像这篇《 搭建拥有数据交互的 SpringBoot 》介绍的,可能两三下就可以获取一个 SpringBoot 项目。
前面提到的,返回JSON数据也不需要额外的配置(依赖都导入了),静态资源也有默认约定。
果然还是便捷啊,不过还是前面说的,先使用 SpringBoot,再深入理解 SpringMVC。
Tips:控制层 @Controller 的核心注解,设置接口的访问路径,类和方法上可以添加。
@RequestMapping 注解的功能
从注解名称上我们可以看到,@RequestMapping 注解的作用就是将请求和处理请求的控制器方法关联起来,建立映射关系。SpringMVC 接收到指定的请求,就会来找到在映射关系中对应的控制器方法来处理这个请求。
@RequestMapping 注解的位置
@RequestMapping 标识一个类:设置映射请求的请求路径的初始信息。
@RequestMapping 标识一个方法:设置映射请求请求路径的具体信息。
Tips:@Target({ElementType.TYPE, ElementType.METHOD})
@RequestMapping 注解的 value 属性
@RequestMapping 注解的 value 属性通过请求的请求地址匹配请求映射。
@RequestMapping 注解的 value 属性是一个字符串类型的数组,表示该请求映射能够匹配多个请求地址所对应的请求,通常数组类型的注解属性,值可以是如下两种形式:
@RequestMapping(value = {"/data", "/test"}) @RequestMapping("/testParam") @RequestMapping 注解的 value 属性必须设置,至少通过请求地址匹配请求映射
@RequestMapping 注解的 method 属性
method 属性通过请求的请求方式(get或post)匹配请求映射
method 属性是一个 RequestMethod 类型的数组,表示该请求映射能够匹配多种请求方式的请求
若当前请求的请求地址满足请求映射的value属性,但是请求方式不满足method属性,则浏览器报错405:Request method ‘POST’ not supported
写法如下:method = {RequestMethod.GET, RequestMethod.POST}
补充:@RequestMapping 的派生注解
对于处理指定请求方式的控制器方法,SpringMVC中提供了@RequestMapping的派生注解
处理get请求的映射–>@GetMapping
处理post请求的映射–>@PostMapping
处理put请求的映射–>@PutMapping
处理delete请求的映射–>@DeleteMapping
补充:常用的请求方式
常用的请求方式有 get,post,put,delete,但是目前浏览器只支持get和post,若在form表单提交时,为method 设置了其他请求方式的字符串(put或delete),则按照默认的请求方式get处理。
Tips:form 不支持发送这个method设置,但 ajax 和 axios 支持。
@RequestMapping 注解的 params属性
Tips:这个属性设置的多个值必须同时满足才能触发,本属性基本不用,了解即可。
params 属性通过请求的请求参数匹配请求映射,是一个字符串类型的数组,可以通过四种表达式设置请求参数和请求映射的匹配关系。
“param”:要求请求映射所匹配的请求必须携带param请求参数
“!param”:要求请求映射所匹配的请求必须不能携带param请求参数
“param=value”:要求请求映射所匹配的请求必须携带param请求参数且param=value
“param!=value”:要求请求映射所匹配的请求必须携带param请求参数但是param!=value
来一段Demo:
@RequestMapping( value = {"/testRequestMapping", "/test"} ,method = {RequestMethod.GET, RequestMethod.POST} ,params = {"username","password!=123456"} ) 若当前请求满足 value和method属性,但不满足params属性,此时页面回报错400:
Parameter conditions “username, password!=123456” not met for actual request parameters: username={admin}, password={123456}
@RequestMapping 注解的 headers 属性
基本同上,用的很少!
headers 属性通过请求的请求头信息匹配请求映射是一个字符串类型的数组,可以通过四种表达式设置请求头信息和请求映射的匹配关系。
“header”:要求请求映射所匹配的请求必须携带header请求头信息
“!header”:要求请求映射所匹配的请求必须不能携带header请求头信息
“header=value”:要求请求映射所匹配的请求必须携带header请求头信息且header=value
“header!=value”:要求请求映射所匹配的请求必须携带header请求头信息且header!=value
若当前请求满足@RequestMapping注解的value和method属性,但是不满足headers属性,此时页面显示404错误,即资源未找到
SpringMVC 支持ant风格的路径
即模糊匹配用法
?:表示任意的单个字符,有且仅能1个,不能是“/”这类型特殊字符
*:表示任意的0个或多个字符
:表示任意的一层或多层目录,只能使用//xxx的方式
SpringMVC 支持路径中的占位符
原始方式:/deleteUser?id=1
rest方式:/deleteUser/1
SpringMVC 路径中的占位符常用于 RESTful 风格中,当请求路径中将某些数据通过路径的方式传输到服务器中,就可以在相应的 @RequestMapping 注解的 value 属性中通过占位符 {xxx} 表示传输的数据,再通过@PathVariable 注解,将占位符所表示的数据赋值给控制器方法的形参。
来一段Demo:
///testRest/1/admin //最终输出的内容为-->id:1,username:admin @RequestMapping("/testRest/{id}/{username}") public String testRest(@PathVariable("id") String id, @PathVariable("username") String username){ System.out.println("id:"+id+",username:"+username); return "success"; } 接收参数的方式,与请求传递的 ContentType 紧密有关,常见的有 application/x-www-form-urlencoded 和 application/json 两种类型,后续专栏介绍。
1、普通参数
可以使用 @RequestParam 注解接收,或者HttpServletRequest#getParameter接收,详细细节后续篇章介绍。
这里只能处理 x-www-form-urlencoded 类型的请求,get和post方式都可以,但 json 不行。
@ResponseBody @RequestMapping("/testParam") public String testParam(HttpServletRequest request, String xxx, @RequestParam("xxx2") String xxx2) { System.out.println("xxx:" + xxx); System.out.println("xxx2:" + xxx2); String xxx3 = request.getParameter("xxx2"); System.out.println("xxx3:" + xxx3); return "hello"; } 
2、POJO 类型参数
这种情况,不能使用 @RequestParam 接收,直接写实体即可,效果如下。
这里是 x-www-form-urlencoded 的示例,如果是 json 请求,直接参考上面章节,使用 @RequestBody。
@ResponseBody @RequestMapping("/testParam2") public Student testParam2(Student student) { student.setName("战神"); return student; } 
Tips:SpringMVC会使用构造器实例化出一个pojo类对象,即Student,然后使用setXxx()方法进行赋值,如果没有构造器会报错,没有set方法那么这个成员变量就为空null值。
3、其他类型
还有数组、List、Map,以及相应的复杂结构,后续章节展开介绍。
HttpMessageConverter,报文信息转换器,将请求报文转换为Java对象,或将Java对象转换为响应报文。
HttpMessageConverter提供了两个注解和两个类型:
@RequestBody,@ResponseBody,RequestEntity,ResponseEntity
@RequestBody
@RequestBody 可以获取请求体,需要在控制器方法设置一个形参,使用@RequestBody进行标识,当前请求的请求体就会为当前注解所标识的形参赋值。
// 输出结果:requestBody:username=admin&password=123456 @RequestMapping("/testRequestBody") public String testRequestBody(@RequestBody String requestBody){ System.out.println("requestBody:"+requestBody); return "success"; } RequestEntity
RequestEntity封装请求报文的一种类型,需要在控制器方法的形参中设置该类型的形参,当前请求的请求报文就会赋值给该形参,可以通过getHeaders()获取请求头信息,通过getBody()获取请求体信息。
@RequestMapping("/testRequestEntity") public String testRequestEntity(RequestEntity requestEntity){ System.out.println("requestHeader:"+requestEntity.getHeaders()); System.out.println("requestBody:"+requestEntity.getBody()); return "success"; } 输出结果:
requestHeader:[host:“localhost:8080”, connection:“keep-alive”, content-length:“27”, cache-control:“max-age=0”, sec-ch-ua:“” Not A;Brand";v=“99”, “Chromium”;v=“90”, “Google Chrome”;v=“90"”, sec-ch-ua-mobile:“?0”, upgrade-insecure-requests:“1”, origin:“http://localhost:8080”, user-agent:“Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36”]
requestBody:username=admin&password=123
@ResponseBody
@ResponseBody用于标识一个控制器方法,可以将该方法的返回值直接作为响应报文的响应体响应到浏览器
@ResponseBody 处理 json
a>导入jackson的依赖
com.fasterxml.jackson.core jackson-databind 2.12.1 b>在SpringMVC的核心配置文件中开启mvc的注解驱动,此时在HandlerAdaptor中会自动装配一个消息转换器:MappingJackson2HttpMessageConverter,可以将响应到浏览器的Java对象转换为Json格式的字符串
c>在处理器方法上使用@ResponseBody注解进行标识
d>将Java对象直接作为控制器方法的返回值返回,就会自动转换为Json格式的字符串
@RestController注解
@RestController注解是springMVC提供的一个复合注解,标识在控制器的类上,就相当于为类添加了@Controller注解,并且为其中的每个方法添加了@ResponseBody注解。
ResponseEntity
ResponseEntity 用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文。
使用 ResponseEntity 实现下载文件的功能
@RequestMapping("/testDown") public ResponseEntity testResponseEntity(HttpSession session) throws IOException { //获取ServletContext对象 ServletContext servletContext = session.getServletContext(); //获取服务器中文件的真实路径 String realPath = servletContext.getRealPath("/static/img/1.jpg"); //创建输入流 InputStream is = new FileInputStream(realPath); //创建字节数组 byte[] bytes = new byte[is.available()]; //将流读到字节数组中 is.read(bytes); //创建HttpHeaders对象设置响应头信息 MultiValueMap headers = new HttpHeaders(); //设置要下载方式以及下载文件的名字 headers.add("Content-Disposition", "attachment;filename=1.jpg"); //设置响应状态码 HttpStatus statusCode = HttpStatus.OK; //创建ResponseEntity对象 ResponseEntity responseEntity = new ResponseEntity<>(bytes, headers, statusCode); //关闭输入流 is.close(); return responseEntity; } 文件上传
文件上传要求form表单的请求方式必须为post,并且添加属性enctype=“multipart/form-data”
SpringMVC中将上传的文件封装到MultipartFile对象中,通过此对象可以获取文件相关信息。
1、添加依赖:
commons-fileupload commons-fileupload 1.3.1 2、在SpringMVC的配置文件中添加配置(注意ID命名一定是 multipartResolver):
3、编写控制层代码:
@RequestMapping("/testUp") public String testUp(MultipartFile photo, HttpSession session) throws IOException { //获取上传的文件的文件名 String fileName = photo.getOriginalFilename(); //处理文件重名问题 String hzName = fileName.substring(fileName.lastIndexOf(".")); fileName = UUID.randomUUID().toString() + hzName; //获取服务器中photo目录的路径 ServletContext servletContext = session.getServletContext(); String photoPath = servletContext.getRealPath("photo"); File file = new File(photoPath); if(!file.exists()){ file.mkdir(); } String finalPath = photoPath + File.separator + fileName; //实现上传功能 photo.transferTo(new File(finalPath)); return "success"; } SpringMVC 中的拦截器用于拦截控制器方法的执行。
SpringMVC 中的拦截器需要实现 HandlerInterceptor,必须在SpringMVC的配置文件中进行配置:
拦截器的三个抽象方法
SpringMVC中的拦截器有三个抽象方法:
1、preHandle:控制器方法执行之前执行preHandle(),其boolean类型的返回值表示是否拦截或放行,返回true为放行,即调用控制器方法;返回false表示拦截,即不调用控制器方法
2、postHandle:控制器方法执行之后执行postHandle()
3、afterComplation:处理完视图和模型数据,渲染视图完毕之后执行afterComplation()
拦截器的执行顺序
浏览器 - 过滤器 - DispatcherServlet - preHandle - 控制层 - postHandle - 视图渲染 - afterComplation
多个拦截器的执行顺序
a>若每个拦截器的preHandle()都返回true
此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关:
preHandle()会按照配置的顺序执行,而postHandle()和afterComplation()会按照配置的反序执行
b>若某个拦截器的preHandle()返回了false
preHandle()返回false和它之前的拦截器的preHandle()都会执行,postHandle()都不执行,返回false的拦截器之前的拦截器的afterComplation()会执行
拦截器源码分析
参考:DispatcherServlet#doDispatch
部分代码如下,关于执行顺序也从源码里面可以看出来,执行流程其实围绕着
处理执行链 HandlerExecutionChain 进行。
if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { HandlerInterceptor[] interceptors = getInterceptors(); if (!ObjectUtils.isEmpty(interceptors)) { for (int i = 0; i < interceptors.length; i++) { HandlerInterceptor interceptor = interceptors[i]; if (!interceptor.preHandle(request, response, this.handler)) { triggerAfterCompletion(request, response, null); return false; } this.interceptorIndex = i; } } return true; } 具体示例:
//Step1. 自定义的拦截器,实现的接口HandlerInterceptor public class CustomInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //预处理,返回true则继续执行。如果需要登录校验,校验不通过返回false即可,通过则返回true。 System.out.println("执行preHandle()方法"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { //后处理 System.out.println("执行postHandle()方法"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //在DispatcherServlet完全处理完请求后被调用 System.out.println("执行afterCompletion()方法"); } } /** * Step2. 注册拦截器到容器中,/**代表所有路径 * @param registry 拦截器注册表对象 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new CustomInterceptor()).addPathPatterns("/**"); } 此篇文章介绍了SpringMVC 项目的基础搭建和一些知识介绍,仅供参考。
后续还会继续从 SpringMVC 的常见用法、源码分析、扩展点分析、企业实战等方面展开。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。
