Spring/Spring MVC
为什么要使用 spring?
- spring 提供 ioc 技术,容器会帮你管理依赖的对象,从而不需要自己创建和管理依赖对象了,更轻松的实现了程序的解耦。
- spring 提供了事务支持,使得事务操作变的更加方便。
- spring 提供了面向切片编程,这样可以更方便的处理某一类的问题。
- 更方便的框架集成,spring 可以很方便的集成其他框架,比如 MyBatis、hibernate 等。
解释一下什么是AOP?意义是什么?实现机制是什么
- 什么是AOP–面向切面编程
- 用于将与业务无关的,但却对多个对象产生影响的公共行为和逻辑,使用动态代理的方式封装为一个统一的可重用的模块,这个模块被命名为“切面”(Aspect),具有无侵入性的特点,可以减少重复代码.如用于鉴权、日志、事务、运行监控等
- 实现机制
- Spring AOP基于动态代理实现.使用JDK动态代理或CGLIB.JDK动态代理支持接口的代理,不支持类的代理,当无法使用JDK动态代理时,Spring AOP才会使用CGLIB
静态代理和动态代理的区别是什么
代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理的代表为Spring AOP
生成代理对象的时机不同
- 静态代理:由程序员主动创建或由特定工具自动生成源代码,再对其编译,在程序运行前代理类的.class文件就已经存在了。
- 动态代理:在程序运行时用反射机制在内存中临时生成代理对象,该代理对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法
AspectJ的静态代理具有更好的性能,但AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理
解释一下什么是 ioc?
ioc:Inversionof Control(中文:控制反转)是 spring 的核心,对于 spring 框架来说,就是由 spring 来负责控制对象的生命周期和对象间的关系。
简单来说,控制指的是当前对象对内部成员的控制权;控制反转指的是,这种控制权不由当前对象管理了,由其他(类,第三方容器)来管理。
spring 有哪些主要模块?
- spring core:框架的最基础部分,提供 ioc 和依赖注入特性。
- spring context:构建于 core 封装包基础上的 context 封装包,提供了一种框架式的对象访问方法。
- spring dao:Data Access Object 提供了JDBC的抽象层。
- spring aop:提供了面向切面的编程实现,让你可以自定义拦截器、切点等。
- spring Web:提供了针对 Web 开发的集成特性,例如文件上传,利用 servlet listeners 进行 ioc 容器初始化和针对 Web 的 ApplicationContext。
- spring Web mvc:spring 中的 mvc 封装包提供了 Web 应用的 Model-View-Controller(MVC)的实现。
spring 常用的注入方式有哪些?
- setter 属性注入
- 构造方法注入
- 注解方式注入
spring 中的 bean 是线程安全的吗?
spring 中的 bean 默认是单例模式,spring 框架并没有对单例 bean 进行多线程的封装处理。
实际上大部分时候 spring bean 无状态的(比如 dao 类),所有某种程度上来说 bean 也是安全的,但如果 bean 有状态的话(比如 view model 对象),那就要开发者自己去保证线程安全了,最简单的就是改变 bean 的作用域,把“singleton”变更为“prototype”,这样请求 bean 相当于 new Bean()了,所以就可以保证线程安全了。
- 有状态就是有数据存储功能。
- 无状态就是不会保存数据。
spring 支持几种 bean 的作用域?
spring 支持 5 种作用域,如下:
- singleton:spring ioc 容器中只存在一个 bean 实例,bean 以单例模式存在,是系统默认值;
- prototype:每次从容器调用 bean 时都会创建一个新的示例,既每次 getBean()相当于执行 new Bean()操作;
- Web 环境下的作用域:
- request:每次 http 请求都会创建一个 bean;
- session:同一个 http session 共享一个 bean 实例;
- global-session:用于 portlet 容器,因为每个 portlet 有单独的 session,globalsession 提供一个全局性的 http session。
「注意:」 使用 prototype 作用域需要慎重的思考,因为频繁创建和销毁 bean 会带来很大的性能开销。
spring 自动装配 bean 有哪些方式?
- no:默认值,表示没有自动装配,应使用显式 bean 引用进行装配。
- byName:它根据 bean 的名称注入对象依赖项。
- byType:它根据类型注入对象依赖项。
- 构造函数:通过构造函数来注入依赖项,需要设置大量的参数。
- autodetect:容器首先通过构造函数使用 autowire 装配,如果不能,则通过 byType 自动装配。
spring 事务实现方式有哪些?
- 声明式事务:声明式事务也有两种实现方式,基于 xml 配置文件的方式和注解方式(在类上添加 @Transaction 注解)。
- 编码方式:提供编码的形式管理和维护事务。
说一下 spring 的事务隔离?
spring 有五大隔离级别,默认值为 ISOLATION_DEFAULT(使用数据库的设置),其他四个隔离级别和数据库的隔离级别一致:
- ISOLATION_DEFAULT:用底层数据库的设置隔离级别,数据库设置的是什么我就用什么;
- ISOLATIONREADUNCOMMITTED:未提交读,最低隔离级别、事务未提交前,就可被其他事务读取(会出现幻读、脏读、不可重复读);
- ISOLATIONREADCOMMITTED:提交读,一个事务提交后才能被其他事务读取到(会造成幻读、不可重复读),SQL server 的默认级别;
- ISOLATIONREPEATABLEREAD:可重复读,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据(会造成幻读),MySQL 的默认级别;
- ISOLATION_SERIALIZABLE:序列化,代价最高最可靠的隔离级别,该隔离级别能防止脏读、不可重复读、幻读。
- 「脏读」 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录 A。
- 「不可重复读」 :是指在一个事务内,多次读同一数据。
- 「幻读」 :指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。
说一下 spring mvc 运行流程?
- spring mvc 先将请求发送给 DispatcherServlet。
- DispatcherServlet 查询一个或多个 HandlerMapping,找到处理请求的 Controller。
- DispatcherServlet 再把请求提交到对应的 Controller。
- Controller 进行业务逻辑处理后,会返回一个ModelAndView。
- Dispathcher 查询一个或多个 ViewResolver 视图解析器,找到 ModelAndView 对象指定的视图对象。
- 视图对象负责渲染返回给客户端。
spring mvc 有哪些组件?
- 前置控制器 DispatcherServlet。
- 映射控制器 HandlerMapping。
- 处理器 Controller。
- 模型和视图 ModelAndView。
- 视图解析器 ViewResolver。
@RequestMapping 的作用是什么?
将 http 请求映射到相应的类/方法上。
@Autowired 的作用是什么?
@Autowired 它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作,通过@Autowired 的使用来消除 set/get 方法。
@Autowired和@Resource注解的区别
@Autowired与@Resource都可以用来装配bean,都可以写在属性或set方法上,@Autowired还可以写在构造方法上,@Resource不可以
1、@Autowired
由Spring提供,只按照类型自动注入
2、@Resource
由J2EE提供,默认按照名称自动注入
@Resource有两个属性:name和type
@Resource装配顺序:
- 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常
- 如果指定了name,则从Spring上下文中查找名称匹配的bean进行装配,找不到则抛出异常
- 如果指定了type,则从Spring上下文中找到类型匹配的bean进行装配,找不到或找到多个,都抛出异常
Spring Boot/Spring Cloud
什么是 spring boot?
spring boot 是为 spring 服务的,是用来简化新 spring 应用的初始搭建以及开发过程的。
为什么要用 spring boot?
- 配置简单
- 独立运行
- 自动装配
- 无代码生成和 xml 配置
- 提供应用监控
- 易上手
- 提升开发效率
spring boot 核心配置文件是什么?
spring boot 核心的两个配置文件:
- bootstrap (. yml 或者 . properties):boostrap 由父 ApplicationContext 加载的,比 applicaton 优先加载,且 boostrap 里面的属性不能被覆盖;
- application (. yml 或者 . properties):用于 spring boot 项目的自动化配置。
spring boot 配置文件有哪几种类型?它们有什么区别?
配置文件有 . properties 格式和 . yml 格式,它们主要的区别是书法风格不同。
. properties 配置如下:
spring.RabbitMQ.port=5672
. yml 配置如下:
spring:
RabbitMQ:
port: 5672
yml 格式不支持 @PropertySource 注解导入。
spring boot 有哪些方式可以实现热部署?
- 使用 devtools 启动热部署,添加 devtools 库,在配置文件中把 spring. devtools. restart. enabled 设置为 true;
- 使用 Intellij Idea 编辑器,勾上自动编译或手动重新编译。
jpa 和 hibernate 有什么区别?
jpa 全称 Java Persistence API,是 Java 持久化接口规范,hibernate 属于 jpa 的具体实现。
什么是 spring cloud?
spring cloud 是一系列框架的有序集合。它利用 spring boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 spring boot 的开发风格做到一键启动和部署。
spring cloud 断路器的作用是什么?
在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。
spring cloud 的核心组件有哪些?
- Eureka:服务注册于发现。
- Feign:基于动态代理机制,根据注解和选择的机器,拼接请求 url 地址,发起请求。
- Ribbon:实现负载均衡,从一个服务的多台机器中选择一台。
- Hystrix:提供线程池,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题。
- Zuul:网关管理,由 Zuul 网关转发请求给对应的服务。
Spring循环依赖怎么解决的?
Spring通过三级缓存解决单例Bean的循环依赖(主要是构造器注入无法解决):
- 一级缓存:singletonObjects,存放完全初始化好的Bean。
- 二级缓存:earlySingletonObjects,存放提前暴露的早期对象(尚未填充属性)。
- 三级缓存:singletonFactories,存放Bean工厂,用于生成早期对象的引用。
- 过程:A依赖B,B依赖A。创建A时,将A的工厂放入三级缓存,然后填充属性时发现依赖B,去创建B;B创建时依赖A,从三级缓存获取A的工厂生成早期A(放入二级缓存),B完成注入后创建完毕,然后A从二级缓存拿到B的引用,完成后续初始化。最终A放入一级缓存。
详细说明:
Spring 框架为了解决单例作用域下的循环依赖问题,设计了一套基于 三级缓存 的机制。下面将详细解释其原理、步骤以及为什么需要三级缓存。
1. 什么是循环依赖
循环依赖是指两个或多个 Bean 之间互相持有对方的引用,形成一个闭环。例如:
- A 依赖 B,B 依赖 A(最简单的循环依赖)。
- A 依赖 B,B 依赖 C,C 依赖 A(更复杂的循环)。
在创建 Bean 时,如果容器无法打破这个环,就会陷入无限递归,最终导致 BeanCurrentlyInCreationException。
2. Spring 能解决哪些循环依赖
Spring 仅能解决 以下条件的循环依赖:
- Bean 的作用域是 单例(singleton)
- 依赖注入的方式为 setter 注入 或 字段注入(@Autowired)
(因为这两种方式可以在对象实例化后,再设置属性) - 不能解决 构造函数注入 的循环依赖(因为构造函数必须在实例化时完成所有参数的注入,此时对象尚未创建,无法提前暴露)。
3. 三级缓存的内部结构
Spring 容器内部维护了三个缓存,用于存放不同阶段的 Bean 实例:
| 缓存名称 | 作用 |
|---|---|
一级缓存 singletonObjects | 存放完全初始化好的单例 Bean(成品) |
二级缓存 earlySingletonObjects | 存放提前暴露的 Bean 实例(半成品),即对象已经创建,但尚未完成属性填充和初始化 |
三级缓存 singletonFactories | 存放生成 Bean 实例的 ObjectFactory 工厂对象(用于生成代理对象等) |
这三个缓存对应 Spring 的 DefaultSingletonBeanRegistry 类中的三个成员变量:
// 一级
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
4. 解决循环依赖的流程(以 A 依赖 B,B 依赖 A 为例)
假设两个 Bean 都是通过 setter 注入,且作用域为单例。
步骤 1:开始创建 A
getBean("a")发现一级缓存中没有 A。- 实例化 A(调用构造函数),得到一个原始对象
aInstance。 - 提前暴露 A:将
aInstance封装为一个ObjectFactory放入三级缓存singletonFactories中(key = “a”)。- 这一步实际上执行的是
addSingletonFactory("a", () -> getEarlyBeanReference("a", mbd, aInstance)),其中getEarlyBeanReference可能会返回原始对象或代理对象(如果有 AOP 增强)。
- 这一步实际上执行的是
步骤 2:填充 A 的属性
- 发现 A 需要注入 B,因此调用
getBean("b")去创建或获取 B。
步骤 3:创建 B(同样的流程)
getBean("b")一级缓存无 B。- 实例化 B,得到原始对象
bInstance。 - 将 B 的工厂放入三级缓存(key = “b”)。
步骤 4:填充 B 的属性
- 发现 B 需要注入 A,调用
getBean("a")。
步骤 5:获取 A 的“提前引用”
- 此时 A 正在创建中,一级缓存没有 A。
- 查看二级缓存
earlySingletonObjects,也没有 A。 - 查看三级缓存
singletonFactories,发现有 A 的工厂。 - 执行工厂,得到提前暴露的 A 的引用(可能是原始对象,也可能是 AOP 代理对象)。
- 将得到的对象放入二级缓存
earlySingletonObjects(key = “a”),并从三级缓存中移除该工厂。
步骤 6:B 获得 A 的引用
- B 成功拿到了 A 的引用(此时 A 尚未完成属性填充,但对象已存在)。
- 继续完成 B 的剩余属性填充和初始化。
- B 初始化完成后,将自己放入一级缓存
singletonObjects(此时 B 是成品),并清理二、三级缓存中的 B。
步骤 7:回到 A
- A 从
getBean("b")调用返回,拿到了初始化好的 B 的完整引用。 - 将 B 注入到 A 中(此时 A 的属性填充完成)。
- 执行 A 的初始化方法(如
afterPropertiesSet、init-method)。 - 将 A 放入一级缓存
singletonObjects,并清理二、三级缓存中的 A。
至此,循环依赖被完美解决,两个 Bean 都已完成创建并存于一级缓存。
5. 为什么需要三级缓存,而不是两级?
有些人可能会问:二级缓存(提前暴露的对象)加上一级缓存(成品)不就够了吗?为什么还需要第三级缓存(工厂)?
关键在于 AOP 代理对象的创建时机。
- 如果 Bean 不需要 AOP 代理,那么提前暴露的对象就是原始对象,没有区别。
- 如果 Bean 需要 AOP 代理(例如被
@Transactional注解),那么代理对象的创建通常是在 Bean 初始化之后(通过BeanPostProcessor的postProcessAfterInitialization)。
但是,在解决循环依赖时,其他 Bean 可能在初始化之前就需要引用这个 Bean。如果等到初始化完成才创建代理,那么其他 Bean 拿到的就是原始对象,而非代理对象,这会导致 AOP 功能失效(例如事务不起作用)。
因此,Spring 通过三级缓存中的 ObjectFactory 来 延迟创建代理对象:
- 工厂方法
getEarlyBeanReference允许在提前暴露阶段就生成代理对象(如果 Bean 需要被代理)。 - 这样,当 B 通过工厂获取 A 的引用时,拿到的可能是一个尚未完全初始化的 A 的代理对象。之后 A 继续完成初始化,但 B 持有的已经是代理对象,保证了 AOP 的正确性。
如果只有二级缓存(提前暴露原始对象),就无法在必要的时候提前创建代理,导致 AOP 失效。
举例说明:
- A 和 B 互相依赖,且 A 需要被事务代理。
- 如果没有三级缓存:
- A 提前暴露原始对象,B 拿到原始 A 并完成创建。
- 之后 A 初始化完成,生成代理对象。
- 但 B 中注入的仍是原始 A,事务注解无效。
- 有了三级缓存:
- B 通过工厂获取 A 时,工厂返回一个代理对象(尚未完全初始化,但已经是代理)。
- B 持有代理 A。
- A 后续初始化完成后,容器中保存的也是同一个代理对象(或者代理对象内部包装了原始对象),因此 B 使用的代理正确。
6. 哪些循环依赖 Spring 无法解决?
- 构造函数注入:因为构造函数必须在实例化时完成,无法提前暴露对象,无法打破循环。
- 非单例作用域(prototype):Spring 不缓存 prototype 的 Bean,每次请求都新建,因此无法提前暴露,也无法解决循环依赖。
- 多例混合:比如 singleton 依赖 prototype,但反过来则无法解决。
对于构造函数注入的循环依赖,可以改用 setter 注入,或者使用 @Lazy 注解延迟加载其中一个依赖。
7. 源码简要追溯
主要逻辑集中在 AbstractBeanFactory.doGetBean() 和 DefaultSingletonBeanRegistry.getSingleton() 方法中。
getSingleton(String beanName, boolean allowEarlyReference) 方法体现了三级缓存的查找顺序:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
当创建一个 Bean 时,在 doCreateBean 中实例化后,会调用 addSingletonFactory 将工厂放入三级缓存。
总结
Spring 通过 三级缓存 巧妙地解决了单例 setter 注入的循环依赖问题:
- 一级缓存存放完整 Bean。
- 二级缓存存放提前暴露的 Bean(半成品)。
- 三级缓存存放生产 Bean 的工厂,用于在必要时提前生成代理对象,避免 AOP 失效。
这种设计既保证了循环依赖的解决,又兼顾了 AOP 等增强功能的正确性。
@Lazy延迟加载
@Lazy 是 Spring 提供的一个灵活控制 Bean 生命周期的注解,主要用途包括:
- 性能优化:延迟非必要 Bean 的初始化,加快启动速度。
- 打破循环依赖:特别是解决构造器注入导致的循环依赖。
- 按需加载:让资源消耗较大的 Bean 在真正需要时才创建。
工作原理
Spring 容器在启动时,对于延迟加载的 Bean,并不会立即调用其构造方法和初始化逻辑,而是:
- 对于
@Lazy标注的类或@Bean方法:容器会将这些 Bean 的创建推迟到第一次调用getBean()获取它们时。但在容器启动阶段,它们会被记录下来,但不会实例化。 - 对于注入点上使用
@Lazy:Spring 会生成一个代理对象(基于 CGLIB 或 JDK 动态代理)并注入到目标 Bean 中。该代理对象会拦截所有方法调用,在首次调用时触发目标 Bean 的创建和初始化,然后将调用委派给真实的 Bean 实例。
正是通过这种代理机制,@Lazy 才能在不破坏依赖关系的情况下实现延迟加载。
CGLIB 与 JDK 动态代理
- JDK 动态代理 是 Java 原生提供的,基于接口的代理,适合接口设计良好的场景。
- CGLIB 代理 通过生成子类实现代理,更加灵活,适合没有接口的类或需要更细粒度控制的场景。
- Spring 根据目标类是否有接口自动选择,也允许强制使用 CGLIB。
- 在实际开发中,如果使用 Spring Boot 2.x,默认 CGLIB 模式可以避免接口定义,使代码更简洁。
SpringBoot 的核心注解是哪个?
核心注解是 @SpringBootApplication 。通常标注在项目的主启动类上。
SpringBootApplication注解包含哪几个注解?
@SpringBootConfiguration:继承自@Configuration,表示该类是一个配置类。@EnableAutoConfiguration:开启自动配置功能。@ComponentScan:启用组件扫描,默认扫描主启动类所在包及其子包。
SpringBoot最核心的注解有哪些?
除了上面的 @SpringBootApplication 组合注解外,还有:
@EnableAutoConfiguration:自动配置的核心。@Conditional系列注解:如@ConditionalOnClass、@ConditionalOnMissingBean,用于控制自动配置在满足特定条件时才生效 。@ConfigurationProperties:用于将配置文件(如application.yml)中的属性绑定到 Java Bean 上 。
SpringBoot自动配置原理是什么?
原理主要基于 @EnableAutoConfiguration 注解 。它会通过 SpringFactoriesLoader 机制,从 classpath 下所有 JAR 包中的 META-INF/spring.factories 文件里,加载 EnableAutoConfiguration 键对应的自动配置类(如 XxxAutoConfiguration)。这些配置类上通常标有 @Conditional 条件注解,只有满足特定条件(如存在相关类、配置了相关属性)时,这些自动配置类才会生效,实例化相应的 Bean。
SpringBoot开启自动配置的原理是什么?
只需要在配置类上使用 @EnableAutoConfiguration 注解即可。由于 @SpringBootApplication 已经组合了该注解,所以只要在启动类上标记 @SpringBootApplication,就相当于开启了自动配置 。
SpringBoot自动配置的类型在哪注册?
自动配置类(类型)的注册信息存放在所有 JAR 包中的 META-INF/spring.factories 文件中 。Spring Boot 启动时会扫描该文件,获取所有注册的自动配置类的全限定名。
SpringBoot自动配置报告怎么查看?
开启 debug 模式。在配置文件(application.properties 或 application.yml)中设置 debug: true。启动应用后,控制台会打印 Positive matches(生效的自动配置)和 Negative matches(未生效的自动配置)报告,方便开发者了解哪些自动配置类被应用了。
SpringBoot怎么排除某些自动配置?
- 在
@SpringBootApplication或@EnableAutoConfiguration注解中使用exclude属性:@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) - 在配置文件中使用
spring.autoconfigure.exclude属性:spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
SpringBoot怎么开启和关闭自动配置?
- 开启:使用
@EnableAutoConfiguration注解(通常通过@SpringBootApplication间接使用)。 - 关闭:一般不建议完全关闭自动配置。如果想禁用特定的自动配置,可以使用排除(exclude)机制。如果不想使用整个 Spring Boot 的自动配置特性,可以不使用
@EnableAutoConfiguration注解,但这会丧失 Spring Boot 的核心优势。
SpringBoot的目录结构是怎样的?
一个典型的 Spring Boot 项目目录结构遵循一定的约定 :
com.example.project ├── Application.java (主启动类,位于根包下) ├── domain/ (或 model) - 实体类 ├── repository/ (或 dao) - 数据访问层 ├── service/ - 业务逻辑层 │ └── impl/ - 业务实现类 └── web/ (或 controller) - 控制器层
此外,资源文件位于 src/main/resources 目录下,包含:
application.properties或application.yml:配置文件。static/:存放静态资源(如 CSS, JS, 图片)。templates/:存放模板文件(如 Thymeleaf)。
SpringBoot中的Starters是什么?
Starters 是一组方便的依赖描述符 。它相当于一个“一站式”的依赖集合。例如,引入 spring-boot-starter-web 依赖,它就会自动传递引入构建 Web 应用所需的所有依赖(如 Spring MVC、内嵌 Tomcat、Jackson 等),避免了开发者手动寻找和配置一个个兼容版本的依赖,极大地简化了项目的依赖管理 。