Spring 原理总览:从启动到请求执行
2026/6/10 17:32:43 网站建设 项目流程

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 对象大概是这样:

UserServiceuserService=newUserService();OrderServiceorderService=newOrderService(userService);

对象由你自己创建,依赖由你自己传递,生命周期也由你自己控制。

这在小项目里没什么问题。但项目一复杂,对象之间的关系会迅速变成一张网:

  • Controller 依赖 Service
  • Service 依赖 Repository
  • Repository 依赖 DataSource
  • Service 之间还可能相互协作
  • 有些对象还要加事务、缓存、权限、日志

如果所有对象都靠业务代码自己new,后面会越来越难维护。

Spring 的第一件事,就是把这些对象统一接管。

比如:

@ServicepublicclassUserService{}

你没有手动new UserService(),但 Spring 启动后,UserService会进入 IOC 容器。

可以简单理解成:

IOC 容器 ├── UserController ├── UserService ├── OrderService ├── UserRepository └── DataSource

这些被 Spring 管理的对象,就叫 Bean。

所以,IOC 不是一个很玄的概念。它做的事情很朴素:

原来由你自己创建和组装对象,现在交给 Spring 创建和组装。

对象控制权从业务代码转移到容器,这就是“控制反转”。

启动时 Spring 到底在忙什么

Spring Boot 项目通常从这里启动:

@SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String[]args){SpringApplication.run(Application.class,args);}}

这几行代码背后,Spring 做了大量准备工作。

简化一下,大致可以理解为:

启动应用 ↓ 扫描类路径 ↓ 发现候选 Bean ↓ 解析注解和配置 ↓ 生成 BeanDefinition ↓ 注册到 IOC 容器 ↓ 实例化 Bean ↓ 完成依赖注入 ↓ 执行初始化逻辑 ↓ 应用准备就绪

注意一个关键点:

Spring 并不是一开始就直接创建所有对象。

它会先分析这些类和配置,生成一批“对象说明书”。这些说明书就是BeanDefinition

理解 Spring 启动流程时,不只要看“对象什么时候创建”,还要看:

Spring 启动阶段先收集元数据,再根据元数据构建对象和规则。

这也是为什么你写一个注解,Spring 就能知道怎么处理。

注解本身不会自动工作。真正工作的是 Spring 在启动时扫描、解析、注册、构建出来的一整套规则。

为什么说 BeanDefinition 是对象说明书

如果 Bean 是对象本身,那么BeanDefinition就是创建这个对象之前的说明书。

它里面会记录类似这些信息:

  • 这个 Bean 对应哪个类
  • 它是单例还是每次新建
  • 它依赖哪些 Bean
  • 它有没有初始化方法
  • 它有没有销毁方法
  • 它有哪些注解信息
  • 它是否需要被代理

比如你写:

@ServicepublicclassOrderService{privatefinalUserServiceuserService;publicOrderService(UserServiceuserService){this.userService=userService;}}

Spring 启动时会识别到:

  • OrderService是一个需要管理的 Bean
  • 它构造方法需要一个UserService
  • 容器里应该先能找到UserService
  • 创建OrderService时要把UserService注入进去

所以@Autowired或构造器注入并不是“运行中突然找对象”。

更准确地说,是 Spring 在容器初始化过程中,根据 Bean 的定义、依赖关系和自动装配规则,把对象关系提前组装好。

这也是为什么 Spring 官方文档会把 Bean 和依赖关系看作配置元数据的一部分。XML、注解、Java Config 只是元数据来源不同,最后都会进入容器的统一处理流程。

你可以把它理解成:

注解 / XML / Java Config ↓ 配置元数据 ↓ BeanDefinition ↓ Bean 实例 ↓ 可运行的对象关系

这条链路非常重要。

因为后面所有“为什么配置会生效”的问题,本质都能塞回这条链路里。

请求进来后 Spring MVC 如何找到 Controller

接下来换到 Web 场景。

你写一个接口:

@RestControllerpublicclassUserController{@GetMapping("/users/{id}")publicUserDTOgetUser(@PathVariableLongid){returnuserService.getUser(id);}}

表面看,是请求/users/1后执行getUser()

但 Spring MVC 不可能每次请求来了,才临时全项目搜索哪个方法能处理这个 URL。

真正的逻辑也是:

启动期建表,运行期查表。

