一、SpringMVC 核心认知:为什么用它?
1.1 SpringMVC 是什么?
SpringMVC 是 Spring 框架的一部分,是基于Java实现MVC模型的轻量级Web框架,核心作用是对 Servlet 进行封装,简化Web开发,主要负责:
- Controller 如何接收前端请求和数据
- 如何将请求/数据转发给业务层
- 如何将响应数据转换成JSON返回给前端
1.2 SpringMVC vs 原生Servlet:开发效率对比
| 开发方式 | 核心特点 | 代码量 | 维护性 | 适用场景 |
|---|---|---|---|---|
| 原生Servlet | 需手动继承HttpServlet、重写doGet/doPost、手动解析请求参数、处理响应 |
冗余,每个请求对应一个Servlet | 耦合度高,多请求需大量if-else判断 | 原理学习、简单小项目 |
| SpringMVC | 基于注解开发,@Controller定义控制器、@RequestMapping映射请求、自动参数绑定 |
极简,一个Controller可处理多个请求 | 解耦,职责清晰,便于维护 | 企业级Web项目、前后端分离项目 |
代码示例对比:
原生Servlet实现/user/save请求:
@WebServlet("/user/save")
public class UserSaveServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
System.out.println("servlet save name ==> " + name);
resp.setContentType("text/json;charset=utf-8");
PrintWriter pw = resp.getWriter();
pw.write("{\"module\":\"servlet save\"}");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
SpringMVC实现/save请求:
@Controller
public class UserController {
@RequestMapping("/save")
@ResponseBody
public String save(String name){
System.out.println("springmvc save name ==> " + name);
return "{\"module\":\"springmvc save\"}";
}
}
1.3 SpringMVC、Servlet、Tomcat 三者关系
| 技术 | 本质 | 核心作用 | 通俗类比 |
|---|---|---|---|
| Servlet | Java EE定义的服务器端组件规范(一套接口) | 定义Web组件与服务器的交互规则,处理请求/响应 | 「空白的办事窗口」 |
| Tomcat | 开源Servlet容器+HTTP服务器 | 提供Servlet运行环境,管理Servlet生命周期,处理网络通信 | 「物业公司」 |
| SpringMVC | 基于Servlet规范的MVC框架 | 封装Servlet重复工作,简化Web开发 | 「智能化办事窗口系统」 |
完整工作流程:
- 客户端发HTTP请求 → Tomcat作为HTTP服务器接收,封装成
HttpServletRequest - Tomcat将请求转发给
DispatcherServlet(SpringMVC核心,本质是Servlet) DispatcherServlet通过HandlerMapping匹配请求URL到Controller方法,HandlerAdapter执行方法- Controller返回结果 →
DispatcherServlet渲染视图/转换JSON,通过HttpServletResponse返回给客户端
1.4 SpringMVC 父子容器设计
SpringMVC 采用父子容器架构,实现分层解耦:
- 根容器 (Root WebApplicationContext)
管理非 Web 层的 Bean:业务层(@Service)、数据层(@Repository、MyBatis、数据源等),全局唯一,所有子容器共享。 - 子容器 (Servlet WebApplicationContext)
管理 Web 层的 Bean:@Controller、拦截器、视图解析器等,每个DispatcherServlet对应一个(通常只有一个)。 - 父子关系:子容器可以访问根容器的 Bean(如 Controller 注入 Service),但根容器不能访问子容器的 Bean。
- 设计目的:业务层不依赖 Web 技术,便于迁移到其他项目(如控制台应用、RPC服务等)。
二、SpringMVC 入门案例:从零搭建第一个项目
2.1 开发环境准备
- IDEA 开发工具
- JDK 1.8+
- Maven 3.6+
- Tomcat 8.5+
2.2 项目搭建步骤(纯注解版,无web.xml)
步骤1:创建Maven Web项目
打开IDEA → 新建Module → 选择Maven → 勾选Create from archetype → 选择org.apache.maven.archetypes:maven-archetype-webapp → 填写项目坐标(GroupId、ArtifactId)→ 完成创建。
步骤2:补全项目目录结构
骨架创建的目录不完整,手动补全:
src/main/java:存放Java代码src/main/resources:存放配置文件src/test/java:存放测试代码
步骤3:导入pom.xml依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itheima</groupId>
<artifactId>springmvc_01_quickstart</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>80</port>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
</project>
补充说明 :SpringMVC是基于Spring的,在pom.xml只导入了spring-webmvc jar包,它会自动依赖Spring相关坐标(如spring-core、spring-context等),无需手动导入。
servlet的坐标为什么需要添加 <scope>provided</scope>?
- scope是maven中jar包依赖作用范围的描述
- 如果不设置默认是
compile,在编译、运行、测试时均有效 - 如果运行有效的话就会和tomcat中的servlet-api包发生冲突,导致启动报错
provided代表的是该包只在编译和测试的时候用,运行的时候无效直接使用tomcat中的,就避免冲突
步骤4:创建SpringMVC配置类
@Configuration
@ComponentScan("com.itheima.controller")
public class SpringMvcConfig {
}
步骤5:创建Servlet容器初始化类
替代传统web.xml,初始化SpringMVC环境,拦截所有请求。
基础写法(仅配置子容器):
public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(SpringMvcConfig.class);
return ctx;
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
}
对比写法:同时配置根容器(加载业务层、数据层):
public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(SpringMvcConfig.class);
return ctx;
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected WebApplicationContext createRootApplicationContext() {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(SpringConfig.class);
return ctx;
}
}
说明 :createRootApplicationContext方法如果返回非空,则创建根容器;如果返回null,表示不创建根容器(所有Bean都放在子容器中)。实际企业开发中通常需要根容器来管理业务层和数据层Bean。
关于 getServletMappings 的路径规则:
return new String[]{"/"};→ 所有请求都交给 SpringMVC(最常用)return new String[]{"/user/*"};→ 只有 /user/ 开头的请求交给 SpringMVCreturn new String[]{"/api/*"};→ 只有 /api/ 开头的请求交给 SpringMVC- 也可同时配置多个:
return new String[]{"/user/*", "/api/*"};
关于 AbstractDispatcherServletInitializer 的三个方法:
createServletApplicationContext:创建 Servlet 容器时,加载 SpringMVC 对应的 bean 并放入WebApplicationContext对象范围中(作用域为ServletContext,即整个 web 容器范围)getServletMappings:设定 SpringMVC 对应的请求映射路径,即 SpringMVC 拦截哪些请求createRootApplicationContext:如果需要加载非 SpringMVC 对应的 bean(如业务层、数据层),使用当前方法创建根容器,用法与createServletApplicationContext相同
优化初始化类:简化代码
Spring提供AbstractAnnotationConfigDispatcherServletInitializer抽象类,简化容器初始化:
public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{SpringConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringMvcConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
优势:无需手动创建AnnotationConfigWebApplicationContext,代码更简洁,职责更清晰。
结论:优化初始类代码是对比写法的官方标准优化方案,是Spring提供的更简洁、更规范的实现,两种代码实现的功能完全一致,但写法和封装层级不同。
1. 两种代码的核心对应关系
| (AbstractDispatcherServletInitializer) | (AbstractAnnotationConfigDispatcherServletInitializer) | 作用 |
|---|---|---|
| createRootApplicationContext() 手动创建根容器,注册SpringConfig.class | getRootConfigClasses() 直接返回配置类数组 | 加载Spring根容器(业务层、数据层等非Web组件) |
| createServletApplicationContext() 手动创建Servlet容器,注册SpringMvcConfig.class | getServletConfigClasses() 直接返回配置类数组 | 加载SpringMVC子容器(Controller、视图解析器等Web组件) |
| getServletMappings() 返回"/" | getServletMappings() 返回"/" | 配置DispatcherServlet的拦截路径 |
2. 优化的核心原理
- 父类封装了重复逻辑 :AbstractAnnotationConfigDispatcherServletInitializer 是 AbstractDispatcherServletInitializer 的子类,它已经在父类基础上,帮你封装了AnnotationConfigWebApplicationContext的创建、注册等重复代码,只需要你返回配置类即可。
- 代码更简洁:对比写法需要手动new AnnotationConfigWebApplicationContext()、ctx.register()、return ctx,优化初始类代码直接返回配置类数组,减少了样板代码。
- 职责更清晰 :抽象类将"容器创建"和"配置类指定"解耦,开发者只需要关注配置类本身,不需要关心容器的实例化细节,符合Spring的"约定大于配置"思想。
- 功能完全等价 :两种代码最终都会创建父子容器(根容器+Servlet容器),并完成DispatcherServlet的初始化,运行效果完全一致。
3. 补充说明
- 对比写法是基础实现,适合理解Spring MVC容器初始化的底层原理;
- 优化初始类代码是Spring官方推荐的简化写法,是实际项目中的标准用法,避免了手动创建容器的冗余代码,降低了出错概率。
步骤6:创建Controller类
@Controller
public class UserController {
@RequestMapping("/save")
@ResponseBody
public String save(){
System.out.println("user save ...");
return "{\"info\":\"springmvc\"}";
}
}
步骤7:配置Tomcat并启动项目
IDEA中配置Tomcat服务器, 启动Tomcat → 浏览器访问http://localhost/save,即可看到返回的JSON数据。
三、配置类层级与完整启动加载链路
3.1 配置类层级与职责表
| 配置类 | 所属容器 | 核心职责 | 管理的 Bean 范围 |
|---|---|---|---|
ServletContainersInitConfig |
Tomcat 入口 | 替代 web.xml,创建父子容器,注册 DispatcherServlet | 不管理 Bean,仅负责初始化 |
SpringConfig |
根容器 | 管理业务层、数据层 | @Service、@Repository、数据源、MyBatis、事务等 |
SpringMvcConfig |
子容器 | 管理表现层 | @Controller、拦截器、消息转换器、视图解析器等 |
JdbcConfig |
根容器(被 SpringConfig 引入) | 管理 JDBC 基础组件 | DataSource 数据源、连接池等 |
MybatisConfig |
根容器(被 SpringConfig 引入) | 管理 MyBatis 框架组件 | SqlSessionFactory、Mapper 扫描、事务管理器等 |
3.2 完整的启动加载链路(时序)
1. Tomcat 启动 → 通过 SPI 机制扫描到 ServletContainersInitConfig
2. Tomcat 调用该类的三个方法:
- createRootApplicationContext()
→ 创建根容器
→ 注册 SpringConfig
→ SpringConfig 通过 @Import 引入 JdbcConfig、MybatisConfig
→ JdbcConfig 初始化 DataSource
→ MybatisConfig 依赖 DataSource 初始化 SqlSessionFactory 等
→ SpringConfig 扫描所有非 Web 层 Bean(排除 @Controller)
- createServletApplicationContext()
→ 创建子容器
→ 注册 SpringMvcConfig
→ SpringMvcConfig 扫描 @Controller,配置 MVC 组件
- getServletMappings() → 返回 "/",将所有请求交给 DispatcherServlet
3. 子容器以根容器为父容器 → 子容器可访问根容器的 Bean
4. DispatcherServlet 初始化
- 加载 HandlerMapping(处理器映射器,记录 @RequestMapping 路径和 Controller 方法的对应关系)
- 准备 HandlerAdapter(处理器适配器,负责调用 Controller 方法)等组件
5. 项目启动完成,等待请求
四、SpringMVC 工作流程深度解析
4.1 启动服务器初始化过程
- Tomcat启动 :执行
ServletContainersInitConfig,初始化Web容器(替代web.xml) - 创建SpringMVC子容器 :执行
createServletApplicationContext(),创建AnnotationConfigWebApplicationContext,加载SpringMvcConfig配置类 - 扫描Bean :
@ComponentScan扫描com.itheima.controller包,创建UserController等Bean - 建立请求映射 :加载
@RequestMapping,建立/save与save()方法的对应关系(注意:所有映射统一存放在HandlerMapping中,并非放在每个Controller Bean里) - 设置拦截规则 :执行
getServletMappings(),设置/拦截所有请求 - DispatcherServlet初始化 :加载
HandlerMapping、HandlerAdapter等核心组件
4.2 单次请求处理过程
- 发送请求 :浏览器访问
http://localhost/save - Tomcat拦截请求 :Web容器判断请求符合SpringMVC拦截规则(
/),将请求交给DispatcherServlet处理 - 解析请求路径 :
DispatcherServlet解析请求URL,得到路径/save - 匹配执行方法 :通过
HandlerMapping找到/save对应的UserController.save()方法,再通过HandlerAdapter执行该方法 - 执行方法 :调用
save()方法,打印日志,返回字符串 - 返回响应 :检测到
@ResponseBody注解,将返回值直接作为响应体(JSON格式)返回给浏览器
五、Bean加载控制:Spring与SpringMVC的职责划分
5.1 问题背景
项目中存在两类Bean:
- SpringMVC控制的Bean :表现层Bean,如
@Controller标记的类 - Spring控制的Bean :业务层Bean(
@Service)、数据层Bean(@Repository)、工具类Bean等
如果两个配置类都扫描根包com.itheima,会导致Bean重复加载,因此需要做加载范围隔离。
5.2 解决方案:两种隔离方式
方式一:精准指定扫描包(推荐)
@Configuration
@ComponentScan({"com.itheima.service","com.itheima.dao"})
public class SpringConfig {
}
@Configuration
@ComponentScan("com.itheima.controller")
public class SpringMvcConfig {
}
补充说明 :上述只是通过例子说明可以精确指定让Spring扫描对应的包结构。真正在做开发的时候,因为Dao最终是交给MapperScannerConfigurer对象来进行扫描处理的,我们只需要将其扫描到service包即可(因为现在使用的是MyBatis自动代理的方式来创建实现对象,所以即使不写dao包的扫描,也能通过Mapper代理生成)。但是,如果项目组不用MyBatis技术而用JDBC或者数据层实现的其他技术,这个时候就不能不写扫描dao包,否则数据层的Bean都没有,无法操作数据库。因此,写上dao包的扫描,属于标准开发,无论使用什么数据层技术都能正常使用。
方式二:排除指定注解
@Configuration
@ComponentScan(
value = "com.itheima",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = Controller.class
)
)
public class SpringConfig {
}
详细说明:
excludeFilters属性:设置扫描加载bean时,排除的过滤规则type属性:设置排除规则,当前使用按照bean定义时的注解类型进行排除ANNOTATION:按照注解排除ASSIGNABLE_TYPE:按照指定的类型过滤ASPECTJ:按照Aspectj表达式排除(基本不会用)REGEX:按照正则表达式排除CUSTOM:按照自定义规则排除
classes属性:设置排除的具体注解类,当前设置排除@Controller定义的bean
八、总结
SpringMVC的核心是对Servlet的封装,通过注解简化开发,实现请求的分发、处理和响应。入门阶段重点掌握:
- 纯注解版项目搭建流程,替代传统
web.xml - 核心注解
@Controller、@RequestMapping、@ResponseBody的使用 - SpringMVC完整工作流程,理解每个环节的作用(包括
DispatcherServlet、HandlerMapping、HandlerAdapter) - 父子容器设计:根容器管理业务/数据层,子容器管理表现层,子容器可访问父容器Bean,反之不行
- Bean加载隔离:通过精准扫描包或排除注解,避免重复加载
- 初始化类的演进 :从
AbstractDispatcherServletInitializer到AbstractAnnotationConfigDispatcherServletInitializer的简化写法