Spring 原理总览:从启动到请求执行
前面出过一篇 Spring Bean 生命周期的原理文章,今天再出一篇关于 Spring 启动工作,以及一个 http 请求的后端链路原理,希望能帮你构建起知识图谱。
Spring 的很多能力,看起来分布在不同模块里:
@Autowired负责依赖注入@Transactional负责声明式事务@GetMapping负责请求映射@Component、@Service负责组件注册WebMvcConfigurer负责扩展 MVC 配置
这些能力背后,其实都指向同一组底层问题:
- 为什么加一个
@Service,对象就能被 Spring 管起来? - 为什么加一个
@Autowired,成员变量就能被自动赋值? - 为什么写一个
WebConfig,就能影响所有请求? - 为什么
@Transactional能自动开启、提交、回滚事务? - 为什么请求来了,Spring MVC 能准确找到某个 Controller 方法?
这些问题看起来分散,其实背后是同一个答案:
Spring 是一个"启动期构建元数据,运行期按规则执行"的框架。
只要抓住这句话,IOC、AOP、MVC、事务、拦截器这些知识点,就能被放回同一套框架里理解。
目录:
- Spring 最核心的职责是什么
- 启动时 Spring 到底在忙什么
- 为什么说 BeanDefinition 是对象说明书
- 请求进来后 Spring MVC 如何找到 Controller
- WebConfig 和 Interceptor 为什么会生效
- Tomcat 在这条链路里负责什么
- Filter、Interceptor、AOP 到底差在哪
- @Transactional 为什么本质上是代理
- 以后怎么用统一视角学习 Spring
Spring 最核心的职责是什么
先从最基础的地方开始。
如果不用 Spring,我们写 Java 对象大概是这样:
java
UserService userService = new UserService();
OrderService orderService = new OrderService(userService);
对象由你自己创建,依赖由你自己传递,生命周期也由你自己控制。
这在小项目里没什么问题。但项目一复杂,对象之间的关系会迅速变成一张网:
- Controller 依赖 Service
- Service 依赖 Repository
- Repository 依赖 DataSource
- Service 之间还可能相互协作
- 有些对象还要加事务、缓存、权限、日志
如果所有对象都靠业务代码自己 new,后面会越来越难维护。
Spring 的第一件事,就是把这些对象统一接管。
比如:
java
@Service
public class UserService {
}
你没有手动 new UserService(),但 Spring 启动后,UserService 会进入 IOC 容器。
可以简单理解成:
text
IOC 容器
├── UserController
├── UserService
├── OrderService
├── UserRepository
└── DataSource
这些被 Spring 管理的对象,就叫 Bean。
所以,IOC 不是一个很玄的概念。它做的事情很朴素:
原来由你自己创建和组装对象,现在交给 Spring 创建和组装。
对象控制权从业务代码转移到容器,这就是"控制反转"。
启动时 Spring 到底在忙什么
Spring Boot 项目通常从这里启动:
java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
这几行代码背后,Spring 做了大量准备工作。
简化一下,大致可以理解为:
text
启动应用
↓
扫描类路径
↓
发现候选 Bean
↓
解析注解和配置
↓
生成 BeanDefinition
↓
注册到 IOC 容器
↓
实例化 Bean
↓
完成依赖注入
↓
执行初始化逻辑
↓
应用准备就绪
注意一个关键点:
Spring 并不是一开始就直接创建所有对象。
它会先分析这些类和配置,生成一批"对象说明书"。这些说明书就是 BeanDefinition。
理解 Spring 启动流程时,不只要看"对象什么时候创建",还要看:
Spring 启动阶段先收集元数据,再根据元数据构建对象和规则。
这也是为什么你写一个注解,Spring 就能知道怎么处理。
注解本身不会自动工作。真正工作的是 Spring 在启动时扫描、解析、注册、构建出来的一整套规则。
为什么说 BeanDefinition 是对象说明书
如果 Bean 是对象本身,那么 BeanDefinition 就是创建这个对象之前的说明书。
它里面会记录类似这些信息:
- 这个 Bean 对应哪个类
- 它是单例还是每次新建
- 它依赖哪些 Bean
- 它有没有初始化方法
- 它有没有销毁方法
- 它有哪些注解信息
- 它是否需要被代理
比如你写:
java
@Service
public class OrderService {
private final UserService userService;
public OrderService(UserService userService) {
this.userService = userService;
}
}
Spring 启动时会识别到:
OrderService是一个需要管理的 Bean- 它构造方法需要一个
UserService - 容器里应该先能找到
UserService - 创建
OrderService时要把UserService注入进去
所以 @Autowired 或构造器注入并不是"运行中突然找对象"。
更准确地说,是 Spring 在容器初始化过程中,根据 Bean 的定义、依赖关系和自动装配规则,把对象关系提前组装好。
这也是为什么 Spring 官方文档会把 Bean 和依赖关系看作配置元数据的一部分。XML、注解、Java Config 只是元数据来源不同,最后都会进入容器的统一处理流程。
你可以把它理解成:
text
注解 / XML / Java Config
↓
配置元数据
↓
BeanDefinition
↓
Bean 实例
↓
可运行的对象关系
这条链路非常重要。
因为后面所有"为什么配置会生效"的问题,本质都能塞回这条链路里。
请求进来后 Spring MVC 如何找到 Controller
接下来换到 Web 场景。
你写一个接口:
java
@RestController
public class UserController {
@GetMapping("/users/{id}")
public UserDTO getUser(@PathVariable Long id) {
return userService.getUser(id);
}
}
表面看,是请求 /users/1 后执行 getUser()。
但 Spring MVC 不可能每次请求来了,才临时全项目搜索哪个方法能处理这个 URL。
真正的逻辑也是:
启动期建表,运行期查表。
启动时,Spring MVC 会扫描 Controller 中的 @RequestMapping、@GetMapping 等注解,建立 URL 到处理方法的映射关系。
可以理解成一张路由表:
text
GET /users/{id} -> UserController#getUser(Long id)
POST /users -> UserController#createUser(UserCreateDTO dto)
DELETE /users/{id} -> UserController#deleteUser(Long id)
请求真的进来时,就不需要重新分析注解了。
运行时只要根据请求路径、HTTP 方法等信息,找到对应的处理方法即可。
这也是为什么 Spring MVC 的核心链路里会出现这些角色:
text
请求
↓
DispatcherServlet
↓
HandlerMapping
↓
HandlerExecutionChain
↓
HandlerAdapter
↓
Controller 方法
↓
HttpMessageConverter
↓
响应
简单翻译一下:
DispatcherServlet:统一接收和调度请求HandlerMapping:根据请求找到处理器HandlerExecutionChain:处理器加上拦截器链HandlerAdapter:用合适的方式调用处理器HttpMessageConverter:把 Java 对象和 HTTP 请求响应体互相转换
所以 DispatcherServlet 不是拦截器。
它更像 Web 层的总调度员,也就是经典的前端控制器模式。
WebConfig 和 Interceptor 为什么会生效
再看一个很常见的问题。
你写了一个配置类:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor);
}
}
为什么这几行代码能影响所有请求?
很多人会误以为:是不是每个请求进来时,都会执行一次 addInterceptors()?
不是。
它真正发生在启动阶段。
大致过程是:
text
Spring MVC 初始化
↓
找到所有 WebMvcConfigurer
↓
执行 addInterceptors()
↓
把拦截器注册到 InterceptorRegistry
↓
保存到 HandlerMapping 相关结构中
所以 registry.addInterceptor(loginInterceptor) 的本质,不是在处理某个请求,而是在注册一条 MVC 规则。
等请求进来时,Spring MVC 已经知道:
text
这个 Controller 调用前后,需要经过哪些 Interceptor
于是一次请求可能变成:
text
请求
↓
LoginInterceptor.preHandle()
↓
TraceInterceptor.preHandle()
↓
Controller
↓
TraceInterceptor.postHandle()
↓
LoginInterceptor.postHandle()
↓
afterCompletion()
这就是 Interceptor 的本质:
它不是 AOP 代理,而是 Spring MVC 请求处理链上的责任链。
它包装的是 Controller 调用前后的 Web 请求流程。
Tomcat 在这条链路里负责什么
前面讲请求链路时,我们会看到:
text
浏览器
↓
Tomcat
↓
Filter
↓
DispatcherServlet
↓
Controller
这里很容易产生一个疑问:
既然已经有了 Filter、DispatcherServlet、Controller,为什么上面还需要一个 Tomcat?
答案是:
Tomcat 负责把"网络请求"变成"Java Web 程序可以处理的 Servlet 请求"。
Filter 和 DispatcherServlet 都不是直接监听端口的。
它们不会自己去做这些事情:
- 监听 8080 端口
- 接收 TCP 连接
- 解析 HTTP 报文
- 维护请求线程
- 创建
HttpServletRequest - 创建
HttpServletResponse - 按 Servlet 规范调用 Filter 和 Servlet
- 管理 Web 应用的生命周期
这些事情,是 Tomcat 这类 Servlet 容器负责的。
可以把层级拆开看:
text
操作系统网络层
↓
Tomcat Connector
↓
HTTP 请求解析
↓
Servlet 容器
↓
FilterChain
↓
DispatcherServlet
↓
Spring MVC
↓
Controller
Spring MVC 的入口是 DispatcherServlet,但 DispatcherServlet 本身也是一个 Servlet。
既然是 Servlet,它就需要运行在 Servlet 容器里。
Tomcat 做的事情,就是提供这个运行环境。
更具体一点:
text
Tomcat
├── 负责监听端口
├── 负责接收 HTTP 请求
├── 负责解析请求报文
├── 负责创建 Request / Response 对象
├── 负责管理线程池
├── 负责执行 Filter 链
└── 负责调用目标 Servlet
而 Spring MVC 接手的是 Tomcat 之后的事情:
text
DispatcherServlet
├── 根据请求查找 HandlerMapping
├── 找到 Controller 方法
├── 执行 Interceptor
├── 调用 HandlerAdapter
├── 执行业务 Controller
└── 通过 HttpMessageConverter 写回响应
所以 Tomcat 和 Spring MVC 的分工可以这样理解:
text
Tomcat:把 HTTP 请求带进 Java 世界
Spring MVC:把 Java Web 请求分发到业务方法
没有 Tomcat 会怎么样?
如果没有 Tomcat,普通 Spring MVC 应用就没有 Servlet 运行环境。
也就是说:
- 没有人监听端口
- 没有人接收浏览器请求
- 没有人解析 HTTP
- 没有人创建
HttpServletRequest - 没有人执行 Filter
- 没有人调用
DispatcherServlet
这时你的 Controller 写得再完整,也不会被浏览器请求触发。
因为请求根本进不了 Spring MVC。
当然,这里说的"没有 Tomcat",不是说一定必须用 Tomcat 这个产品。
你也可以用 Jetty、Undertow 等其他 Web 容器。
Spring Boot 默认常见的是内嵌 Tomcat,所以你平时可能没有显式安装 Tomcat,也能启动 Web 项目:
text
Spring Boot 应用启动
↓
启动内嵌 Tomcat
↓
Tomcat 监听端口
↓
注册 DispatcherServlet
↓
请求进入 Spring MVC
这也是 Spring Boot Web 项目能直接 java -jar 跑起来的原因之一。
以前传统部署方式是:
text
打成 war 包
↓
放进外部 Tomcat
↓
由外部 Tomcat 启动应用
Spring Boot 常见方式是:
text
打成 jar 包
↓
应用自己带着内嵌 Tomcat
↓
main 方法启动整个 Web 服务
但无论是外部 Tomcat,还是内嵌 Tomcat,职责没有变:
Tomcat 负责 Servlet 容器层,Spring MVC 负责 Web 框架层。
所以完整链路应该这样看:
text
浏览器发送 HTTP 请求
↓
Tomcat 接收和解析请求
↓
Tomcat 创建 Request / Response
↓
Tomcat 执行 FilterChain
↓
Tomcat 调用 DispatcherServlet
↓
Spring MVC 查找 Controller
↓
Controller 执行业务逻辑
↓
Spring MVC 写回响应
↓
Tomcat 把响应返回给浏览器
一句话总结:
Tomcat 是 Spring MVC 的运行底座。它负责把 HTTP 世界接进 Java Servlet 世界;DispatcherServlet 则负责把 Servlet 请求分发进 Spring MVC 的业务处理流程。
Filter、Interceptor、AOP 到底差在哪
Filter、Interceptor、AOP 经常被放在一起问。
它们都能做"增强",但增强的位置完全不同。
Filter 在更外层。
text
浏览器
↓
Tomcat
↓
Filter
↓
DispatcherServlet
Filter 属于 Servlet 体系,处理的是更原始的 HTTP 请求和响应。它适合做:
- 跨域处理
- 编码处理
- 请求压缩
- 安全框架入口
- 全局请求包装
Interceptor 在 Spring MVC 内部。
text
DispatcherServlet
↓
Interceptor
↓
Controller
它已经进入 Spring MVC 的处理范围,能拿到更多 Handler 相关上下文。它适合做:
- 登录校验
- 权限判断
- Controller 日志
- 接口耗时统计
- 请求链路追踪
AOP 则不关心 HTTP。
text
调用方
↓
代理对象
↓
目标 Bean 方法
AOP 包装的是 Spring Bean 的方法调用。它适合做:
- 事务
- 缓存
- 重试
- 异步
- 方法级日志
- 业务切面
所以三者的区别,不要只背"执行顺序"。
更关键的是看它们包装的对象:
text
Filter 包装 HTTP 请求响应
Interceptor 包装 Controller 调用链
AOP 包装 Bean 方法调用
一旦抓住这个区别,就不会把 Interceptor 和 AOP 混在一起。
@Transactional 为什么本质上是代理
最后看 @Transactional。
你写:
java
@Transactional
public void createOrder() {
orderRepository.save(order);
}
从业务代码看,你只是加了一个注解。
但 Spring 启动时会识别事务元数据,然后为相关 Bean 创建代理对象。
运行时调用的不是原始对象,而是代理对象:
text
调用 createOrder()
↓
进入事务代理
↓
开启事务
↓
执行目标方法
↓
根据结果提交或回滚
Spring 官方文档里对声明式事务的解释也非常明确:它依赖 AOP 代理,事务通知由注解或 XML 这类元数据驱动,并通过 TransactionInterceptor 配合事务管理器完成方法调用前后的事务处理。
这也是为什么事务有一些经典失效场景:
- 同一个类内部方法互相调用,可能绕过代理
- 非 public 方法在代理模式下可能不符合预期
- 异常被 catch 后没有继续抛出,事务感知不到失败
- 默认情况下,受检异常不一定触发回滚
- 新线程里的逻辑不自动继承当前线程事务
这些不是"玄学失效",而是代理模型带来的边界。
你只要记住一句话:
@Transactional生效的关键,不是注解本身,而是调用有没有经过 Spring 创建的事务代理。
这比单独背十几个事务失效场景更有用。
以后怎么用统一视角学习 Spring
到这里,你会发现 Spring 很多能力都能用同一套问题来理解:
text
启动时收集了什么元数据?
启动时构建了什么对象关系?
启动时注册了什么执行规则?
运行时谁来查这些规则?
运行时谁来真正执行?
比如:
@Autowired:
text
启动时分析依赖关系
↓
容器创建 Bean 时完成注入
↓
运行时直接使用已经组装好的对象
@GetMapping:
text
启动时扫描 Controller 方法
↓
建立请求路径到方法的映射
↓
运行时 HandlerMapping 查找并交给 HandlerAdapter 调用
WebConfig:
text
启动时执行 MVC 配置回调
↓
把拦截器、格式化器、消息转换器等注册进 MVC 体系
↓
运行时按已注册规则处理请求
@Transactional:
text
启动时识别事务元数据
↓
创建 AOP 代理
↓
运行时代理在方法前后控制事务
所以学 Spring,不要只问"这个注解怎么用"。
更应该问:
这个注解在启动期被谁扫描?变成了什么元数据?注册到了哪里?运行期由谁使用?
这个问题一问出来,很多零散知识点就会自动连成一条线。
最后总结
Spring 不是一个"帮你写业务代码"的框架。
它真正厉害的地方,是把对象管理、依赖关系、请求路由、事务、AOP、MVC 配置这些能力,都放进了一套统一机制里:
text
启动期扫描元数据
↓
构建对象关系
↓
构建执行规则
↓
运行期按规则调度执行
IOC、AOP、MVC、事务、Interceptor、Spring Security,本质上都是这套思想在不同场景下的具体实现。
当你能用这套视角看 Spring,再去学循环依赖、三级缓存、自动配置、事务传播、Security 过滤器链,就不会觉得它们是完全割裂的知识点。
它们只是同一个框架思想,在不同层面的展开。
如果你以前看 Spring 总觉得概念很多,可以先不用急着展开更多分支。
先把这句话记住:
Spring 的核心机制,是启动期把注解和配置变成规则,运行期按规则执行。
参考资料:
- Spring Framework Reference: IoC Container
- Spring Framework Reference: Web MVC DispatcherServlet and HandlerMapping
- Spring Framework Reference: Spring AOP Proxies
- Spring Framework Reference: Declarative Transaction Management