启动时,Spring MVC 会扫描 Controller 中的@RequestMapping@GetMapping等注解,建立 URL 到处理方法的映射关系。

可以理解成一张路由表:

GET /users/{id} -> UserController#getUser(Long id) POST /users -> UserController#createUser(UserCreateDTO dto) DELETE /users/{id} -> UserController#deleteUser(Long id)

请求真的进来时,就不需要重新分析注解了。

运行时只要根据请求路径、HTTP 方法等信息,找到对应的处理方法即可。

这也是为什么 Spring MVC 的核心链路里会出现这些角色:

请求 ↓ DispatcherServlet ↓ HandlerMapping ↓ HandlerExecutionChain ↓ HandlerAdapter ↓ Controller 方法 ↓ HttpMessageConverter ↓ 响应

简单翻译一下:

  • DispatcherServlet:统一接收和调度请求
  • HandlerMapping:根据请求找到处理器
  • HandlerExecutionChain:处理器加上拦截器链
  • HandlerAdapter:用合适的方式调用处理器
  • HttpMessageConverter:把 Java 对象和 HTTP 请求响应体互相转换

所以DispatcherServlet不是拦截器。

它更像 Web 层的总调度员,也就是经典的前端控制器模式。

WebConfig 和 Interceptor 为什么会生效

再看一个很常见的问题。

你写了一个配置类:

@ConfigurationpublicclassWebConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){registry.addInterceptor(loginInterceptor);}}

为什么这几行代码能影响所有请求?

很多人会误以为:是不是每个请求进来时,都会执行一次addInterceptors()

不是。

它真正发生在启动阶段。

大致过程是:

Spring MVC 初始化 ↓ 找到所有 WebMvcConfigurer ↓ 执行 addInterceptors() ↓ 把拦截器注册到 InterceptorRegistry ↓ 保存到 HandlerMapping 相关结构中

所以registry.addInterceptor(loginInterceptor)的本质,不是在处理某个请求,而是在注册一条 MVC 规则。

等请求进来时,Spring MVC 已经知道:

这个 Controller 调用前后,需要经过哪些 Interceptor

于是一次请求可能变成:

请求 ↓ LoginInterceptor.preHandle() ↓ TraceInterceptor.preHandle() ↓ Controller ↓ TraceInterceptor.postHandle() ↓ LoginInterceptor.postHandle() ↓ afterCompletion()

这就是 Interceptor 的本质:

它不是 AOP 代理,而是 Spring MVC 请求处理链上的责任链。

它包装的是 Controller 调用前后的 Web 请求流程。

Tomcat 在这条链路里负责什么

前面讲请求链路时,我们会看到:

浏览器 ↓ 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 容器负责的。

可以把层级拆开看:

操作系统网络层 ↓ Tomcat Connector ↓ HTTP 请求解析 ↓ Servlet 容器 ↓ FilterChain ↓ DispatcherServlet ↓ Spring MVC ↓ Controller

Spring MVC 的入口是DispatcherServlet,但DispatcherServlet本身也是一个 Servlet。

既然是 Servlet,它就需要运行在 Servlet 容器里。

Tomcat 做的事情,就是提供这个运行环境。

更具体一点:

Tomcat ├── 负责监听端口 ├── 负责接收 HTTP 请求 ├── 负责解析请求报文 ├── 负责创建 Request / Response 对象 ├── 负责管理线程池 ├── 负责执行 Filter 链 └── 负责调用目标 Servlet

而 Spring MVC 接手的是 Tomcat 之后的事情:

DispatcherServlet ├── 根据请求查找 HandlerMapping ├── 找到 Controller 方法 ├── 执行 Interceptor ├── 调用 HandlerAdapter ├── 执行业务 Controller └── 通过 HttpMessageConverter 写回响应

所以 Tomcat 和 Spring MVC 的分工可以这样理解:

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 项目:

Spring Boot 应用启动 ↓ 启动内嵌 Tomcat ↓ Tomcat 监听端口 ↓ 注册 DispatcherServlet ↓ 请求进入 Spring MVC

这也是 Spring Boot Web 项目能直接java -jar跑起来的原因之一。

以前传统部署方式是:

打成 war 包 ↓ 放进外部 Tomcat ↓ 由外部 Tomcat 启动应用

Spring Boot 常见方式是:

打成 jar 包 ↓ 应用自己带着内嵌 Tomcat ↓ main 方法启动整个 Web 服务

