Spring 与 Spring Boot 面试题总结:IoC、AOP、事务与自动配置
前言
这是 JavaGuide 面试突击版本,只保留最常问的面试题,并对重点内容进行了 ⭐️ 标注。提供亮色和暗色两个主题,需要打印的朋友请选择亮色版本。
时间充裕的朋友,推荐使用 JavaGuide 网站系统学习,内容更全面深入。
如果你想要付费支持/面试辅导(比如简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!
面试突击最新版可在公众号回复「PDF」获取(知识星球会提前同步最新版)。

重要说明
本站所有面试题保持年度系统性优化完善,严格同步 Java 技术生态与招聘市场的最新动态,确保内容时效性与前瞻性。
这部分内容摘自 JavaGuide 下面几篇文章中的重点:
- Spring 常见面试题总结(Spring 基础、IoC、AOP、MVC、事务、循环依赖等)
- SpringBoot 常见面试题总结
- Spring&SpringBoot常用注解总结
- IoC & AOP详解(快速搞懂)
- Spring 事务详解
- Spring 中的设计模式详解
- SpringBoot 自动装配原理详解
- Async 注解原理分析
Spring IoC
⭐️什么是 IoC?
IoC (Inversion of Control )即控制反转/反转控制。它是一种思想不是一个技术实现。描述的是:Java 开发领域对象的创建以及管理的问题。
例如:现有类 A 依赖于类 B
- 传统的开发方式 :往往是在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来
- 使用 IoC 思想的开发方式 :不通过 new 关键字来创建对象,而是通过 IoC 容器(Spring 框架) 来帮助我们实例化对象。我们需要哪个对象,直接从 IoC 容器里面去取即可。
从以上两种开发方式的对比来看:我们 “丧失了一个权力” (创建、管理对象的权力),从而也得到了一个好处(不用再考虑对象的创建、管理等一系列的事情)
为什么叫控制反转?
- 控制 :指的是对象创建(实例化、管理)的权力
- 反转 :控制权交给外部环境(IoC 容器)

⭐️IoC 解决了什么问题?
IoC 的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。这样有什么好处呢?
- 对象之间的耦合度或者说依赖程度降低;
- 资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。
例如:现有一个针对 User 的操作,利用 Service 和 Dao 两层结构进行开发
在没有使用 IoC 思想的情况下,Service 层想要使用 Dao 层的具体实现的话,需要通过 new 关键字在UserServiceImpl 中手动 new 出 IUserDao 的具体实现类 UserDaoImpl(不能直接 new 接口类)。
很完美,这种方式也是可以实现的,但是我们想象一下如下场景:
开发过程中突然接到一个新的需求,针对IUserDao 接口开发出另一个具体实现类。因为 Server 层依赖了IUserDao的具体实现,所以我们需要修改UserServiceImpl中 new 的对象。如果只有一个类引用了IUserDao的具体实现,可能觉得还好,修改起来也不是很费力气,但是如果有许许多多的地方都引用了IUserDao的具体实现的话,一旦需要更换IUserDao 的实现方式,那修改起来将会非常的头疼。

使用 IoC 的思想,我们将对象的控制权(创建、管理)交由 IoC 容器去管理,我们在使用的时候直接向 IoC 容器 “要” 就可以了

什么是 Spring Bean?
简单来说,Bean 代指的就是那些被 IoC 容器所管理的对象。
我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。
<!-- Constructor-arg with 'value' attribute -->
<bean id="..." class="...">
<constructor-arg value="..."/>
</bean>下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。

org.springframework.beans和 org.springframework.context 这两个包是 IoC 实现的基础,如果想要研究 IoC 相关的源码的话,可以去看看
将一个类声明为 Bean 的注解有哪些?
@Component:通用的注解,可标注任意类为Spring组件。如果一个 Bean 不知道属于哪个层,可以使用@Component注解标注。@Repository: 对应持久层即 Dao 层,主要用于数据库相关操作。@Service: 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。@Controller: 对应 Spring MVC 控制层,主要用于接受用户请求并调用Service层返回数据给前端页面。
@Component 和 @Bean 的区别是什么?
@Component注解作用于类,而@Bean注解作用于方法。@Component通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用@ComponentScan注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。@Bean注解比@Component注解的自定义性更强,而且很多地方我们只能通过@Bean注解来注册 bean。比如当我们引用第三方库中的类需要装配到Spring容器时,则只能通过@Bean来实现。
@Bean注解使用示例:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}上面的代码相当于下面的 xml 配置
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>下面这个例子是通过 @Component 无法实现的。
@Bean
public OneService getService(status) {
case (status) {
when 1:
return new serviceImpl1();
when 2:
return new serviceImpl2();
when 3:
return new serviceImpl3();
}
}注入 Bean 的注解有哪些?
Spring 提供的 @Autowired,以及 Jakarta 规范提供的 @Resource 和 @Inject,都可以用于注入 Bean。
| Annotation | Package | Source |
|---|---|---|
@Autowired | org.springframework.beans.factory.annotation | Spring 2.5+ |
@Resource | jakarta.annotation(Spring 6+) | Jakarta Annotations / JSR-250 |
@Inject | jakarta.inject(Spring 6+) | Jakarta Dependency Injection / JSR-330 |
@Autowired 和@Resource使用的比较多一些。
⭐️@Autowired 和 @Resource 的区别是什么?
@Autowired 是 Spring 内置的注解,默认注入逻辑为先按类型(byType)匹配,若存在多个同类型 Bean,则再尝试按名称(byName)筛选。
具体来说:
- 优先根据接口 / 类的类型在 Spring 容器中查找匹配的 Bean。若只找到一个符合类型的 Bean,直接注入,无需考虑名称;
- 若找到多个同类型的 Bean(例如一个接口有多个实现类),则会尝试通过属性名或参数名与 Bean 的名称进行匹配(默认 Bean 名称为类名首字母小写,除非通过
@Bean(name = "...")或@Component("...")显式指定)。
当一个接口存在多个实现类时:
- 若属性名与某个 Bean 的名称一致,则注入该 Bean;
- 若属性名与所有 Bean 名称都不匹配,会抛出
NoUniqueBeanDefinitionException,此时需要通过@Qualifier显式指定要注入的 Bean 名称。
举例说明:
// SmsService 接口有两个实现类:SmsServiceImpl1、SmsServiceImpl2(均被 Spring 管理)
// 报错:byType 匹配到多个 Bean,且属性名 "smsService" 与两个实现类的默认名称(smsServiceImpl1、smsServiceImpl2)都不匹配
@Autowired
private SmsService smsService;
// 正确:属性名 "smsServiceImpl1" 与实现类 SmsServiceImpl1 的默认名称匹配
@Autowired
private SmsService smsServiceImpl1;
// 正确:通过 @Qualifier 显式指定 Bean 名称 "smsServiceImpl1"
@Autowired
@Qualifier(value = "smsServiceImpl1")
private SmsService smsService;实际开发实践中,我们还是建议通过 @Qualifier 注解来显式指定名称而不是依赖变量的名称。
@Resource 源自 JSR-250 规范。在 JDK 6 到 JDK 10 中,javax.annotation.Resource 曾随 JDK 提供;从 JDK 11 开始需要单独引入 API 依赖。Spring 5/Java EE 8 项目通常使用 javax.annotation-api,Spring 6/Jakarta EE 9+ 项目使用 jakarta.annotation-api。
Spring 处理无参数 @Resource 时,先按字段名或属性名查找;未找到同名 Bean 才回退到按类型匹配。按类型找到多个候选 Bean 时不会继续“筛选”,而会因不唯一抛出异常。
@Resource 有两个比较重要且日常开发常用的属性:name(名称)、type(类型)。
public @interface Resource {
String name() default "";
Class<?> type() default Object.class;
}如果仅指定 name 属性则注入方式为byName,如果仅指定type属性则注入方式为byType,如果同时指定name 和type属性(不建议这么做)则注入方式为byType+byName。
// 报错,byName 和 byType 都无法匹配到 bean
@Resource
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Resource
private SmsService smsServiceImpl1;
// 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式)
@Resource(name = "smsServiceImpl1")
private SmsService smsService;简单总结一下:
@Autowired是 Spring 提供的注解,@Resource是 Jakarta Annotations/JSR-250 规范提供的注解。Autowired默认的注入方式为byType(根据类型进行匹配),@Resource默认注入方式为byName(根据名称进行匹配)。- 当一个接口存在多个实现类的情况下,
@Autowired和@Resource都需要通过名称才能正确匹配到对应的 Bean。Autowired可以通过@Qualifier注解来显式指定名称,@Resource可以通过name属性来显式指定名称。 @Autowired支持在构造函数、方法、字段和参数上使用。@Resource主要用于字段和方法上的注入,不支持在构造函数或参数上使用。
考虑到 @Resource 的语义更清晰(名称优先),并且是 Java 标准,能减少对 Spring 框架的强耦合,我们通常更推荐使用 @Resource,尤其是在需要按名称注入的场景下。而 @Autowired 配合构造器注入,在实现依赖注入的不可变性和强制性方面有优势,也是一种非常好的实践。
注入 Bean 的方式有哪些?
依赖注入 (Dependency Injection, DI) 的常见方式:
- 构造函数注入:通过类的构造函数来注入依赖项。
- Setter 注入:通过类的 Setter 方法来注入依赖项。
- Field(字段) 注入:直接在类的字段上使用注解(如
@Autowired或@Resource)来注入依赖项。
构造函数注入示例:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
//...
}Setter 注入示例:
@Service
public class UserService {
private UserRepository userRepository;
// 在 Spring 4.3 及以后的版本,特定情况下 @Autowired 可以省略不写
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
//...
}Field 注入示例:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
//...
}⭐️构造函数注入还是 Setter 注入?
Spring 官方有对这个问题的回答:https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html#beans-setter-injection。
我这里主要提取总结完善一下 Spring 官方的建议。
Spring 官方推荐构造函数注入,这种注入方式的优势如下:
- 依赖完整性:确保所有必需依赖在对象创建时就被注入,避免了空指针异常的风险。
- 不可变性:有助于创建不可变对象,提高了线程安全性。
- 初始化保证:组件在使用前已完全初始化,减少了潜在的错误。
- 测试便利性:在单元测试中,可以直接通过构造函数传入模拟的依赖项,而不必依赖 Spring 容器进行注入。
构造函数注入适合处理必需的依赖项,而 Setter 注入 则更适合可选的依赖项,这些依赖项可以有默认值或在对象生命周期中动态设置。虽然 @Autowired 可以用于 Setter 方法来处理必需的依赖项,但构造函数注入仍然是更好的选择。
在某些情况下(例如第三方类不提供 Setter 方法),构造函数注入可能是唯一的选择。
⭐️Bean 的作用域有哪些?
Spring 中 Bean 的作用域通常有下面几种:
- singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
- prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续
getBean()两次,得到的是不同的 Bean 实例。 - request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
- session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
- application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
- websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
如何配置 bean 的作用域呢?
xml 方式:
<bean id="..." class="..." scope="singleton"></bean>注解方式:
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person personPrototype() {
return new Person();
}⭐️Bean 是线程安全的吗?
Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。
我们这里以最常用的两种作用域 prototype 和 singleton 为例介绍。几乎所有场景的 Bean 作用域都是使用默认的 singleton ,重点关注 singleton 作用域即可。
prototype 作用域只保证每次向容器获取时创建新实例,不提供线程安全保证:调用方若把同一个 prototype 实例共享给多个线程,仍可能发生竞争。singleton Bean 在容器中共享,更容易暴露可变状态问题;Bean 是否线程安全最终取决于自身状态、依赖对象和访问方式,而不能只看作用域。
有状态 Bean 示例:
// 定义了一个购物车类,其中包含一个保存用户的购物车里商品的 List
@Component
public class ShoppingCart {
private List<String> items = new ArrayList<>();
public void addItem(String item) {
items.add(item);
}
public List<String> getItems() {
return items;
}
}不过,大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。
无状态 Bean 示例:
// 定义了一个用户服务,它仅包含业务逻辑而不保存任何状态。
@Component
public class UserService {
public User findUserById(Long id) {
//...
}
//...
}对于有状态单例 Bean 的线程安全问题,常见的三种解决办法是:
- 避免可变成员变量: 尽量设计 Bean 为无状态。
- 使用
ThreadLocal: 将可变成员变量保存在ThreadLocal中,确保线程独立。 - 使用同步机制: 利用
synchronized或ReentrantLock来进行同步控制,确保线程安全。
这里以 ThreadLocal为例,演示一下ThreadLocal 保存用户登录信息的场景:
public class UserThreadLocal {
private UserThreadLocal() {}
private static final ThreadLocal<SysUser> LOCAL = ThreadLocal.withInitial(() -> null);
public static void put(SysUser sysUser) {
LOCAL.set(sysUser);
}
public static SysUser get() {
return LOCAL.get();
}
public static void remove() {
LOCAL.remove();
}
}⭐️Bean 的生命周期了解么?
- 创建 Bean 的实例:Bean 容器找到 BeanDefinition 后,选用构造器、静态/实例工厂方法或 Supplier 等实例化策略;反射是常见手段,但不是唯一方式。
- Bean 属性赋值/填充:为已经实例化的 Bean 设置属性和依赖,例如处理字段或 Setter 方法上的
@Autowired、@Value、@Resource。构造器注入发生在实例化阶段,不属于实例创建后的属性填充。 - Bean 初始化:
- 如果 Bean 实现了
BeanNameAware接口,调用setBeanName()方法,传入 Bean 的名字。 - 如果 Bean 实现了
BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 - 如果 Bean 实现了
BeanFactoryAware接口,调用setBeanFactory()方法,传入BeanFactory对象的实例。 - 与上面的类似,如果实现了其他
*.Aware接口,就调用相应的方法。 - 如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor对象,执行postProcessBeforeInitialization()方法 - 如果 Bean 实现了
InitializingBean接口,执行afterPropertiesSet()方法。 - 如果 Bean 在配置文件中的定义包含
init-method属性,执行指定的方法。 - 如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor对象,执行postProcessAfterInitialization()方法。
- 如果 Bean 实现了
- 销毁 Bean:销毁并不是说要立马把 Bean 给销毁掉,而是把 Bean 的销毁方法先记录下来,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。
- 如果 Bean 实现了
DisposableBean接口,执行destroy()方法。 - 如果 Bean 在配置文件中的定义包含
destroy-method属性,执行指定的 Bean 销毁方法。或者,也可以直接通过@PreDestroy注解标记 Bean 销毁之前执行的方法。
- 如果 Bean 实现了
AbstractAutowireCapableBeanFactory 的 doCreateBean() 方法中能看到依次执行了这 4 个阶段:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
// 1. 创建 Bean 的实例
BeanWrapper instanceWrapper = null;
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
Object exposedObject = bean;
try {
// 2. Bean 属性赋值/填充
populateBean(beanName, mbd, instanceWrapper);
// 3. Bean 初始化
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
// 4. 销毁 Bean-注册回调接口
try {
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
return exposedObject;
}Aware 接口能让 Bean 能拿到 Spring 容器资源。
Spring 中提供的 Aware 接口主要有:
BeanNameAware:注入当前 bean 对应 beanName;BeanClassLoaderAware:注入加载当前 bean 的 ClassLoader;BeanFactoryAware:注入当前BeanFactory容器的引用。
BeanPostProcessor 接口是 Spring 为修改 Bean 提供的强大扩展点。
public interface BeanPostProcessor {
// 初始化前置处理
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
// 初始化后置处理
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}postProcessBeforeInitialization:Bean 实例化、属性注入完成后,InitializingBean#afterPropertiesSet方法以及自定义的init-method方法之前执行;postProcessAfterInitialization:类似于上面,不过是在InitializingBean#afterPropertiesSet方法以及自定义的init-method方法之后执行。
InitializingBean 和 init-method 是 Spring 为 Bean 初始化提供的扩展点。
public interface InitializingBean {
// 初始化逻辑
void afterPropertiesSet() throws Exception;
}指定 init-method 方法,指定初始化方法:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="demo" class="com.chaycao.Demo" init-method="init"/>
</beans>如何记忆呢?
- 整体上可以简单分为四步:实例化 —> 属性赋值 —> 初始化 —> 销毁。
- 初始化这一步涉及到的步骤比较多,包含
Aware接口的依赖注入、BeanPostProcessor在初始化前后的处理以及InitializingBean和init-method的初始化操作。 - 销毁这一步会注册相关销毁回调接口,最后通过
DisposableBean和destory-method进行销毁。
最后,再分享一张清晰的图解(图源:如何记忆 Spring Bean 的生命周期)。

Spring AOP
⭐️谈谈自己对于 AOP 的了解
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
Spring AOP 基于运行时代理。未强制使用类代理且目标对象存在合适接口时,通常可使用 JDK 动态代理;没有接口或配置了 proxyTargetClass=true 时使用 CGLIB 子类代理。Spring Boot 2.0+ 的 AOP 自动配置默认偏向 CGLIB 类代理;Spring Boot 2.0 之前即使默认属性偏向 JDK 代理,没有可用接口时 Spring 也会回退到 CGLIB,而不是仅因未实现接口就报错。final 类不能被 CGLIB 继承,final/private 方法也不能被这种代理增强。

当然你也可以使用 AspectJ !Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。
AOP 切面编程涉及到的一些专业术语:
| 术语 | 含义 |
|---|---|
| 目标(Target) | 被通知的对象 |
| 代理(Proxy) | 向目标对象应用通知之后创建的代理对象 |
| 连接点(JoinPoint) | 目标对象的所属类中,定义的所有方法均为连接点 |
| 切入点(Pointcut) | 被切面拦截 / 增强的连接点(切入点一定是连接点,连接点不一定是切入点) |
| 通知(Advice) | 增强的逻辑 / 代码,也即拦截到目标对象的连接点之后要做的事情 |
| 切面(Aspect) | 切入点(Pointcut)+通知(Advice) |
| Weaving(织入) | 将通知应用到目标对象,进而生成代理对象的过程动作 |
⭐️Spring AOP 和 AspectJ AOP 有什么区别?
| 特性 | Spring AOP | AspectJ |
|---|---|---|
| 增强方式 | 运行时增强(基于动态代理) | 编译时、后编译或类加载时织入(操作字节码) |
| 切入点支持 | 方法级(Spring Bean 范围内,不支持 final 和 staic 方法) | 方法级、字段、构造器、静态方法等 |
| 性能 | 运行时依赖代理,有一定开销,切面多时性能较低 | 运行时无代理开销,性能更高 |
| 复杂性 | 简单,易用,适合大多数场景 | 功能强大,但相对复杂 |
| 使用场景 | Spring 应用下比较简单的 AOP 需求 | 高性能、高复杂度的 AOP 需求 |
如何选择?
- 功能考量:AspectJ 支持更复杂的 AOP 场景,Spring AOP 更简单易用。如果你需要增强
final方法、静态方法、字段访问、构造器调用等,或者需要在非 Spring 管理的对象上应用增强逻辑,AspectJ 是唯一的选择。 - 性能考量:切面数量较少时两者性能差异不大,但切面较多时 AspectJ 性能更优。
一句话总结:简单场景优先使用 Spring AOP;复杂场景或高性能需求时,选择 AspectJ。
⭐️AOP 常见的通知类型有哪些?

- Before(前置通知):目标对象的方法调用之前触发
- After (后置通知):目标对象的方法调用之后触发
- AfterReturning(返回通知):目标对象的方法调用完成,在返回结果值之后触发
- AfterThrowing(异常通知):目标对象的方法运行中抛出 / 触发异常后触发。AfterReturning 和 AfterThrowing 两者互斥。如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值。
- Around (环绕通知):编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接拿到目标对象,以及要执行的方法,所以环绕通知可以任意的在目标对象的方法调用前后搞事,甚至不调用目标对象的方法
多个切面的执行顺序如何控制?
1、通常使用@Order 注解直接定义切面顺序
// 值越小优先级越高
@Order(3)
@Component
@Aspect
public class LoggingAspect implements Ordered {2、实现Ordered 接口重写 getOrder 方法。
@Component
@Aspect
public class LoggingAspect implements Ordered {
// ....
@Override
public int getOrder() {
// 返回值越小优先级越高
return 1;
}
}Spring MVC
说说自己对于 Spring MVC 了解?
MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。

网上有很多人说 MVC 不是设计模式,只是软件设计规范,我个人更倾向于 MVC 同样是众多设计模式中的一种。java-design-patterns 项目中就有关于 MVC 的相关介绍。

想要真正理解 Spring MVC,我们先来看看 Model 1 和 Model 2 这两个没有 Spring MVC 的时代。
Model 1 时代
很多学 Java 后端比较晚的朋友可能并没有接触过 Model 1 时代下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。
这个模式下 JSP 即是控制层(Controller)又是表现层(View)。显而易见,这种模式存在很多问题。比如控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;再比如前端和后端相互依赖,难以进行测试维护并且开发效率极低。

Model 2 时代
学过 Servlet 并做过相关 Demo 的朋友应该了解“Java Bean(Model)+ JSP(View)+Servlet(Controller) ”这种开发模式,这就是早期的 JavaWeb MVC 开发模式。
- Model:系统涉及的数据,也就是 dao 和 bean。
- View:展示模型中的数据,只是用来展示。
- Controller:接受用户请求,并将请求发送至 Model,最后返回数据给 JSP 并展示给用户

Model2 模式下还存在很多问题,Model2 的抽象和封装程度还远远不够,使用 Model2 进行开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。
于是,很多 JavaWeb 开发相关的 MVC 框架应运而生比如 Struts2,但是 Struts2 比较笨重。
Spring MVC 时代
随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。相比于 Struts2 , Spring MVC 使用更加简单和方便,开发效率更高,并且 Spring MVC 运行速度更快。
MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)、Controller 层(控制层,返回数据给前台页面)。
Spring MVC 的核心组件有哪些?
记住了下面这些组件,也就记住了 SpringMVC 的工作原理。
DispatcherServlet:核心的中央处理器,负责接收请求、分发,并给予客户端响应。HandlerMapping:处理器映射器,根据 URL 去匹配查找能处理的Handler,并会将请求涉及到的拦截器和Handler一起封装。HandlerAdapter:处理器适配器,根据HandlerMapping找到的Handler,适配执行对应的Handler;Handler:请求处理器,处理实际请求的处理器。ViewResolver:视图解析器,根据Handler返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给DispatcherServlet响应客户端
⭐️SpringMVC 工作原理了解吗?
Spring MVC 原理如下图所示:
SpringMVC 工作原理的图解我没有自己画,直接图省事在网上找了一个非常清晰直观的,原出处不明。

流程说明(重要):
- 客户端(浏览器)发送请求,
DispatcherServlet拦截请求。 DispatcherServlet根据请求信息调用HandlerMapping。HandlerMapping根据 URL 去匹配查找能处理的Handler(也就是我们平常说的Controller控制器) ,并会将请求涉及到的拦截器和Handler一起封装。DispatcherServlet调用HandlerAdapter适配器执行Handler。Handler完成对用户请求的处理后,会返回一个ModelAndView对象给DispatcherServlet,ModelAndView顾名思义,包含了数据模型以及相应的视图的信息。Model是返回的数据对象,View是个逻辑上的View。ViewResolver会根据逻辑View查找实际的View。DispaterServlet把返回的Model传给View(视图渲染)。- 把
View返回给请求者(浏览器)
上述流程是传统开发模式(JSP,Thymeleaf 等)的工作原理。然而现在主流的开发方式是前后端分离,这种情况下 Spring MVC 的 View 概念发生了一些变化。由于 View 通常由前端框架(Vue, React 等)来处理,后端不再负责渲染页面,而是只负责提供数据,因此:
- 前后端分离时,后端通常不再返回具体的视图,而是返回纯数据(通常是 JSON 格式),由前端负责渲染和展示。
View的部分在前后端分离的场景下往往不需要设置,Spring MVC 的控制器方法只需要返回数据,不再返回ModelAndView,而是直接返回数据,Spring 会自动将其转换为 JSON 格式。相应的,ViewResolver也将不再被使用。
怎么做到呢?
- 使用
@RestController注解代替传统的@Controller注解,这样所有方法默认会返回 JSON 格式的数据,而不是试图解析视图。 - 如果你使用的是
@Controller,可以结合@ResponseBody注解来返回 JSON。
统一异常处理怎么做?
推荐使用注解的方式统一异常处理,具体会使用到 @ControllerAdvice + @ExceptionHandler 这两个注解 。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(BaseException.class)
public ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) {
//......
}
@ExceptionHandler(value = ResourceNotFoundException.class)
public ResponseEntity<ErrorReponse> handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {
//......
}
}这种异常处理方式不会给 Controller 创建 AOP 代理。Controller 方法抛出异常后,Spring MVC 会通过 HandlerExceptionResolver 处理链查找能处理该异常的 @ExceptionHandler 方法。
ExceptionHandlerMethodResolver 中 getMappedMethod 方法决定了异常具体被哪个被 @ExceptionHandler 注解修饰的方法处理异常。
@Nullable
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<>();
//找到可以处理的所有异常信息。mappedMethods 中存放了异常和处理异常的方法的对应关系
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
// 不为空说明有方法处理异常
if (!matches.isEmpty()) {
// 按照匹配程度从小到大排序
matches.sort(new ExceptionDepthComparator(exceptionType));
// 返回处理异常的方法
return this.mappedMethods.get(matches.get(0));
}
else {
return null;
}
}从源代码看出:getMappedMethod()会首先找到可以匹配处理异常的所有方法信息,然后对其进行从小到大的排序,最后取最小的那一个匹配的方法(即匹配度最高的那个)。
Spring 框架中用到了哪些设计模式?
关于下面这些设计模式的详细介绍,可以看我写的 Spring 中的设计模式详解 这篇文章。
- 工厂设计模式 : Spring 使用工厂模式通过
BeanFactory、ApplicationContext创建 bean 对象。 - 代理设计模式 : Spring AOP 功能的实现。
- 单例设计模式 : Spring 中的 Bean 默认都是单例的。
- 模板方法模式 : Spring 中
jdbcTemplate、hibernateTemplate等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 - 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
- 适配器模式 : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配
Controller。 - ……
⭐️Spring 的循环依赖
Spring 循环依赖了解吗,怎么解决?
循环依赖是指 Bean 对象循环引用,是两个或多个 Bean 之间相互持有对方的引用,例如 CircularDependencyA → CircularDependencyB → CircularDependencyA。
@Component
public class CircularDependencyA {
@Autowired
private CircularDependencyB circB;
}
@Component
public class CircularDependencyB {
@Autowired
private CircularDependencyA circA;
}单个对象的自我依赖也会出现循环依赖,但这种概率极低,属于是代码编写错误。
@Component
public class CircularDependencyA {
@Autowired
private CircularDependencyA circA;
}Spring 框架可以通过三级缓存解决部分单例 Bean 的 Setter/字段注入循环依赖。构造器循环依赖、prototype Bean 循环依赖等场景不能依靠该机制解决;即使容器能解决,也不代表这种设计值得保留。
Spring 中的三级缓存其实就是三个 Map,如下:
// 一级缓存
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 三级缓存
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);简单来说,Spring 的三级缓存包括:
- 一级缓存(singletonObjects):存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池,为“Spring 的单例属性”⽽⽣。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。
- 二级缓存(earlySingletonObjects):存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中
ObjectFactory产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用ObjectFactory#getObject()都是会产生新的代理对象的。 - 三级缓存(singletonFactories):存放
ObjectFactory,ObjectFactory的getObject()方法(最终调用的是getEarlyBeanReference()方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。
接下来说一下 Spring 创建 Bean 的流程:
- 先去 一级缓存
singletonObjects中获取,存在就返回; - 如果不存在或者对象正在创建中,于是去 二级缓存
earlySingletonObjects中获取; - 如果还没有获取到,就去 三级缓存
singletonFactories中获取,通过执行ObjectFactory的getObject()就可以获取该对象,获取成功之后,从三级缓存移除,并将该对象加入到二级缓存中。
在三级缓存中存储的是 ObjectFacoty :
public interface ObjectFactory<T> {
T getObject() throws BeansException;
}Spring 在创建 Bean 的时候,如果允许循环依赖的话,Spring 就会将刚刚实例化完成,但是属性还没有初始化完的 Bean 对象给提前暴露出去,这里通过 addSingletonFactory 方法,向三级缓存中添加一个 ObjectFactory 对象:
// AbstractAutowireCapableBeanFactory # doCreateBean #
public abstract class AbstractAutowireCapableBeanFactory ... {
protected Object doCreateBean(...) {
//...
// 支撑循环依赖:将 ()->getEarlyBeanReference 作为一个 ObjectFactory 对象的 getObject() 方法加入到三级缓存中
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
}那么上边在说 Spring 创建 Bean 的流程时说了,如果一级缓存、二级缓存都取不到对象时,会去三级缓存中通过 ObjectFactory 的 getObject 方法获取对象。
class A {
// 使用了 B
private B b;
}
class B {
// 使用了 A
private A a;
}以上面的循环依赖代码为例,整个解决循环依赖的流程如下:
- 当 Spring 创建 A 之后,发现 A 依赖了 B ,又去创建 B,B 依赖了 A ,又去创建 A;
- 在 B 创建 A 的时候,那么此时 A 就发生了循环依赖,由于 A 此时还没有初始化完成,因此在 一二级缓存 中肯定没有 A;
- 那么此时就去三级缓存中调用
getObject()方法去获取 A 的 前期暴露的对象 ,也就是调用上边加入的getEarlyBeanReference()方法,生成一个 A 的 前期暴露对象; - 然后就将这个
ObjectFactory从三级缓存中移除,并且将前期暴露对象放入到二级缓存中,那么 B 就将这个前期暴露对象注入到依赖,来支持循环依赖。
只用两级缓存够吗? 在没有 AOP 的情况下,确实可以只使用一级和二级缓存来解决循环依赖问题。但是,当涉及到 AOP 时,三级缓存就显得非常重要了,因为它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象,从而避免了同一个 Bean 有多个代理对象的问题。
最后总结一下 Spring 如何解决三级缓存:
在三级缓存这一块,主要记一下 Spring 是如何支持循环依赖的即可,也就是如果发生循环依赖的话,就去 三级缓存 singletonFactories 中拿到三级缓存中存储的 ObjectFactory 并调用它的 getObject() 方法来获取这个循环依赖对象的早期引用(对象还未完成初始化),并且将这个早期引用放到二级缓存中,这样在循环依赖时就不会重复初始化。
三级缓存只适用于部分单例 Bean 的 Setter/字段注入循环依赖,非单例 Bean、构造器循环依赖以及某些代理创建时机冲突的场景仍无法通过它解决。
@Lazy 能解决循环依赖吗?
@Lazy 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。
Spring Boot 2.2 新增了全局懒加载属性,开启后全局 bean 被设置为懒加载,需要时再去创建。
配置文件配置全局懒加载:
#默认false
spring.main.lazy-initialization=true编码的方式设置全局懒加载:
SpringApplication springApplication=new SpringApplication(Start.class);
springApplication.setLazyInitialization(true);
springApplication.run(args);如非必要,尽量不要用全局懒加载。全局懒加载会让 Bean 第一次使用的时候加载会变慢,并且它会延迟应用程序问题的发现(当 Bean 被初始化时,问题才会出现)。
如果一个 Bean 没有被标记为懒加载,那么它会在 Spring IoC 容器启动的过程中被创建和初始化。如果一个 Bean 被标记为懒加载,那么它不会在 Spring IoC 容器启动时立即实例化,而是在第一次被请求时才创建。这可以帮助减少应用启动时的初始化时间,也可以用来解决循环依赖问题。
循环依赖问题是如何通过@Lazy 解决的呢?假设 A 和 B 循环依赖,可以在 A 对 B 的注入点添加 @Lazy,例如构造参数 A(@Lazy B b)。此时延迟解析的是依赖 B,并不是简单地把 @Lazy 标在 A 的构造器或类型上。
- 首先 Spring 会去创建 A 的 Bean,创建时需要注入 B 的属性;
- 由于在 A 对 B 的注入点标注了
@Lazy,Spring 会创建 B 的延迟解析代理对象并注入 A; - 之后开始执行 B 的实例化、初始化,在注入 B 中的 A 属性时,此时 A 已经创建完毕了,就可以将 A 给注入进去。
从上面的加载流程可以看出: @Lazy 解决循环依赖的关键点在于代理对象的使用。
- 没有
@Lazy的情况下:在 Spring 容器初始化A时会立即尝试创建B,而在创建B的过程中又会尝试创建A,最终导致循环依赖(即无限递归,最终抛出异常)。 - 使用
@Lazy的情况下:Spring 不会立即创建B,而是会注入一个B的代理对象。由于此时B仍未被真正初始化,A的初始化可以顺利完成。等到A实例实际调用B的方法时,代理对象才会触发B的真正初始化。
@Lazy 注入点代理能够在一定程度上打破循环依赖链,包括某些构造器注入场景。但这没有从设计上消除循环依赖,复杂依赖下还可能产生更隐蔽的初始化问题,因此最佳实践仍是重新划分职责、避免循环依赖。
SpringBoot 允许循环依赖发生么?
SpringBoot 2.6.x 以前是默认允许循环依赖的,也就是说你的代码出现了循环依赖问题,一般情况下也不会报错。SpringBoot 2.6.x 以后官方不再推荐编写存在循环依赖的代码,建议开发者自己写代码的时候去减少不必要的互相依赖。这其实也是我们最应该去做的,循环依赖本身就是一种设计缺陷,我们不应该过度依赖 Spring 而忽视了编码的规范和质量,说不定未来某个 SpringBoot 版本就彻底禁止循环依赖的代码了。
SpringBoot 2.6.x 以后,如果你不想重构循环依赖的代码的话,也可以采用下面这些方法:
- 在全局配置文件中设置允许循环依赖存在:
spring.main.allow-circular-references=true。最简单粗暴的方式,不太推荐。 - 在导致循环依赖的 Bean 上添加
@Lazy注解,这是一种比较推荐的方式。@Lazy用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。 - ……
⭐️Spring 事务
这里只保留面试最常追问的事务问题。事务抽象和使用方式的完整介绍可以看主站的 Spring 事务详解。
⭐️Spring 支持哪两种事务管理方式?
- 编程式事务:通过
TransactionTemplate或PlatformTransactionManager手动控制事务。事务边界清晰、控制灵活,适合只需要包裹少量数据库操作的场景,但业务代码会掺杂事务控制逻辑。 - 声明式事务:通过
@Transactional或 XML 配置声明事务。它基于 Spring AOP 实现,代码更简洁,也是日常开发最常见的方式。
声明式事务不一定适合包住整个业务方法。假设一个方法先执行数据库操作,再调用耗时的远程接口,直接给整个方法添加 @Transactional 会延长连接和锁的持有时间。这时可以用 TransactionTemplate 缩小事务范围。
⭐️@Transactional 的基本原理是什么?
默认代理模式下,Spring 会为目标 Bean 创建代理。外部调用事务方法时,请求先经过代理中的事务拦截器:
- 读取方法或类上的事务属性。
- 通过对应的事务管理器开启事务,或者按照传播行为加入现有事务。
- 调用目标方法。
- 方法正常结束时提交事务;抛出符合回滚规则的异常时回滚事务。
- 清理与当前线程绑定的事务资源。
@Transactional 只负责声明事务属性,真正完成开启、提交和回滚的是代理、事务拦截器和事务管理器。
⭐️为什么同一个类中的方法自调用会导致事务失效?
默认代理模式只能拦截经过代理对象的方法调用。同一个对象内部通过 this.method() 调用另一个方法时,请求直接落到目标对象,不会再次经过代理,因此被调用方法上的 @Transactional 不会生效。
@Service
public class OrderService {
public void createOrder() {
// 直接调用当前对象的方法,没有经过 Spring 代理
saveOrder();
}
@Transactional
public void saveOrder() {
// 数据库操作
}
}常见解决办法有:
- 把事务方法拆到另一个 Spring Bean 中,通过 Bean 之间的调用进入代理。
- 把
@Transactional放到外部实际调用的方法上,重新划分事务边界。 - 使用
TransactionTemplate显式控制事务。 - 确实需要拦截自调用时使用 AspectJ 模式,但实现和维护成本更高。
不建议在业务代码中到处调用 AopContext.currentProxy() 获取代理对象,这会让业务逻辑与 AOP 基础设施强耦合。
除了自调用,还有哪些情况会导致 @Transactional 没生效?
可以从下面几个方向排查:
- 对象没有交给 Spring 管理:通过
new创建的对象不会经过 Spring 代理。 - 调用绕过了代理:除了自调用,直接持有并调用原始对象也会绕过代理。
- 方法不符合代理规则:JDK 动态代理要求方法在接口中声明;
private、final方法不能按照普通覆盖方式被代理。 - 异常没有命中回滚规则:Spring 默认不会因受检异常回滚。
- 异常被方法内部捕获:事务拦截器看到方法正常返回,会尝试提交事务。
- 事务管理器选错:多数据源项目中,事务管理器控制的数据源和实际操作的数据源不是同一个。
- 底层资源不支持事务:数据库表、外部服务或者其他资源不受当前事务管理器控制。
排查事务失效时,先确认运行时对象是不是代理、调用链是否经过代理、事务是否真的开启,再检查异常和底层资源。
⭐️Spring 事务中有哪几种事务传播行为?
传播行为决定一个事务方法被调用时,应该加入现有事务、创建新事务、以非事务方式运行,还是拒绝调用。它描述的是方法调用边界上的事务关系,不是数据库的事务隔离级别。
Spring 定义了 7 种传播行为:
| 传播行为 | 当前有事务 | 当前无事务 |
|---|---|---|
REQUIRED | 加入当前事务 | 新建事务 |
REQUIRES_NEW | 挂起当前事务,新建独立事务 | 新建事务 |
NESTED | 在当前事务中创建嵌套保存点 | 通常按 REQUIRED 处理 |
SUPPORTS | 加入当前事务 | 非事务运行 |
MANDATORY | 加入当前事务 | 抛出异常 |
NOT_SUPPORTED | 挂起当前事务,非事务运行 | 非事务运行 |
NEVER | 抛出异常 | 非事务运行 |
@Transactional 默认使用 REQUIRED。其他传播行为需要根据内外层操作是否应该互相影响、是否允许独立提交来选择。
⭐️REQUIRED、REQUIRES_NEW 和 NESTED 有什么区别?
| 对比项 | REQUIRED | REQUIRES_NEW | NESTED |
|---|---|---|---|
| 物理事务 | 与外层共用 | 新建独立事务 | 与外层共用 |
| 外层事务 | 直接参与 | 暂时挂起 | 继续存在 |
| 回滚方式 | 共同提交或回滚 | 内外层可以独立提交或回滚 | 可以回滚到保存点 |
| 典型用途 | 一组操作必须同成同败 | 确实需要独立提交的审计记录等操作 | 允许局部失败、外层继续的 JDBC 操作 |
| 主要代价 | 内层失败影响整个事务 | 额外占用连接,可能导致连接池耗尽 | 依赖事务管理器和数据库保存点能力 |
NESTED 通常基于 JDBC 保存点实现,不会新建独立事务。内层可以回滚到保存点,但外层最终回滚时,内层结果仍会一起回滚。
REQUIRES_NEW 会挂起已经占用连接的外层事务,再为内层事务申请一个连接。如果多个并发线程都持有外层连接并等待内层连接,而连接池没有额外容量,就可能出现连接池耗尽。使用前要评估并发量、嵌套次数和连接池大小。
Spring 事务中的隔离级别有哪几种?
Spring 通过 Isolation 枚举提供 5 种隔离级别:
| 隔离级别 | 说明 |
|---|---|
DEFAULT | 使用底层数据库的默认隔离级别 |
READ_UNCOMMITTED | 可以读取其他事务尚未提交的数据,可能出现脏读、不可重复读和幻读 |
READ_COMMITTED | 只能读取已经提交的数据,可以避免脏读 |
REPEATABLE_READ | 同一事务内多次读取同一数据的结果保持一致,可以避免脏读和不可重复读 |
SERIALIZABLE | 通过串行化执行提供最强隔离,但并发性能最低 |
隔离级别最终由数据库实现。不同数据库对快照读、锁定读和幻读的处理可能不同,不能只根据 Spring 枚举判断具体行为。一个方法加入已有事务时,内层声明的隔离级别通常也不会改变已经存在的物理事务。
⭐️Spring 默认在哪些异常下回滚?
默认情况下,Spring 遇到 RuntimeException 或 Error 时回滚,遇到受检异常时不回滚。

如果业务要求受检异常也回滚,可以通过 rollbackFor 显式配置:
@Transactional(rollbackFor = Exception.class)
public void createOrder() throws Exception {
// 数据库操作
}也可以使用 noRollbackFor 排除不需要回滚的异常。业务异常是否应该回滚,取决于异常发生后当前数据能否合法提交,不能只根据异常名称判断。
方法把异常 catch 住了,事务还会回滚吗?
通常不会。事务拦截器根据方法最终是正常返回还是抛出异常来决定提交或回滚。方法内部捕获异常后正常返回,外层代理无法知道业务操作已经失败。
常见处理方式有:
- 记录必要信息后重新抛出异常。
- 业务必须捕获异常时,使用
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()把当前事务标记为只能回滚。 - 调整事务边界,让负责判断成功或失败的上层方法控制事务。
如果内外层方法都使用 REQUIRED,它们会加入同一个物理事务。内层抛出运行时异常后,事务可能已经被标记为 rollback-only。即使外层捕获异常并正常返回,最终提交时 Spring 仍会回滚,并抛出 UnexpectedRollbackException,防止调用方误以为事务提交成功。
⭐️@Async 方法能继承调用方的事务吗?
通常不能。命令式事务资源与当前线程绑定,@Async 会在另一个线程中执行,原线程的事务上下文不会自动传播过去。
如果异步方法自身需要事务,可以把它放在独立 Bean 中,并在异步线程入口声明事务。此时开启的是一个独立事务,还要注意调用方事务可能尚未提交,异步任务就已经开始读取数据。
需要在主事务提交后可靠执行的任务,可以根据业务选择事务事件、消息队列或任务表,并明确重试和幂等策略。
事务方法里可以调用远程接口吗?
技术上可以,但通常不建议把不可控的远程调用放在本地数据库事务中。远程服务变慢会延长数据库连接和锁的持有时间,放大超时、死锁和连接池压力;远程调用成功后,本地事务仍可能回滚,两个系统也不会因此形成原子事务。
常见处理方式是缩小本地事务,只完成必要的数据变更。需要跨服务通知时,可以把待发送事件和业务数据写入同一个本地事务,再通过 Outbox、本地消息表或事务消息可靠投递,并配合幂等、重试、补偿和对账。
⭐️Spring 事务边界应该放在哪一层?
事务边界通常放在编排一个完整业务用例的 Service 层。这个方法知道哪些数据库操作必须同成同败,也能避免 Controller 和 DAO 各自控制一小段事务,造成业务边界割裂。
事务范围不是越大越好。一个实用原则是:覆盖必须原子完成的数据库操作,但不要包住用户交互、文件上传和不可控的远程调用。 同时避免慢 SQL、大批量循环和复杂计算长时间占用事务资源。
线上出现“事务没有回滚”,你会怎么排查?
可以按照下面的顺序排查:
- 确认现象:是整个事务都提交了,还是部分操作来自另一个数据源、异步线程或远程服务?
- 确认代理:对象是否由 Spring 管理,调用是否经过代理,是否存在自调用?
- 确认事务:事务是否真的开启,使用了哪个事务管理器,传播行为是什么?
- 确认异常:异常有没有抛出代理边界,是否被捕获,是否命中回滚规则?
- 确认资源:数据库和执行方式是否支持事务,相关操作是否使用同一个事务连接?
- 复现验证:打开事务日志并补充集成测试,用最小案例验证数据库最终状态。
遇到“部分数据回滚、部分没有回滚”时,重点确认未回滚的操作属于哪个线程、连接、数据源和事务管理器。
Spring Security
Spring Security 重要的是实战,这里仅对小部分知识点进行总结。
有哪些控制请求访问权限的方法?

permitAll():无条件允许任何形式访问,不管你登录还是没有登录。anonymous():允许匿名访问,也就是没有登录才可以访问。denyAll():无条件决绝任何形式的访问。authenticated():只允许已认证的用户访问。fullyAuthenticated():只允许完整认证的用户访问,不接受匿名认证或 remember-me 认证。hasRole(String): 只允许指定的角色访问。hasAnyRole(String): 指定一个或者多个角色,满足其一的用户即可访问。hasAuthority(String):只允许具有指定权限的用户访问hasAnyAuthority(String):指定一个或者多个权限,满足其一的用户即可访问。hasIpAddress(String): 只允许指定 ip 的用户访问。
hasRole 和 hasAuthority 有区别吗?
可以看看松哥的这篇文章:Spring Security 中的 hasRole 和 hasAuthority 有区别吗?,介绍的比较详细。
⭐️如何对密码进行加密?
如果需要保存密码,应先通过带盐、可调工作因子的自适应单向哈希编码再保存,而不是使用可逆加密,也不能保存明文。
Spring Security 提供了多种密码编码实现,它们实现 PasswordEncoder 接口。
PasswordEncoder 有 encode()、matches() 两个抽象方法,以及可按需覆盖的默认方法 upgradeEncoding()。
public interface PasswordEncoder {
// 对原始密码进行单向编码
String encode(CharSequence var1);
// 比对原始密码和数据库中保存的密码
boolean matches(CharSequence var1, String var2);
// 判断已编码密码是否需要升级,默认返回 false
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
官方推荐使用可调节工作因子的自适应单向函数,并根据服务器性能把单次验证耗时调到可接受范围,例如 bcrypt、PBKDF2、scrypt 或 Argon2。登录时用 matches() 验证,不能通过“解密”取回原密码。
如何优雅更换系统使用的加密算法?
如果现有密码编码方案的工作因子或算法已经不能满足安全要求,需要升级到新的方案,这个时候应该怎么办呢?
推荐的做法是通过 DelegatingPasswordEncoder 兼容多种密码编码方案:新密码使用当前推荐方案,旧密码仍可按其编码前缀验证,并在用户成功登录后逐步升级。
DelegatingPasswordEncoder 本身不是新的密码哈希算法,而是根据编码结果中的 {id} 前缀委派给对应的 PasswordEncoder。Spring Security 5.0 之后,框架默认使用这种可迁移的编码格式。
SpringBoot
⭐️Spring,Spring MVC,Spring Boot 之间什么关系?
很多人对 Spring,Spring MVC,Spring Boot 这三者傻傻分不清楚!这里简单介绍一下这三者,其实很简单,没有什么高深的东西。
Spring 包含了多个功能模块(上面刚刚提到过),其中最重要的是 Spring-Core(主要提供 IoC 依赖注入功能的支持) 模块, Spring 中的其他模块(比如 Spring MVC)的功能实现基本都需要依赖于该模块。
下图对应的是 Spring Framework 4.x。Spring Framework 5.0 引入了响应式、非阻塞的 WebFlux,并逐步淘汰 Portlet 相关支持;现代版本的模块组成应以目标版本官方文档为准。WebFlux 可以运行在 Netty 或支持非阻塞 I/O 的 Servlet 容器上,但只有数据库驱动、远程调用等下游也不阻塞时,应用才是端到端非阻塞。

Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。

使用 Spring 进行开发各种配置过于麻烦比如开启某些 Spring 特性时,需要用 XML 或 Java 进行显式配置。于是,Spring Boot 诞生了!
Spring 旨在简化 J2EE 企业应用程序开发。Spring Boot 旨在简化 Spring 开发(减少配置文件,开箱即用!)。
Spring Boot 只是简化了配置,如果你需要构建 MVC 架构的 Web 程序,你还是需要使用 Spring MVC 作为 MVC 框架,只是说 Spring Boot 帮你简化了 Spring MVC 的很多配置,真正做到开箱即用!
⭐️Spring Boot 支持哪些内嵌 Servlet 容器?如何选择?
Spring Boot 3.x 常见的内嵌 Servlet 容器包括 Tomcat、Jetty 和 Undertow;具体支持项随 Spring Boot 大版本变化。spring-boot-starter-webflux 默认使用 Reactor Netty,它不属于 Servlet 容器。
当你在项目中引入 spring-boot-starter-web 这个起步依赖时,Spring Boot 默认会包含并启用 Tomcat 作为内嵌 Servlet 容器。
如果你想使用 Jetty 或 Undertow,需要在构建文件(如 Maven 的 pom.xml或 Gradle 的 build.gradle)中,从 spring-boot-starter-web 中排除默认的 Tomcat 依赖 (spring-boot-starter-tomcat),添加你想使用的容器对应的 Starter 依赖(例如 spring-boot-starter-jetty 或 spring-boot-starter-undertow)。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- 排除默认的 Tomcat 依赖 -->
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!--引入其他的 Servlet 容器-->
<dependency>
<artifactId>spring-boot-starter-jetty</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>在 Spring Boot 项目中,我们可以根据具体应用场景和性能需求,灵活地选择不同的嵌入式 Servlet 容器来提供 HTTP 服务:
- Tomcat:默认选择,生态成熟,适合大多数 Servlet Web 应用。
- Undertow:在 Spring Boot 3.x 中可选,资源占用和性能特征不同,是否更快要用真实业务压测验证。
- Jetty:可作为 Servlet 容器替代方案,对长连接和 WebSocket 有成熟支持,但也不能脱离版本、配置和负载断言一定优于 Tomcat。
⚠️ 注意 :
Spring Boot 4.0 完全移除了对 Undertow 的内嵌支持——不仅删掉了 spring-boot-starter-undertow,也不再提供任何 Undertow 相关的自动配置。移除的根本原因是:Spring Boot 4.0 基线升级到 Servlet 6.1(也就是说必须支持 Servlet 6.1 才能留在 starter 列表里),而截至 2025-10 官方发布说明时,Undertow 尚未兼容该版本。
Spring Boot 默认使用的日志框架是什么?
Spring Boot 默认选用 SLF4J (Simple Logging Facade for Java) 作为其日志门面 (Facade) / 日志抽象层,并搭配 Logback 作为默认的具体日志实现库 (Implementation)。
⭐️Spring Boot 的自动配置是如何实现的?
Spring Boot 的自动配置机制是通过 @SpringBootApplication 注解启动的,这个注解本质上是几个关键注解的组合。我们可以将 @SpringBootApplication 看作是 @Configuration、@EnableAutoConfiguration 和 @ComponentScan 注解的集合。
@EnableAutoConfiguration: 启用 Spring Boot 的自动配置机制。它是自动配置的核心,允许 Spring Boot 根据项目的依赖和配置自动配置 Spring 应用的各个部分。@ComponentScan: 启用组件扫描,扫描被@Component(以及@Service、@Controller等)注解的类,并将这些类注册为 Spring 容器中的 Bean。默认情况下,它会扫描该类所在包及其子包下的所有类。@Configuration: 允许在上下文中注册额外的 Bean 或导入其他配置类。它相当于一个具有@Bean方法的 Spring 配置类。
@EnableAutoConfiguration是启动自动配置的关键,源码如下(建议自己打断点调试,走一遍基本的流程):
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}这个注解通过 @Import 导入了 AutoConfigurationImportSelector 类,而 AutoConfigurationImportSelector 是自动配置的核心类之一。@Import 注解的作用是将指定的配置类或 Bean 导入到当前的配置类中。
AutoConfigurationImportSelector 会加载候选自动配置类,再结合排除项、条件注解和排序等规则筛选。下面代码是 Spring Boot 2.1.x 的旧版源码节选,用于理解流程,不代表现代版本的完整实现。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}Spring Boot 2.6 及更早版本主要通过 META-INF/spring.factories 的 EnableAutoConfiguration key 注册自动配置类。Spring Boot 2.7 引入 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 并兼容旧方式;Spring Boot 3.0 移除了通过 spring.factories 的该 key 注册自动配置类的支持,但 spring.factories 的其他用途仍然保留。
自动配置信息有了,那么自动配置还差什么呢?
@Conditional 注解!在自动配置类中,Spring Boot 使用了一系列条件注解(如 @Conditional、@ConditionalOnClass、@ConditionalOnBean 等)来判断某些配置是否应该生效。这些注解是 @Conditional 注解的扩展,用于在特定条件满足时才启用相应的配置。
例如,自动配置类通常会组合 @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 等条件,做到“类路径存在、用户未自行配置且属性满足时”才生效。不同 Spring Boot/Spring Security 版本的配置类和条件会变化,下面使用 WebSecurityConfigurerAdapter 的代码属于旧版本语境;该适配器在 Spring Security 5.7 被弃用,现代项目通常声明 SecurityFilterChain Bean。
WebSecurityEnablerConfiguration 类的源码如下:
@Configuration
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
public class WebSecurityEnablerConfiguration {
}WebSecurityEnablerConfiguration类中使用@ConditionalOnBean指定了容器中必须还有WebSecurityConfigurerAdapter 类或其实现类。所以,一般情况下 Spring Security 配置类都会去实现 WebSecurityConfigurerAdapter,这样自动将配置就完成了。
最后,简单总结一下:Spring Boot 的自动配置由 @EnableAutoConfiguration 启动,AutoConfigurationImportSelector 负责发现、筛选候选自动配置,再由条件注解决定具体配置是否生效。候选配置的注册位置要区分版本:旧版主要是 spring.factories,Spring Boot 2.7+ 主要是 AutoConfiguration.imports。
⭐自动配置是详细的源码解读可以参考 JavaGuide 上这篇文章:SpringBoot 自动装配原理详解。
Spring Boot 如何加载配置文件?如果两种配置文件同时存在,会怎样处理?
Spring Boot 会自动从类路径的根目录(通常是项目的 src/main/resources/ 目录)下查找并加载名为 application.properties 或 application.yml (包括 .yaml 扩展名) 的文件。
如果在同一目录下同时存在 application.properties 和 application.yml 文件,application.properties 文件中的配置项优先级更高,会覆盖 application.yml 中相同的配置项。为了避免配置冲突和混淆,建议在一个项目中只使用一种格式。
如果开发者没有提供任何 application.properties 或 application.yml 文件,或者文件中没有定义某个特定的配置项,Spring Boot 将会使用其内置的默认配置值(如果该配置项有默认值的话)。
Spring Boot 加载配置文件的优先级了解么?
Spring Boot 加载配置文件的优先级设计得非常灵活,主要是为了方便我们在不同环境(开发、测试、生产)下覆盖或指定配置。它的原则是:后加载的覆盖先加载的,而且离用户(或部署环境)越近的优先级越高。
加载顺序如下:
当前项目根目录下
config/子目录的配置文件 (./config/application.yml或./config/application.properties):优先级最高,通常放在运行 Jar 包同级的config目录里。当前项目根目录下的配置文件 (
./application.yml或./application.properties): 直接放在运行 Jar 包同级目录里,优先级次之。类路径内
config/子目录的配置文件 (classpath:/config/application.yml或classpath:/config/application.properties): 对应项目中的src/main/resources/config/下的文件,优先级再次之。类路径下的配置文件 (
classpath:/application.yml或classpath:/application.properties): 对应项目中的src/main/resources/根目录下的文件,在这些位置里优先级最低。
总结:Jar 包外 config/ > Jar 包外根目录 > Jar 包内 config/> Jar 包内根目录。

简单记忆规则:
- 包外 > 包内(方便部署时覆盖配置)。
config/目录 > 根目录(无论包内还是包外,config目录里的配置优先级更高)。
如果某个 Profile 文件(如 application-dev.yml)被激活(通过 spring.profiles.active=dev 指定),那么,在同一个目录下,Profile 文件的优先级高于通用文件。例如:
src/main/resources/application-dev.yml的配置会覆盖src/main/resources/application.yml中的同名配置。- 同样地,Jar 包外的
config/application-dev.yml会覆盖config/application.yml。
通过这样的灵活设计,Spring Boot 能很好地适应各种环境的配置需求,同时确保配置文件的覆盖和管理清晰有序。
⭐️更多 SpringBoot 面试题
更多 Spring Boot 相关的面试题欢迎加入我的知识星球,已经整理到了《Java 面试指北》中。
很多 Spring Boot 重要的新特性都已经同步到了这篇文章中,质量很高,保证内容与时俱进!


