Spring MVC 导引
一句话定位:Spring MVC 并非独立于 Spring 容器之外的新世界,而是构建在 IoC 容器之上的 Web 层扩展。理解这一点,是打通"容器思维"与"Web 开发"任督二脉的关键。本章承上启下,只回答一个问题:DispatcherServlet 与 ApplicationContext 到底是什么关系?
Spring MVC 与 IoC 容器的关系
Spring MVC 的全称是 Spring Web MVC,它是 Spring Framework 的 Web 模块(spring-webmvc)提供的基于 Servlet API 的 Web 框架。它的核心设计哲学是:所有 Web 层组件(Controller、Interceptor、ViewResolver 等)都是 Spring 容器中的 Bean。
这意味着:
- Controller 是 Bean:你写的
@Controller或@RestController类,本质上和@Service、@Repository一样,由 Spring 容器实例化、组装依赖、管理生命周期 - DispatcherServlet 从容器获取 Bean:前端控制器
DispatcherServlet本身被注册为 Spring 管理的 Bean,它通过容器查找HandlerMapping、HandlerAdapter、Controller等协作组件 - 依赖注入贯穿 Web 层:Controller 中注入 Service、Service 中注入 Repository,这条依赖链完全由容器的 DI 机制支撑
非 Web 容器 vs Web 容器的演进
关键理解:WebApplicationContext 是 ApplicationContext 的子接口,它额外提供了 ServletContext 的访问能力。Spring MVC 没有创造新的容器,而是复用并扩展了已有的 IoC 容器。
DispatcherServlet 如何从容器获取 Bean
DispatcherServlet 的容器层级
在典型的 Spring MVC 应用中,存在两个 IoC 容器层级:
| 层级 | 容器类型 | 职责 | 包含的 Bean |
|---|---|---|---|
| 父容器(Root Context) | WebApplicationContext | 管理业务层和数据层 Bean | Service、Repository、DataSource、TransactionManager |
| 子容器(Servlet Context) | WebApplicationContext | 管理 Web 层 Bean | Controller、ViewResolver、HandlerMapping、Interceptor |
父子容器规则:
- 子容器可以访问父容器中的 Bean(Controller 可以注入 Service)
- 父容器不能访问子容器中的 Bean(Service 不能注入 Controller)
- 这种隔离确保了 Web 层组件不会污染业务层,同时业务层可以被多个 DispatcherServlet(子容器)共享
请求处理时的容器交互
当一个 HTTP 请求到达时,DispatcherServlet 与容器的交互流程如下:
核心洞察:
DispatcherServlet不是靠自己new出 Controller,而是像所有普通 Bean 一样,通过applicationContext.getBean()从容器的 Bean 定义注册表中查找- Controller 中的
@Autowired依赖(如 Service)在容器初始化阶段就已经完成注入,请求到来时直接调用即可 - 这意味着:Spring MVC 的请求处理本质上是容器内 Bean 之间的协作调用
Controller 也是 Bean
Controller 的生命周期
你写在 @Controller 或 @RestController 类中的每一个实例变量、每一个 @Autowired 字段,都遵循与普通 Bean 完全相同的规则:
与普通 Bean 的唯一区别:Controller 类上多了一层 Web 语义标记(@Controller 或 @RestController),使得 RequestMappingHandlerMapping 能够识别它并建立 URL 到方法的映射。但实例化、注入、生命周期管理完全由容器负责。
提及但不展开的 MVC 注解
以下注解是 Spring MVC 请求映射和处理的核心,但本章不展开讲解其用法,仅列出名称供后续章节学习:
@Controller:标记类为 Web 控制器(本质上是@Component的派生)@RestController:@Controller+@ResponseBody的组合@RequestMapping及其派生:@GetMapping、@PostMapping、@PutMapping、@DeleteMapping@RequestParam、@PathVariable、@RequestBody、@ResponseBody@ModelAttribute、@ExceptionHandler、@ControllerAdvice
这些注解的详细用法、参数绑定规则、RESTful 设计实践,将在后续专门的 Spring MVC 章节中系统讲解。本章只需建立认知:它们都是标注在容器 Bean 上的元数据,由 DispatcherServlet 在运行时读取并驱动请求分发。
完整示例
场景简述
飞翔科技公司的学生成绩管理系统已经完成了 Service 层和 Repository 层的开发。架构师白歌要求小崔接入 Web 层,暴露 HTTP 接口供前端黄俪调用。小崔需要理解:Controller 不是凭空创建的,而是从已有的 IoC 容器中"生长"出来的 Web 端点。
操作前:纯容器内的 Service 层
// 操作前:只有 Service 和 Repository,没有 Web 层
package com.feixiang.student.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class StudentService {
private final StudentRepository studentRepository;
@Autowired
public StudentService(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
public Student findById(Long id) {
return studentRepository.findById(id);
}
}
此时应用可以通过 AnnotationConfigApplicationContext 启动,但没有任何 HTTP 入口。前端黄俪无法调用。
接入 Web 层后的完整代码
小崔添加 Controller 类:
package com.feixiang.student.controller;
import com.feixiang.student.domain.Student;
import com.feixiang.student.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 学生信息控制器
*
* 注意:这个 Controller 是 Spring 容器中的一个普通 Bean,
* 它的 StudentService 依赖在容器启动时就已经注入完成。
*/
@RestController
@RequestMapping("/api/students")
public class StudentController {
private final StudentService studentService;
@Autowired
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
@GetMapping("/{id}")
public Student getStudent(@PathVariable Long id) {
return studentService.findById(id);
}
}
启动类(Spring Boot 自动配置内嵌 Tomcat 和 DispatcherServlet):
package com.feixiang.student;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StudentManagementApplication {
public static void main(String[] args) {
SpringApplication.run(StudentManagementApplication.class, args);
}
}
操作后运行结果及分析
启动应用,控制台输出:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.18)
2024-05-20 11:30:15.123 INFO 12345 --- [main] o.s.b.w.e.t.TomcatWebServer :
Tomcat initialized with port(s): 8080 (http)
2024-05-20 11:30:15.456 INFO 12345 --- [main] o.s.b.w.s.ServletContextInitializerBeans :
Mapping servlet: 'dispatcherServlet' to [/]
2024-05-20 11:30:15.789 INFO 12345 --- [main] c.f.s.StudentManagementApplication :
Started StudentManagementApplication in 2.456 seconds
使用 curl 测试接口:
curl http://localhost:8080/api/students/2024001
响应:
{
"id": 2024001,
"name": "小崔",
"major": "计算机科学与技术",
"score": 92.5
}
分析:
- 容器启动:
StudentManagementApplication启动时,@SpringBootApplication触发组件扫描,StudentController、StudentService、StudentRepository全部被注册为 Bean。 - 依赖注入:
StudentController的构造器参数StudentService在容器初始化阶段被注入。 - DispatcherServlet 注册:Spring Boot 自动配置将
DispatcherServlet注册到 Tomcat,映射路径为/。 - 请求处理:
curl请求到达时,Tomcat 交给DispatcherServlet,后者从 Servlet WebApplicationContext 中查找HandlerMapping,映射到StudentController.getStudent()方法,调用并返回 JSON。
整个过程中,Controller 没有离开过容器。它和普通 Service 的区别仅在于:它的方法被 URL 映射元数据标注,可以被 DispatcherServlet 通过反射调用。
易错场景与面试考点
易错场景:试图在 Controller 中手动 new Service
小崔最初不理解 Controller 是容器 Bean,写出了这样的代码:
// 错误示范:手动创建 Service,脱离容器管理
@RestController
@RequestMapping("/api/students")
public class StudentController {
private final StudentService studentService = new StudentService(new StudentRepository());
// ← 错误!手动 new 出来的对象不在 Spring 容器中
@GetMapping("/{id}")
public Student getStudent(@PathVariable Long id) {
return studentService.findById(id);
}
}
后果:
StudentRepository没有 DataSource 注入,因为手动new的对象不经过容器StudentService上的@Transactional失效,因为 AOP 代理由容器创建- 如果
StudentRepository依赖JdbcTemplate,则NullPointerException
正确做法:所有依赖必须通过容器注入,Controller 中绝不应该出现 new Service()。
面试考点
Q:DispatcherServlet 和 ApplicationContext 是什么关系?
DispatcherServlet是 Spring MVC 的前端控制器,它本身被注册为 Spring 容器中的一个 Bean。在运行时,它通过持有的WebApplicationContext引用,从容器中查找HandlerMapping、HandlerAdapter、Controller等协作组件。可以说,DispatcherServlet 是容器的使用者,而非容器的替代者。
Q:Spring MVC 中为什么需要父子容器?
父容器(Root Context)管理业务层和数据层 Bean,子容器(Servlet Context)管理 Web 层 Bean。父子容器实现了关注点隔离:Web 层组件可以访问业务层,但业务层不感知 Web 层。同时,多个 DispatcherServlet 可以共享同一个父容器中的 Service 和 Repository,避免重复创建。
Q:Controller 是单例还是多例?
默认是 singleton(单例),与所有其他 Spring Bean 一样。这意味着 Controller 的实例变量会被所有请求共享,因此 Controller 中不应持有请求级别的可变状态。如果需要请求级状态,应使用方法参数或 ThreadLocal。
Q:Spring Boot 中为什么没有显式配置 DispatcherServlet?
Spring Boot 的
DispatcherServletAutoConfiguration自动检测类路径中的spring-webmvc,自动创建DispatcherServletBean 并注册到内嵌 Tomcat。开发者只需写 Controller,无需关心 Servlet 注册细节。这是自动配置哲学的典型体现。
教程学习路径总结
至此,本教程的 Spring Core 核心内容已全部覆盖。让我们回顾从容器到 Web 的完整学习路径:
学习建议:
- 不要跳过容器基础:Spring MVC 的所有魔法都建立在 IoC 容器之上。如果不懂 Bean 生命周期和依赖注入,遇到
@Transactional失效、AOP 不生效等问题时将无从下手。 - 重视条件注解:
@ConditionalOnClass、@ConditionalOnProperty、@ConditionalOnMissingBean是阅读 Spring Boot 源码和排查自动配置问题的必备工具。 - 从容器视角看 Web:当你写
@RestController时,记住你只是在定义一个带有 URL 映射元数据的普通 Bean。请求处理的本质是容器内 Bean 的协作调用。
愿你朝华相顾,愿你前程似锦。 容器之道,一通百通。