Spring 原理总览:从启动到请求执行

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、事务、拦截器这些知识点,就能被放回同一套框架里理解。

目录:

  1. Spring 最核心的职责是什么
  2. 启动时 Spring 到底在忙什么
  3. 为什么说 BeanDefinition 是对象说明书
  4. 请求进来后 Spring MVC 如何找到 Controller
  5. WebConfig 和 Interceptor 为什么会生效
  6. Tomcat 在这条链路里负责什么
  7. Filter、Interceptor、AOP 到底差在哪
  8. @Transactional 为什么本质上是代理
  9. 以后怎么用统一视角学习 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
相关推荐
天天进步20151 小时前
Python全栈项目--基于Python的数据库管理工具
开发语言·数据库·python
大大杰哥2 小时前
SSeEmitter的基本使用和介绍
java·sse·通信
cui_ruicheng2 小时前
MySQL(三):库操作与表操作
数据库·mysql·oracle
闪电悠米2 小时前
黑马点评-Redis 消息队列-02_list_pubsub_limits
java·数据库·ide·redis·缓存·list·intellij-idea
海梨花2 小时前
字节面试高频算法题
java·算法·面试·职场和发展
野生技术架构师2 小时前
Java 23 种设计模式:从踩坑到精通 —— 开篇及系列介绍
java·开发语言·设计模式
suoyue_zhan2 小时前
SQL经典案例之数据库的CTE递归循环使用
数据库·sql
Sammyyyyy2 小时前
2026 Mac 本地大模型部署深度解析与混合架构指南
数据库·人工智能·macos·ai·架构·servbay
折哥的程序人生 · 物流技术专研2 小时前
《Java 100 天进阶之路》第93篇:Redis实战应用:缓存策略与分布式锁(2026版)
java·redis·缓存·面试·架构·求职招聘