但无论是外部 Tomcat,还是内嵌 Tomcat,职责没有变:

Tomcat 负责 Servlet 容器层,Spring MVC 负责 Web 框架层。

所以完整链路应该这样看:

浏览器发送 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 在更外层。

浏览器 ↓ Tomcat ↓ Filter ↓ DispatcherServlet

Filter 属于 Servlet 体系,处理的是更原始的 HTTP 请求和响应。它适合做:

  • 跨域处理
  • 编码处理
  • 请求压缩
  • 安全框架入口
  • 全局请求包装

Interceptor 在 Spring MVC 内部。

DispatcherServlet ↓ Interceptor ↓ Controller

它已经进入 Spring MVC 的处理范围,能拿到更多 Handler 相关上下文。它适合做:

  • 登录校验
  • 权限判断
  • Controller 日志
  • 接口耗时统计
  • 请求链路追踪

AOP 则不关心 HTTP。

调用方 ↓ 代理对象 ↓ 目标 Bean 方法

AOP 包装的是 Spring Bean 的方法调用。它适合做:

  • 事务
  • 缓存
  • 重试
  • 异步
  • 方法级日志
  • 业务切面

所以三者的区别,不要只背“执行顺序”。

更关键的是看它们包装的对象:

Filter 包装 HTTP 请求响应 Interceptor 包装 Controller 调用链 AOP 包装 Bean 方法调用

一旦抓住这个区别,就不会把 Interceptor 和 AOP 混在一起。

@Transactional 为什么本质上是代理

最后看@Transactional

你写:

@TransactionalpublicvoidcreateOrder(){orderRepository.save(order);}

从业务代码看,你只是加了一个注解。

但 Spring 启动时会识别事务元数据,然后为相关 Bean 创建代理对象。

运行时调用的不是原始对象,而是代理对象:

调用 createOrder() ↓ 进入事务代理 ↓ 开启事务 ↓ 执行目标方法 ↓ 根据结果提交或回滚

Spring 官方文档里对声明式事务的解释也非常明确:它依赖 AOP 代理,事务通知由注解或 XML 这类元数据驱动,并通过TransactionInterceptor配合事务管理器完成方法调用前后的事务处理。

这也是为什么事务有一些经典失效场景:

  • 同一个类内部方法互相调用,可能绕过代理
  • 非 public 方法在代理模式下可能不符合预期
  • 异常被 catch 后没有继续抛出,事务感知不到失败
  • 默认情况下,受检异常不一定触发回滚
  • 新线程里的逻辑不自动继承当前线程事务

这些不是“玄学失效”,而是代理模型带来的边界。

你只要记住一句话:

@Transactional生效的关键,不是注解本身,而是调用有没有经过 Spring 创建的事务代理。

这比单独背十几个事务失效场景更有用。

以后怎么用统一视角学习 Spring

到这里,你会发现 Spring 很多能力都能用同一套问题来理解:

启动时收集了什么元数据? 启动时构建了什么对象关系? 启动时注册了什么执行规则? 运行时谁来查这些规则? 运行时谁来真正执行?

比如:

@Autowired

启动时分析依赖关系 ↓ 容器创建 Bean 时完成注入 ↓ 运行时直接使用已经组装好的对象

@GetMapping

启动时扫描 Controller 方法 ↓ 建立请求路径到方法的映射 ↓ 运行时 HandlerMapping 查找并交给 HandlerAdapter 调用

WebConfig

启动时执行 MVC 配置回调 ↓ 把拦截器、格式化器、消息转换器等注册进 MVC 体系 ↓ 运行时按已注册规则处理请求

@Transactional

启动时识别事务元数据 ↓ 创建 AOP 代理 ↓ 运行时代理在方法前后控制事务

所以学 Spring,不要只问“这个注解怎么用”。

更应该问:

这个注解在启动期被谁扫描?变成了什么元数据?注册到了哪里?运行期由谁使用?

这个问题一问出来,很多零散知识点就会自动连成一条线。

最后总结

Spring 不是一个“帮你写业务代码”的框架。

它真正厉害的地方,是把对象管理、依赖关系、请求路由、事务、AOP、MVC 配置这些能力,都放进了一套统一机制里:

启动期扫描元数据 ↓ 构建对象关系 ↓ 构建执行规则 ↓ 运行期按规则调度执行

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询