SpringMVC 入门到精通:Servlet 对比、工作流程、Bean 加载控制完整指南(Spring系列11)

一、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/ 开头的请求交给 SpringMVC
  • return 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 启动服务器初始化过程

  1. Tomcat启动 :执行ServletContainersInitConfig,初始化Web容器(替代web.xml)
  2. 创建SpringMVC子容器 :执行createServletApplicationContext(),创建AnnotationConfigWebApplicationContext,加载SpringMvcConfig配置类
  3. 扫描Bean@ComponentScan扫描com.itheima.controller包,创建UserController等Bean
  4. 建立请求映射 :加载@RequestMapping,建立/savesave()方法的对应关系(注意:所有映射统一存放在HandlerMapping中,并非放在每个Controller Bean里)
  5. 设置拦截规则 :执行getServletMappings(),设置/拦截所有请求
  6. DispatcherServlet初始化 :加载HandlerMappingHandlerAdapter等核心组件

4.2 单次请求处理过程

  1. 发送请求 :浏览器访问http://localhost/save
  2. Tomcat拦截请求 :Web容器判断请求符合SpringMVC拦截规则(/),将请求交给DispatcherServlet处理
  3. 解析请求路径DispatcherServlet解析请求URL,得到路径/save
  4. 匹配执行方法 :通过HandlerMapping找到/save对应的UserController.save()方法,再通过HandlerAdapter执行该方法
  5. 执行方法 :调用save()方法,打印日志,返回字符串
  6. 返回响应 :检测到@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的封装,通过注解简化开发,实现请求的分发、处理和响应。入门阶段重点掌握:

  1. 纯注解版项目搭建流程,替代传统web.xml
  2. 核心注解@Controller@RequestMapping@ResponseBody的使用
  3. SpringMVC完整工作流程,理解每个环节的作用(包括DispatcherServletHandlerMappingHandlerAdapter
  4. 父子容器设计:根容器管理业务/数据层,子容器管理表现层,子容器可访问父容器Bean,反之不行
  5. Bean加载隔离:通过精准扫描包或排除注解,避免重复加载
  6. 初始化类的演进 :从AbstractDispatcherServletInitializerAbstractAnnotationConfigDispatcherServletInitializer的简化写法
相关推荐
小江的记录本2 小时前
【JEECG Boot】 JEECG Boot 数据字典管理——六大核心功能(内含:《JEECG Boot 数据字典开发速查清单》)
java·前端·数据库·spring boot·后端·spring·mybatis
小江的记录本2 小时前
【JEECG Boot】 JEECG Boot——Online表单 系统性知识体系全解
java·前端·spring boot·后端·spring·低代码·mybatis
Mr_Xuhhh2 小时前
[特殊字符] 《网络知识和Servlet重点知识整理》
网络·servlet
都说名字长不会被发现2 小时前
Spring 线程池最佳实践:如何优雅管理多线程任务
java·spring·线程池·并发编程
wok1572 小时前
WebMVC 和 WebFlux 架构选型
java·spring·架构·mvc
希望永不加班2 小时前
SpringBoot 邮件发送:文本邮件与 HTML 邮件
java·spring boot·后端·spring·html
漫霂2 小时前
SpringSecurity入门应用
java·数据库·spring
杰克尼14 小时前
springCloud_day07(MQ高级)
java·spring·spring cloud
杰克尼16 小时前
SpringCloud_day05
后端·spring·spring cloud