《JAVA面经实录》- Spring 框架面试题
《JAVA面经实录》- Spring 框架面试题
(一)、Spring 高频面试题
一. 基础核心
1. Spring 核心特性是什么?
1.1 面试回答(资深版)
Spring 核心是轻量级、非侵入式的一站式企业级开发框架,两大核心底层:
(1)IOC(控制反转):容器接管对象创建、依赖管理,解耦代码
(2)AOP(面向切面编程):统一处理横切逻辑(日志、事务、权限)
扩展特性:事务管理、Spring MVC、数据访问、事件驱动、整合第三方框架(MyBatis、Redis、RabbitMQ 等)、支持注解式开发、简化配置。
1.2 生产踩坑
踩坑1:过度依赖 Spring 容器,将非业务组件(如工具类)也交给容器管理,导致容器启动缓慢、Bean 数量冗余,甚至出现内存溢出。 解决方案:工具类用 static 方法实现,无需注入容器;仅将 Service、Dao、Controller 等核心业务组件交给 Spring 管理。
1.3 面试追问
(1)Spring 为什么说是“轻量级”?(答:非侵入式,无需继承特定类/实现特定接口;核心包体积小;依赖少;可按需加载组件,不强制使用全部功能)
(2)Spring 非侵入式的具体体现?(答:开发时无需让业务类继承 Spring 的类或实现 Spring 的接口,仅通过注解/配置即可集成,业务代码可独立于 Spring 运行)
2. IOC 是什么?解决了什么问题?
2.1 定义
IOC(Inversion of Control)控制反转:把对象创建、依赖注入、生命周期管理全部交给 Spring 容器,不再由开发者手动 new 对象、维护依赖关系。
2.2 解决的问题
(1)硬编码耦合:不用在代码里 `new Xxx()`,降低类与类之间的强依赖,修改实现类时无需改动业务代码。
(2)代码可维护性差:容器统一管理对象,对象的创建、销毁、依赖替换都可通过配置/注解统一调整,降低维护成本。
(3)资源浪费:容器统一控制 Bean 的作用域(单例/多例),避免频繁创建/销毁对象造成的资源消耗。
(4)便于单元测试:依赖可通过 Mock 注入,无需构造复杂的依赖链,提升测试效率。
2.3 代码对比:传统方式 vs IOC
// 1. 传统方式:强耦合,难测试,修改 UserDaoImpl 需改动此处代码
public class UserService {
// 硬编码依赖,耦合度极高
private UserDao userDao = new UserDaoImpl();
}
// 2. IOC 方式:容器注入,解耦,修改实现类只需调整配置/注解
@Service
public class UserService {
// 容器自动注入,无需手动 new
@Autowired
private UserDao userDao;
}
2.4 生产踩坑
踩坑2:依赖注入时,多个 Bean 实现同一接口,未指定 Bean 名称,导致容器启动报错(NoUniqueBeanDefinitionException)。 解决方案:用 @Qualifier("beanName") 指定注入的 Bean 名称,或用 @Primary 标记默认注入的 Bean。
2.5 面试追问
(1)IOC 和 DI 的关系?(答:DI 是 IOC 的具体实现方式,IOC 是思想,DI 是手段;IOC 强调“反转控制”,DI 强调“依赖注入”,通过 DI 实现 IOC 的核心思想)
(2)Spring 容器初始化时,如何加载 Bean 定义?(答:通过 XML 配置、注解(@Component、@Service 等)、Java 配置类(@Configuration)三种方式加载 BeanDefinition,存入 BeanDefinitionRegistry)
3. DI 依赖注入有哪几种方式?
3.1 标准答案(3种,按推荐优先级排序
-
构造器注入(官方推荐,Spring 4.x+ 重点推荐)
-
优点:依赖不可变(用 final 修饰)、强制依赖(必须传入依赖,否则无法实例化)、线程安全、避免循环依赖(构造器注入无法解决循环依赖,会直接报错,提前暴露问题)。
-
缺点:依赖过多时,构造方法参数过长,代码不够简洁。
-
-
Setter 方法注入
-
优点:可选依赖(可通过 @Autowired(required = false) 设置非强制依赖)、可动态修改依赖(通过 Setter 方法重新赋值)。
-
缺点:依赖可变,线程不安全(需手动保证线程安全)、无法保证依赖一定被注入(可能出现空指针)。
-
-
字段注入(@Autowired,最常用但不推荐)
-
优点:代码简洁,无需写构造器/Setter 方法,开发效率高。
-
缺点:难以单元测试(无法通过构造器注入 Mock 对象)、可能隐藏设计问题(依赖过多时不易察觉)、不支持 final 修饰依赖、Spring 官方不推荐(容易出现空指针)。
-
3.2 代码实现
@Service
public class UserService {
// 1. 构造器注入(推荐),依赖用 final 修饰,不可变
private final UserDao userDao;
private final RoleDao roleDao;
// 构造器注入,Spring 4.3+ 可省略
@Autowired
public UserService(UserDao userDao, RoleDao roleDao) {
this.userDao = userDao;
this.roleDao = roleDao;
}
// 2. Setter 注入,可选依赖
private LogDao logDao;
@Autowired(required = false)
public void setLogDao(LogDao logDao) {
this.logDao = logDao;
}
// 3. 字段注入(不推荐)
@Autowired
private RedisTemplate<String, Object> redisTemplate;
}
3.3 生产踩坑
踩坑3:用字段注入时,在构造方法中使用注入的依赖,导致空指针异常(构造方法执行早于字段注入)。 解决方案:将依赖改为构造器注入,或在 @PostConstruct 方法中使用依赖(构造方法执行后执行)。
3.4 面试追问
-
为什么官方推荐构造器注入?(答:强制依赖、依赖不可变、线程安全、避免循环依赖隐患、便于单元测试)
-
@Autowired 和 @Resource 的区别?(答:@Autowired 是 Spring 注解,按类型注入,可配合 @Qualifier 按名称注入;@Resource 是 JDK 注解,按名称注入,无名称时按类型注入;@Autowired 支持 required = false,@Resource 支持 name 和 type 属性)
4. BeanFactory 和 ApplicationContext 区别?
|
特性 |
BeanFactory |
ApplicationContext |
|---|---|---|
|
定位 |
Spring 顶层容器,基础实现(如 DefaultListableBeanFactory) |
高级容器,继承 BeanFactory,是 BeanFactory 的增强版 |
|
Bean 加载 |
懒加载(调用 getBean() 时才创建 Bean) |
容器启动时就预加载所有单例 Bean(可通过 @Lazy 注解设置懒加载) |
|
功能 |
仅提供基础 DI 功能,无额外扩展 |
支持国际化、事件驱动、资源加载、AOP 自动装配、环境配置等企业级功能 |
|
使用场景 |
内存极小的设备(如嵌入式设备)、对启动速度要求极高的场景 |
99% 的企业级开发场景(Spring Boot 底层用的是 AnnotationConfigApplicationContext) |
|
实现类 |
DefaultListableBeanFactory、XmlBeanFactory(已过时) |
AnnotationConfigApplicationContext、ClassPathXmlApplicationContext、FileSystemXmlApplicationContext |
一句话总结:`ApplicationContext` = BeanFactory + 企业级扩展功能 + 启动预加载,是企业开发的首选。
4.1 生产踩坑
踩坑4:误用 BeanFactory 作为容器,导致无法使用 @Transactional、@EventListener 等功能(BeanFactory 不支持 AOP 自动装配和事件驱动)。 解决方案:企业开发统一使用 ApplicationContext 及其实现类,避免使用 BeanFactory。
4.2 面试追问
-
ApplicationContext 启动时,预加载单例 Bean 的好处和弊端?(答:好处:提前初始化,避免第一次调用时的性能损耗;弊端:容器启动速度变慢,内存占用增加)
-
如何让 ApplicationContext 中的单例 Bean 实现懒加载?(答:在 Bean 上添加 @Lazy 注解,或在 @Bean 注解中设置 lazyInit = true)
5. Spring Bean 的生命周期?
5.1 标准流程(面试必背 11 步,结合源码逻辑)
-
加载 Bean 定义:Spring 容器扫描注解/配置,将 Bean 信息封装为 BeanDefinition,存入 BeanDefinitionRegistry。
-
实例化(Instantiation):容器通过构造方法创建 Bean 实例(无参/有参构造,优先无参)。
-
填充属性(DI 依赖注入):容器将依赖的 Bean 注入到当前 Bean 的字段/Setter 方法中。
-
执行 Aware 接口:如果 Bean 实现了 BeanNameAware、BeanFactoryAware、ApplicationContextAware 等接口,容器会调用对应的方法,注入 Bean 名称、BeanFactory、ApplicationContext 等信息。
-
执行 BeanPostProcessor#postProcessBeforeInitialization(初始化前增强):所有 Bean 初始化前都会执行该方法,可对 Bean 进行动态修改(如添加代理)。
-
执行 @PostConstruct 初始化方法:JSR-250 注解,在 Bean 初始化前执行(由 Spring 容器调用)。
-
执行 InitializingBean#afterPropertiesSet:如果 Bean 实现了该接口,调用该方法,完成自定义初始化逻辑。
-
执行自定义 init-method:通过 XML 配置(init-method)或 @Bean(initMethod = "xxx") 指定的初始化方法。
-
执行 BeanPostProcessor#postProcessAfterInitialization(初始化后增强):所有 Bean 初始化后执行,常用作动态代理(如 AOP 代理)。
-
Bean 就绪(使用中):Bean 进入容器,供其他 Bean 调用或外部使用。
-
销毁(Destruction):容器关闭时,执行顺序为:@PreDestroy → DisposableBean#destroy → 自定义 destroy-method。
5.2 生命周期代码演示
java
@Component
// 自定义 init-method 和 destroy-method(也可通过 @Bean 注解指定)
public class LifeBean implements BeanNameAware, InitializingBean, DisposableBean {
// 1. 实例化(构造方法执行)
public LifeBean() {
System.out.println("1. 实例化:调用构造方法创建 Bean");
}
// 2. Aware 接口:BeanNameAware,注入 Bean 名称
@Override
public void setBeanName(String name) {
System.out.println("2. 执行 Aware 接口:Bean 名称 = " + name);
}
// 3. 依赖注入(Setter 注入,也可字段注入)
@Autowired
public void setUserDao(UserDao userDao) {
System.out.println("3. 依赖注入:注入 UserDao");
}
// 4. 初始化前:@PostConstruct
@PostConstruct
public void postConstruct() {
System.out.println("4. 执行 @PostConstruct 初始化方法");
}
// 5. 初始化:InitializingBean
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("5. 执行 InitializingBean#afterPropertiesSet");
}
// 6. 自定义 init-method(需配置)
public void initMethod() {
System.out.println("6. 执行自定义 init-method");
}
// 7. 销毁前:@PreDestroy
@PreDestroy
public void preDestroy() {
System.out.println("7. 执行 @PreDestroy 销毁前方法");
}
// 8. 销毁:DisposableBean
@Override
public void destroy() throws Exception {
System.out.println("8. 执行 DisposableBean#destroy 销毁方法");
}
}
5.3 生产踩坑
踩坑5:在 @PostConstruct 方法中调用其他 Bean 的方法,未考虑该 Bean 未初始化完成的情况,导致空指针。 原因:@PostConstruct 执行时,当前 Bean 已完成依赖注入,但其他 Bean 可能还未初始化(尤其是非单例 Bean)。 解决方案:使用 Spring 事件驱动(ApplicationEvent),在所有 Bean 初始化完成后执行逻辑;或通过 @DependsOn 注解指定 Bean 的初始化顺序。
5.4 面试追问
-
BeanPostProcessor 的作用是什么?有哪些常用实现类?(答:作用是对 Bean 进行初始化前后的增强,无需修改 Bean 本身;常用实现类:AutowiredAnnotationBeanPostProcessor(处理 @Autowired 注入)、AnnotationAwareAspectJAutoProxyCreator(AOP 代理创建))
-
@PostConstruct 和 init-method 的执行顺序?为什么?(答:@PostConstruct 先执行,再执行 init-method;因为 @PostConstruct 是 JSR-250 注解,由 Spring 的 CommonAnnotationBeanPostProcessor 处理,在 BeanPostProcessor 初始化前阶段执行,而 init-method 是在 InitializingBean 之后执行)
-
Bean 的销毁时机是什么?(答:单例 Bean:容器关闭时销毁;多例 Bean:容器不负责销毁,由开发者手动管理,或由 GC 回收)
6. Bean 的作用域有哪些?singleton 和 prototype 区别?
6.1 5 种作用域(Spring 5.x+ 支持)
-
singleton(默认):单例,容器中只有一个 Bean 实例,所有请求都共享该实例。
-
prototype:多例,每次调用 getBean() 或注入时,都会新建一个 Bean 实例。
-
request:一次 HTTP 请求,创建一个 Bean 实例,请求结束后销毁(仅适用于 Spring MVC 环境)。
-
session:一个 HTTP 会话,创建一个 Bean 实例,会话结束后销毁(仅适用于 Spring MVC 环境)。
-
application:全局应用级,整个 Web 应用中只有一个 Bean 实例,与 ServletContext 生命周期一致(仅适用于 Spring MVC 环境)。
6.2 singleton vs prototype(高频对比,面试必背)
|
特性 |
singleton(单例) |
prototype(多例) |
|---|---|---|
|
创建次数 |
容器启动时(或第一次调用时,懒加载)创建 1 次 |
每次 getBean() 或注入时,都新建 1 个实例 |
|
线程安全 |
不安全(无状态 Bean 才安全;有状态 Bean 会出现线程安全问题) |
安全(每次都是新实例,不存在共享状态) |
|
性能 |
高(无需频繁创建/销毁实例,复用性强) |
低(频繁创建/销毁实例,消耗资源) |
|
销毁管理 |
容器负责销毁(容器关闭时) |
容器不负责销毁,由 GC 回收(开发者可手动销毁) |
|
使用场景 |
Service、Dao、Controller、工具类等无状态组件 |
有状态组件(如:封装请求参数的 Bean、线程不安全的工具类) |
6.3 代码
java
// 单例(默认,可省略 @Scope 注解)
@Scope("singleton")
@Component
public class SingletonBean {}
// 多例
@Scope("prototype")
@Component
public class ProtoBean {}
// request 作用域(Spring MVC 环境)
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class RequestBean {}
注意:request/session/application 作用域在非 Web 环境下无法使用,否则会报错;proxyMode = ScopedProxyMode.TARGET_CLASS 表示生成 CGLIB 代理,避免出现作用域不匹配问题。
6.4 生产踩坑
踩坑6:将有状态的 Bean(如包含成员变量且会修改)设置为 singleton 作用域,导致多线程并发时数据错乱。 案例:一个 singleton 的 OrderService 中,有一个成员变量 orderId,多线程调用时,线程 A 修改 orderId 后,线程 B 读取到的是修改后的值,导致业务异常。 解决方案:将有状态 Bean 改为 prototype 作用域;或避免使用成员变量,改用局部变量/ThreadLocal 存储线程私有数据。
踩坑7:在 singleton Bean 中注入 prototype Bean,导致 prototype Bean 变成“伪单例”(仅注入一次,后续调用都是同一个实例)。 解决方案:使用 ObjectFactory<T> 或 @Lookup 注解,每次获取 prototype Bean 时都新建实例。
java
@Service
public class SingletonService {
// 方案1:使用 ObjectFactory 获取 prototype Bean
@Autowired
private ObjectFactory<ProtoBean> protoBeanFactory;
public void doSomething() {
// 每次调用都获取新的 prototype 实例
ProtoBean protoBean = protoBeanFactory.getObject();
}
// 方案2:使用 @Lookup 注解(抽象方法,Spring 自动生成实现)
@Lookup
public ProtoBean getProtoBean() {
return null;
}
}
6.5 面试追问
-
singleton Bean 为什么线程不安全?如何保证 singleton Bean 的线程安全?(答:因为 singleton Bean 是共享实例,多线程同时修改其成员变量会出现并发问题;解决方案:1. 避免使用成员变量,用局部变量;2. 使用 ThreadLocal 存储线程私有数据;3. 对共享资源加锁(synchronized);4. 将有状态 Bean 改为 prototype)
-
@Lookup 注解的作用是什么?原理是什么?(答:作用是在 singleton Bean 中获取 prototype Bean 的新实例;原理:Spring 会动态生成该抽象方法的实现,每次调用都会通过 BeanFactory 获取新的 prototype 实例)
-
request 作用域的 Bean,为什么需要设置 proxyMode?(答:因为 singleton Bean 的生命周期比 request 长,直接注入会导致 request Bean 过期,设置 proxyMode 后,注入的是代理对象,每次调用时才会获取当前 request 对应的 Bean 实例)
二、AOP 相关
1. AOP 是什么?核心概念(切面、通知、切点、连接点、目标对象)
1.1 定义
AOP(Aspect-Oriented Programming,面向切面编程):在不修改业务代码的前提下,通过动态代理技术,统一增强横切逻辑(日志、事务、权限、限流、异常处理等),实现“业务逻辑”与“横切逻辑”的解耦。
1.2 核心 5 大概念(面试必背,结合实例理解)
-
连接点(JoinPoint):程序执行过程中可被拦截的点,Spring AOP 中仅支持方法连接点(即所有方法都可能是连接点)。
-
切点(Pointcut):真正要拦截的连接点,通过切点表达式(如 execution、@annotation)定义拦截规则。例如:拦截所有 Service 层的方法。
-
通知(Advice):拦截到切点后执行的增强逻辑,分为 5 种类型(前置、后置、环绕、异常、最终)。
-
切面(Aspect):切点(Pointcut) + 通知(Advice)的组合,是 AOP 的核心,负责定义“拦截哪些方法”和“拦截后做什么”。
-
目标对象(Target):被代理的原始业务对象(如 UserService 实例),AOP 增强的是目标对象的方法。
补充:织入(Weaving)
织入是将切面逻辑融入到目标对象方法中的过程,Spring AOP 采用动态织入(运行时通过动态代理实现),而 AspectJ 采用静态织入(编译期织入)。
1.3 生产踩坑
踩坑8:切点表达式编写错误,导致切面未生效或误拦截无关方法。 案例:想拦截 com.example.service 包下的所有方法,表达式写成 execution(* com.example.service.*(..)),漏写一个“..”,导致只拦截 service 包下的一级方法,子包下的方法未被拦截。 正确表达式:execution(* com.example.service..*(..))(“..”表示包含子包)。
1.4 面试追问
-
AOP 和 OOP 的区别?(答:OOP 是面向对象,关注“对象”和“继承/封装/多态”,解决业务逻辑的纵向复用;AOP 是面向切面,关注“横切逻辑”,解决横切逻辑的横向复用,弥补 OOP 的不足)
-
Spring AOP 和 AspectJ 的区别?(答:1. 实现方式:Spring AOP 是动态代理,AspectJ 是静态织入;2. 功能范围:Spring AOP 仅支持方法级拦截,AspectJ 支持方法、字段、构造器等多种拦截;3. 性能:AspectJ 静态织入性能更高,Spring AOP 动态代理性能稍低;4. 易用性:Spring AOP 集成在 Spring 中,无需额外编译,AspectJ 需要单独编译)
2. Spring AOP 实现原理?JDK 动态代理 vs CGLIB
2.1 底层原理
Spring AOP 的底层核心是动态代理,Spring 会根据目标对象是否有接口,自动选择代理方式:
-
目标对象有接口:使用 JDK 动态代理(默认),生成接口的代理对象,代理对象实现目标接口,并重写目标方法,植入增强逻辑。
-
目标对象无接口:使用 CGLIB 动态代理,生成目标对象的子类,重写目标方法,植入增强逻辑。
补充:Spring Boot 2.x+ 中,即使目标对象有接口,也可通过配置 spring.aop.proxy-target-class=true,强制使用 CGLIB 代理。
2.2 JDK 动态代理 vs CGLIB(高频对比)
|
代理方式 |
实现原理 |
目标类要求 |
性能(JDK8+) |
优点 |
缺点 |
|---|---|---|---|---|---|
|
JDK 动态代理 |
实现 InvocationHandler 接口,通过 Proxy.newProxyInstance() 生成代理对象 |
必须实现至少一个接口 |
高(生成代理快,执行效率高) |
原生 JDK 支持,无需依赖第三方包;无侵入性(不修改目标类) |
仅能代理接口方法,无法代理类方法 |
|
CGLIB |
基于 ASM 字节码框架,生成目标类的子类,重写目标方法 |
不能被 final 修饰(子类无法继承);方法不能被 final 修饰(无法重写) |
稍低(生成代理慢,执行效率略低于 JDK 代理) |
无需目标类实现接口,可代理任意类方法 |
依赖第三方包(Spring 已集成);有侵入性(生成子类);无法代理 final 方法 |
2.3 核心代码演示(JDK 动态代理)
java
// 1. 定义接口
public interface UserService {
void addUser();
}
// 2. 目标对象(实现接口)
public class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("业务逻辑:新增用户");
}
}
// 3. JDK 动态代理实现(InvocationHandler)
public class JdkProxy implements InvocationHandler {
// 目标对象
private final Object target;
public JdkProxy(Object target) {
this.target = target;
}
// 代理方法:拦截目标方法,植入增强逻辑
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置增强(日志)
System.out.println("前置增强:记录方法调用日志");
// 执行目标方法
Object result = method.invoke(target, args);
// 后置增强(日志)
System.out.println("后置增强:记录方法执行完成日志");
return result;
}
// 生成代理对象
public Object getProxy() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
}
// 4. 测试
public class TestProxy {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
JdkProxy proxy = new JdkProxy(target);
UserService proxyInstance = (UserService) proxy.getProxy();
proxyInstance.addUser(); // 执行代理方法,触发增强逻辑
}
}
2.4 生产踩坑
踩坑9:目标方法被 final 修饰,导致 CGLIB 代理失效,切面逻辑未执行。 原因:CGLIB 是通过继承目标类重写方法实现代理,final 方法无法被重写,因此无法植入增强逻辑。 解决方案:移除目标方法的 final 修饰符;若必须用 final,改用 JDK 动态代理(需目标类实现接口)。
2.5 面试追问
-
JDK 动态代理为什么只能代理接口?(答:JDK 动态代理生成的代理类,默认继承了 Proxy 类,而 Java 不支持多继承,因此无法继承目标类,只能实现目标接口,从而代理接口方法)
-
Spring AOP 中,如何强制使用 CGLIB 代理?(答:1. 配置文件中设置 <aop:aspectj-autoproxy proxy-target-class="true"/>;2. Spring Boot 中,配置 spring.aop.proxy-target-class=true;3. 目标类没有实现任何接口)
-
动态代理的核心思想是什么?(答:不修改目标对象的代码,通过生成代理对象,拦截目标方法的调用,植入增强逻辑,实现解耦)
3. 通知类型有哪些?@Before / @After / @Around 等执行顺序
3.1 5 种通知类型(按执行顺序排序)
-
@Around(环绕通知):最强通知,包围目标方法,可在目标方法执行前、执行后、异常时、最终执行自定义逻辑,还可控制目标方法是否执行(通过 ProceedingJoinPoint.proceed())。
-
@Before(前置通知):目标方法执行前执行,无法阻止目标方法执行(除非抛出异常)。
-
@AfterReturning(返回后通知):目标方法正常执行完成后执行,可获取目标方法的返回值,异常时不执行。
-
@AfterThrowing(异常后通知):目标方法执行抛出异常时执行,可获取异常信息,正常执行时不执行。
-
@After(最终通知):目标方法执行完成后(无论正常还是异常)都执行,类似于 try-catch-finally 中的 finally。
3.2 标准执行顺序(无异常情况)
环绕开始 → @Before → 执行业务方法 → @AfterReturning → @After → 环绕结束
3.4 异常情况执行顺序
环绕开始 → @Before → 执行业务方法(抛出异常) → @AfterThrowing → @After → 环绕异常处理(若有)
3.5 AOP 代码实现(日志切面,实战常用)
@Aspect // 标记为切面类
@Component // 交给 Spring 管理
public class LogAspect {
// 切点:拦截 com.example.service 包及其子包下的所有方法
@Pointcut("execution(* com.example.service..*.*(..))")
public void logPointcut() {} // 切点签名,无实际逻辑
// 1. 环绕通知(最常用,可控制目标方法执行)
@Around("logPointcut()")
public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 环绕开始:记录请求信息
String methodName = joinPoint.getSignature().getName(); // 获取方法名
Object[] args = joinPoint.getArgs(); // 获取方法参数
System.out.println("环绕开始:方法 " + methodName + " 开始执行,参数:" + Arrays.toString(args));
try {
// 执行目标方法
Object result = joinPoint.proceed();
// 环绕结束:记录返回值
System.out.println("环绕结束:方法 " + methodName + " 执行完成,返回值:" + result);
return result;
} catch (Throwable e) {
// 环绕异常处理:记录异常信息
System.out.println("环绕异常:方法 " + methodName + " 执行异常,异常信息:" + e.getMessage());
throw e; // 抛出异常,不影响原有业务逻辑的异常处理
}
}
// 2. 前置通知
@Before("logPointcut()")
public void beforeLog(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("前置通知:方法 " + methodName + " 即将执行");
}
// 3. 返回后通知(获取返回值)
@AfterReturning(value = "logPointcut()", returning = "result")
public void afterReturningLog(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("返回后通知:方法 " + methodName + " 执行完成,返回值:" + result);
}
// 4. 异常后通知(获取异常)
@AfterThrowing(value = "logPointcut()", throwing = "e")
public void afterThrowingLog(JoinPoint joinPoint, Throwable e) {
String methodName = joinPoint.getSignature().getName();
System.out.println("异常后通知:方法 " + methodName + " 执行异常,异常信息:" + e.getMessage());
}
// 5. 最终通知
@After("logPointcut()")
public void afterLog(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("最终通知:方法 " + methodName + " 执行结束(无论是否异常)");
}
}
3.6 生产踩坑
踩坑10:环绕通知中忘记调用 joinPoint.proceed(),导致目标方法未执行,业务逻辑中断。 解决方案:环绕通知中必须调用 joinPoint.proceed(),才能执行目标方法;若需阻止目标方法执行,可抛出异常或不调用 proceed()。
踩坑11:@AfterReturning 和 @AfterThrowing 同时配置,误以为两者会互斥执行,但实际若目标方法异常,@AfterReturning 不执行,@AfterThrowing 执行,@After 始终执行,无需额外判断。
3.7 面试追问
-
@Around 通知和其他通知的区别?(答:1. @Around 可控制目标方法是否执行,其他通知不能;2. @Around 可获取目标方法的返回值和异常,其他通知只能获取其中一种;3. @Around 执行顺序最早,结束顺序最晚,包围整个目标方法)
-
JoinPoint 和 ProceedingJoinPoint 的区别?(答:ProceedingJoinPoint 继承自 JoinPoint,仅在 @Around 通知中可用,多了 proceed() 方法,用于执行目标方法;JoinPoint 可在其他所有通知中使用,用于获取方法信息、参数等)
-
如何在切面中获取请求参数、请求地址等 Web 相关信息?(答:通过 RequestContextHolder 获取当前请求的 HttpServletRequest 对象,进而获取请求信息)
4. Spring AOP 为什么不能拦截 static/private 方法?
4.1 根本原因(面试必背)
Spring AOP 基于动态代理实现,而动态代理的核心是“重写目标方法”,static/private 方法无法被重写,因此无法拦截:
-
private 方法:访问权限为私有,动态代理类(JDK 代理/ CGLIB 代理)无法访问和重写目标类的 private 方法,因此无法植入增强逻辑。
-
static 方法:静态方法属于类,不属于实例,动态代理是针对实例的代理(生成实例的代理对象),无法代理静态方法;且 static 方法无法被重写(子类可继承,但不能重写,只能隐藏)。
补充说明
即使使用 AspectJ 静态织入,也只能拦截 static 方法(通过编译期织入),但无法拦截 private 方法;Spring AOP 不支持 AspectJ 的静态织入(默认动态代理),因此 static/private 方法都无法拦截。
4.2 生产踩坑
踩坑12:将需要拦截的日志、权限逻辑写在 static 方法中,配置切面后发现未生效,排查后才知道 Spring AOP 无法拦截 static 方法。 解决方案:将 static 方法改为实例方法(public);若必须用 static 方法,改用 AspectJ 静态织入,或手动在 static 方法中调用增强逻辑。
4.3 面试追问
-
除了 static/private 方法,Spring AOP 还不能拦截哪些方法?(答:final 方法、构造方法、静态代码块;final 方法无法被重写,构造方法和静态代码块在实例化/类加载时执行,动态代理无法拦截)
-
如何实现对 static 方法的拦截?(答:1. 改用 AspectJ 静态织入,在编译期将切面逻辑织入目标类;2. 不使用 static 方法,改为 public 实例方法;3. 手动在 static 方法中调用增强逻辑,避免依赖 AOP)
三、事务管理(高频中的高频,生产踩坑最多)
1. Spring 事务管理方式有哪两种?
(1)、核心答案(面试直接说)
Spring事务管理分为「编程式事务」和「声明式事务」,二者底层均依赖Spring统一事务抽象层 PlatformTransactionManager,核心区别在于事务控制的载体、粒度和侵入性。
(2)、详细解析
1. 编程式事务
定义:通过代码手动控制事务的开启、提交、回滚,完全由开发人员主导事务流程。
手动通过代码开启、提交、回滚事务(如 TransactionTemplate、PlatformTransactionManager),灵活性高,但代码侵入性强,开发效率低,仅适用于复杂事务场景(如多事务嵌套、动态控制事务)。
核心API:
-
PlatformTransactionManager:事务管理器核心接口,负责事务的创建、提交、回滚 -
TransactionTemplate:Spring提供的简化版编程式事务工具,减少冗余代码
优点:事务粒度极细,可精确到代码块级别;复杂流程、动态条件(如根据业务结果决定是否回滚)下可控性强。
缺点:代码侵入性强,事务代码与业务代码耦合;冗余代码多,维护成本高。
2. 声明式事务
定义:基于Spring AOP动态代理实现,通过 @Transactional 注解或XML配置,自动完成事务的开启、提交、回滚,无需手动编写事务控制代码。
通过注解(@Transactional)或 XML 配置实现,基于 AOP 动态代理,无需手动编写事务代码,开发效率高,解耦业务逻辑和事务管理。 核心:Spring 自动拦截 @Transactional 注解的方法,开启事务、执行目标方法、提交/回滚事务。
本质:AOP动态代理 + TransactionInterceptor(事务拦截器),拦截目标方法并自动处理事务。
优点:无侵入性,业务代码纯净;配置简单、易维护,支持全局统一管理;企业开发99%场景适用。
缺点:事务最小粒度为方法级别,无法实现代码块级别的细粒度控制;复杂场景(如动态事务)易出现失效问题。
(3)、完整代码实现
1. 编程式事务(TransactionTemplate 实现)
@Service
public class UserService {
// 注入事务模板
@Autowired
private TransactionTemplate transactionTemplate;
// 注入持久层接口
@Autowired
private UserMapper userMapper;
/**
* 编程式事务示例:更新用户信息
*/
public void updateUserProgrammatic(Long userId) {
// 执行事务逻辑,返回执行结果(true=成功,false=失败)
Boolean executeResult = transactionTemplate.execute(status -> {
try {
// 核心业务操作
User user = new User();
user.setId(userId);
user.setUserName("updated");
userMapper.updateById(user);
// 模拟异常(测试回滚,实际可删除)
// int i = 1 / 0;
return true; // 执行成功,自动提交事务
} catch (Exception e) {
// 手动标记事务回滚(异常时必须执行)
status.setRollbackOnly();
e.printStackTrace();
return false; // 执行失败,触发回滚
}
});
}
}
2. 声明式事务(@Transactional 实现)
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 声明式事务示例:更新用户信息
* rollbackFor = Exception.class:指定所有异常都回滚(避免受检异常不回滚)
*/
@Transactional(rollbackFor = Exception.class)
public void updateUserDeclare(Long userId) {
User user = new User();
user.setId(userId);
user.setUserName("updated");
userMapper.updateById(user);
// 模拟异常(测试回滚,实际可删除)
// if (true) {
// throw new RuntimeException("更新失败,触发回滚");
// }
}
}
(4)、生产踩坑案例
踩坑场景1:混合使用声明式事务和编程式事务,导致事务管理混乱,出现部分提交、部分回滚的情况。 案例:在 @Transactional 注解的方法中,手动调用 TransactionTemplate,导致两个事务独立,外层事务回滚时,内层 TransactionTemplate 的事务未回滚。 解决方案:统一使用一种事务管理方式,优先声明式事务;若需复杂事务控制,全部使用编程式事务。
踩坑场景2:老项目混合使用编程式和声明式事务,出现事务未提交、数据库连接泄漏,导致应用卡顿、数据不一致。
排查思路:
-
查看应用日志,确认是否存在多个事务管理器(如同时配置了编程式事务管理器和声明式事务管理器);
-
检查编程式事务代码,确认是否手动获取了数据库连接,且未在异常时释放;
-
检查嵌套事务场景,确认是否存在事务未正常归还连接的情况;
-
统一事务管理器,避免混合使用,优先使用声明式事务,复杂场景单独使用编程式事务。
(5)、高频面试追问(必背)
Q1:编程式事务和声明式事务底层是否共用一套机制?
A1:是。二者底层都依赖 PlatformTransactionManager(事务管理器)和 TransactionSynchronizationManager(线程绑定资源),只是事务触发方式不同:编程式手动调用API,声明式通过AOP拦截自动触发。
Q2:什么时候优先使用编程式事务?
A2:3种场景:① 事务粒度小于方法级别(如循环内部分片段需要事务);② 动态条件事务(如根据业务逻辑结果决定是否回滚);③ 跨多个方法的复杂事务流程(无法用声明式事务统一控制)。
2. @Transactional 工作原理?
(1)、核心答案(面试直接说)
@Transactional 的本质是「Spring AOP动态代理 + 事务拦截器 + 线程绑定资源」,通过AOP拦截目标方法,由事务拦截器统一控制事务的开启、提交、回滚,并用ThreadLocal绑定数据库连接,保证事务内连接唯一。
(2)、完整执行流程(面试满分版)
-
容器启动阶段:
-
Spring自动配置类
TransactionAutoConfiguration生效,初始化事务管理器PlatformTransactionManager; -
Spring扫描所有类上、方法上的
@Transactional注解,标记需要被事务增强的方法; -
根据目标类是否实现接口,选择动态代理方式:实现接口用JDK动态代理,未实现接口用CGLIB代理,生成代理对象。
-
-
方法调用阶段:
-
外部调用目标方法时,实际调用的是代理对象的方法,而非原始对象;
-
代理对象触发
TransactionInterceptor(事务拦截器)的invoke()方法,开始事务控制。
-
-
获取事务阶段:
-
事务拦截器调用
PlatformTransactionManager#getTransaction()方法; -
根据
@Transactional配置的传播机制,判断当前是否需要新建事务、加入已有事务或挂起事务。
-
-
绑定线程资源阶段:
-
通过
TransactionSynchronizationManager类,使用ThreadLocal将数据库连接(Connection)与当前线程绑定; -
保证整个事务执行过程中,所有数据库操作都使用同一个连接,确保事务原子性。
-
-
执行目标方法阶段:调用原始对象的目标方法,执行核心业务逻辑。
-
异常判断阶段:
-
如果方法抛出
RuntimeException或Error,事务拦截器触发事务回滚; -
如果抛出受检异常(如IOException、SQLException),默认不回滚(需通过
rollbackFor配置指定回滚异常)。
-
-
事务提交/回滚阶段:
-
方法正常执行无异常:事务拦截器调用
PlatformTransactionManager#commit()提交事务; -
方法抛出异常:调用
PlatformTransactionManager#rollback()回滚事务。
-
-
资源清理阶段:解除ThreadLocal与数据库连接的绑定,将连接归还到连接池,释放资源。
总结:
-
Spring 启动时,通过 AnnotationTransactionAttributeSource 解析所有带有 @Transactional 注解的方法,获取事务属性(传播机制、隔离级别、回滚规则等)。
-
通过 AOP 动态代理,为目标对象生成代理对象,代理对象会拦截 @Transactional 注解的方法。
-
方法执行前:代理对象调用 PlatformTransactionManager 的 getTransaction() 方法,根据事务属性开启事务,生成 TransactionStatus 对象(事务状态)。
-
执行目标方法:若目标方法正常执行完成,代理对象调用 PlatformTransactionManager 的 commit() 方法,提交事务。
-
若目标方法抛出异常,代理对象根据 @Transactional 的 rollbackFor 属性,判断是否需要回滚;若需要,调用 rollback() 方法回滚事务;否则,提交事务。
(3)、核心源码类(资深必掌握)
-
TransactionInterceptor:事务切面核心拦截器,负责拦截目标方法,执行事务的开启、提交、回滚逻辑; -
AbstractPlatformTransactionManager:事务管理模板类,定义了事务操作的统一流程(模板方法模式); -
DataSourceTransactionManager:最常用的事务管理器,基于数据源实现事务管理,适配关系型数据库; -
TransactionSynchronizationManager:线程绑定资源管理类,通过ThreadLocal存储当前线程的事务信息(连接、事务状态等)。
(4)、代码示例(正常使用@Transactional)
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* @Transactional 完整配置示例
* propagation:传播机制(默认REQUIRED)
* isolation:隔离级别(默认数据库默认)
* rollbackFor:指定回滚的异常类型
* timeout:事务超时时间(单位:秒)
*/
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
rollbackFor = Exception.class,
timeout = 30
)
public void createOrder(Order order) {
// 核心业务:创建订单
orderMapper.insert(order);
// 关联操作:扣减库存(假设调用库存服务)
// stockService.deductStock(order.getProductId(), order.getQuantity());
}
}
(5)、生产踩坑案例
踩坑场景:开发人员在 @Transactional 注解的方法内,用try-catch捕获了所有异常,但未重新抛出,导致事务不回滚,出现脏数据(如订单创建成功,库存扣减失败,但事务未回滚,订单数据残留)。
错误代码示例:
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
try {
orderMapper.insert(order);
int i = 1 / 0; // 模拟异常
} catch (Exception e) {
// 只捕获异常,未抛出,Spring无法感知异常
e.printStackTrace();
}
}
排查思路:
-
查看业务日志,确认方法内是否有异常抛出,以及异常是否被捕获;
-
打开Spring事务日志(配置logging.level.org.springframework.transaction=DEBUG),查看事务是否触发回滚;
-
修改代码:要么删除try-catch,让异常自动抛出;要么在catch块中重新抛出异常(throw new RuntimeException(e));
-
确认
rollbackFor配置正确,覆盖需要回滚的异常类型。
修正代码:
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
try {
orderMapper.insert(order);
int i = 1 / 0; // 模拟异常
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e); // 重新抛出异常,触发事务回滚
}
}
(6)、高频面试追问(必背)
Q1:事务连接存在哪里?为什么多线程调用时事务会失效?
A1:事务连接存在 TransactionSynchronizationManager 的ThreadLocal中。ThreadLocal的特点是线程隔离,每个线程有独立的存储空间,因此多线程调用时,新线程无法获取到原线程绑定的事务连接,事务自然失效。
Q2:为什么 @Transactional 默认只回滚 RuntimeException 和 Error?
A2:这是Spring的设计哲学:① 受检异常(如IOException)是可预期、可恢复的业务异常(开发人员在编码时必须处理),通常代表业务逻辑正常的分支,不应触发事务回滚;② 运行时异常(如NullPointerException)和Error是不可预期的系统异常,代表系统出现故障,必须触发事务回滚,保证数据一致性。
Q3:@Transactional 注解加在类上和加在方法上,有什么区别?
A3:① 加在类上:该类中所有public方法都会被事务增强;② 加在方法上:只有该方法会被事务增强;③ 优先级:方法上的注解配置会覆盖类上的配置(如类上配置传播机制为REQUIRED,方法上配置为REQUIRES_NEW,则以方法为准)。
3. 事务传播机制有哪些?常用的 REQUIRED、REQUIRES_NEW 区别
(1)、核心答案(面试直接说)
Spring定义了7种事务传播机制,核心作用是:当一个事务方法调用另一个事务方法时,决定新方法的事务如何与原有事务进行关联(新建、加入、挂起等)。其中最常用的是 REQUIRED(默认)和 REQUIRES_NEW,二者核心区别是:REQUIRED 共用原有事务,REQUIRES_NEW 新建独立事务,互不影响。
(2)、7种事务传播机制(完整枚举)
public enum Propagation {
// 1. REQUIRED(默认):有事务则加入,无则新建
REQUIRED,
// 2. SUPPORTS:有事务则加入,无则以非事务方式执行
SUPPORTS,
// 3. MANDATORY:必须在已有事务中执行,否则抛异常
MANDATORY,
// 4. REQUIRES_NEW:始终新建独立事务,挂起原有事务
REQUIRES_NEW,
// 5. NOT_SUPPORTED:以非事务方式执行,挂起原有事务
NOT_SUPPORTED,
// 6. NEVER:以非事务方式执行,有事务则抛异常
NEVER,
// 7. NESTED:嵌套事务(保存点机制),依赖原有事务
NESTED
}
(3)、重点传播机制详解(3个必掌握)
1. REQUIRED(默认传播机制)
核心逻辑:如果当前线程已有事务,则当前方法加入该事务,共用同一个事务和数据库连接;如果当前线程没有事务,则新建一个事务。
特点:所有加入同一事务的方法,要么全部成功提交,要么全部失败回滚,相互影响。
2. REQUIRES_NEW(常用独立事务)
核心逻辑:无论当前线程是否有事务,都会新建一个独立的事务,同时将原有事务挂起;新事务执行完成后,再恢复原有事务的执行。
特点:新事务与原有事务完全隔离,使用独立的数据库连接;新事务的提交/回滚与原有事务无关,互不影响。
3. NESTED(嵌套事务)
核心逻辑:基于原有事务创建嵌套事务,使用保存点(savepoint)机制;嵌套事务可以独立回滚,但外层事务回滚会导致内层嵌套事务一起回滚。
特点:共用同一个数据库连接,与外层事务绑定;适合需要部分回滚的场景(如批量操作,部分失败回滚,部分成功提交)。
(4)、REQUIRED 与 REQUIRES_NEW 核心区别(面试必背对比表)
|
对比维度 |
REQUIRED(默认) |
REQUIRES_NEW |
|---|---|---|
|
事务数量 |
1个(共用原有事务) |
2个(新建独立事务 + 原有事务) |
|
数据库连接 |
共用1个连接 |
占用2个独立连接 |
|
内层异常影响 |
内层异常会导致外层事务一起回滚 |
内层异常仅回滚自身,外层事务可选择不回滚 |
|
外层异常影响 |
外层异常会导致内层事务一起回滚 |
外层异常不影响内层事务(内层已提交) |
|
锁竞争 |
低(共用连接,锁冲突少) |
高(多连接,易出现锁等待、连接耗尽) |
|
实现机制 |
加入原有事务,复用连接和事务状态 |
挂起原有事务,新建事务,独立管理 |
|
适用场景 |
常规业务(如订单创建+库存扣减,需原子性) |
独立子任务(如日志记录、消息发送,失败不影响主流程) |
(5)、代码示例(REQUIRED 与 REQUIRES_NEW 对比)
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private LogService logService;
/**
* 主方法:传播机制 REQUIRED(默认)
*/
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void createOrder(Order order) {
// 主业务:创建订单(REQUIRED 事务)
orderMapper.insert(order);
// 调用日志服务(日志方法为 REQUIRES_NEW)
logService.addOrderLog(order.getId(), "订单创建成功");
// 模拟异常(测试回滚影响)
// int i = 1 / 0;
}
}
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
/**
* 日志方法:传播机制 REQUIRES_NEW(独立事务)
*/
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void addOrderLog(Long orderId, String content) {
Log log = new Log();
log.setOrderId(orderId);
log.setContent(content);
logMapper.insert(log);
}
}
测试结果:
-
如果 createOrder 方法抛出异常:订单事务回滚(订单不插入),但 logService 的事务已独立提交(日志插入成功);
-
如果 logService 方法抛出异常:日志事务回滚(日志不插入),createOrder 方法也会回滚(订单不插入)。
(6)、生产踩坑案例
踩坑场景:高并发下单接口中,开发人员给所有方法(包括日志、通知等非核心方法)都配置了 REQUIRES_NEW 传播机制,导致数据库连接池耗尽,应用假死,接口响应超时。
排查思路:
-
通过监控工具(如Prometheus、Druid)查看数据库连接池活跃连接数,发现连接数瞬间拉满;
-
全局搜索代码中 REQUIRES_NEW 的使用场景,发现大量非核心方法(日志、通知)滥用 REQUIRES_NEW;
-
分析业务场景:日志、通知等非核心方法,无需独立事务,改为 REQUIRED 传播机制;
-
优化:非核心的日志、通知操作,可改为异步执行(如用@Async),进一步降低连接占用。
(7)、高频面试追问(必背)
Q1:NESTED 和 REQUIRES_NEW 的区别是什么?
A1:核心3点区别:① 事务独立性:NESTED 是嵌套事务,依赖外层事务,共用一个连接;REQUIRES_NEW 是完全独立事务,使用新连接;② 回滚影响:外层事务回滚,NESTED 内层事务也会回滚;REQUIRES_NEW 内层事务不受影响;③ 实现机制:NESTED 基于保存点(savepoint),REQUIRES_NEW 基于事务挂起+新建。
Q2:为什么 REQUIRES_NEW 容易导致数据库连接池耗尽?
A2:因为 REQUIRES_NEW 会为每个方法新建独立事务,占用独立的数据库连接;高并发场景下,大量方法同时调用,会瞬间占用多个连接,超出连接池最大容量,导致连接耗尽,应用无法获取新连接,出现假死。
Q3:什么场景下必须用 REQUIRES_NEW?
A3:核心场景:非核心子任务,失败不影响主流程。例如:订单创建成功后,记录操作日志、发送通知,即使日志/通知失败,也不能导致订单事务回滚,此时日志/通知方法需配置 REQUIRES_NEW。
4. 事务隔离级别?脏读、不可重复读、幻读
(1)、核心答案(面试直接说)
事务隔离级别是为了解决事务并发时的3大问题(脏读、不可重复读、幻读),Spring支持5种隔离级别,底层基于数据库的锁机制和MVCC(多版本并发控制)实现。其中MySQL InnoDB默认隔离级别是 REPEATABLE_READ,Oracle默认是 READ_COMMITTED。
(2)、Spring 支持的5种隔离级别(完整枚举)
public enum Isolation {
// 1. DEFAULT:使用数据库默认的隔离级别(推荐)
DEFAULT,
// 2. READ_UNCOMMITTED:读未提交(最低级别,允许脏读)
READ_UNCOMMITTED,
// 3. READ_COMMITTED:读已提交(解决脏读,允许不可重复读、幻读)
READ_COMMITTED,
// 4. REPEATABLE_READ:可重复读(解决脏读、不可重复读,部分解决幻读)
REPEATABLE_READ,
// 5. SERIALIZABLE:串行化(最高级别,解决所有并发问题,性能最低)
SERIALIZABLE
}
(3)、事务并发3大问题(必背定义+原理)
1. 脏读(Dirty Read)
定义:一个事务(A)读到了另一个事务(B)未提交的数据。如果事务B后续回滚,那么事务A读到的数据就是无效的“脏数据”,会导致业务逻辑错乱。
示例:
-
事务B:更新用户余额,从100元改为200元,但未提交;
-
事务A:查询该用户余额,读到200元(未提交数据);
-
事务B:因异常回滚,余额恢复为100元;
-
事务A:基于读到的200元进行后续操作(如转账),导致数据错误。
2. 不可重复读(Non-Repeatable Read)
定义:同一事务(A)内,多次查询同一行数据,结果不一致。通常由其他事务(B)对该数据进行 update 操作导致。
示例:
-
事务A:第一次查询用户余额,为100元;
-
事务B:更新该用户余额为200元,并提交;
-
事务A:第二次查询该用户余额,变为200元,与第一次查询结果不一致。
3. 幻读(Phantom Read)
定义:同一事务(A)内,多次查询同一条件的数据,行数不一致。通常由其他事务(B)对该条件下的数据进行 insert 或 delete 操作导致。
示例:
-
事务A:查询年龄>18的用户,得到3条数据;
-
事务B:插入1条年龄>18的用户,并提交;
-
事务A:再次查询年龄>18的用户,得到4条数据,出现“幻影”数据。
(4)、隔离级别与并发问题对应关系(面试必背表)
|
隔离级别 |
脏读 |
不可重复读 |
幻读 |
性能 |
适用场景 |
|---|---|---|---|---|---|
|
READ_UNCOMMITTED |
允许 |
允许 |
允许 |
最高 |
极少使用(如临时统计,不要求数据准确) |
|
READ_COMMITTED |
禁止 |
允许 |
允许 |
较高 |
大多数业务(如电商、社交,允许不可重复读) |
|
REPEATABLE_READ |
禁止 |
禁止 |
部分解决 |
中等 |
金融、支付、库存(要求数据一致性高) |
|
SERIALIZABLE |
禁止 |
禁止 |
禁止 |
最低 |
极少使用(如银行核心交易,不允许任何并发问题) |
(5)、关键补充:MySQL 如何解决幻读?
MySQL InnoDB 默认隔离级别是 REPEATABLE_READ,通过以下两种机制解决幻读:
-
MVCC(多版本并发控制):普通查询(快照读)时,读取的是数据的历史版本,不会读到其他事务新增的“幻影”数据;
-
Next-Key Lock(临键锁):写操作(update、delete、insert)时,会对数据行及其间隙加锁,防止其他事务在间隙中插入数据,从根本上杜绝幻读。
(6)、代码配置(隔离级别使用示例)
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 配置隔离级别为 READ_COMMITTED(适配大多数业务)
* rollbackFor = Exception.class:确保所有异常都回滚
*/
@Transactional(
isolation = Isolation.READ_COMMITTED,
rollbackFor = Exception.class
)
public User getUserById(Long userId) {
return userMapper.selectById(userId);
}
/**
* 金融场景:配置隔离级别为 REPEATABLE_READ
*/
@Transactional(
isolation = Isolation.REPEATABLE_READ,
rollbackFor = Exception.class
)
public void deductBalance(Long userId, BigDecimal amount) {
User user = userMapper.selectById(userId);
if (user.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
user.setBalance(user.getBalance().subtract(amount));
userMapper.updateById(user);
}
}
(7)、生产踩坑案例
踩坑场景:电商库存扣减场景,使用 MySQL 默认隔离级别 REPEATABLE_READ,高并发下出现幻读,导致库存超卖(如库存只剩1件,两个用户同时下单,都查询到库存为1,都扣减成功,最终库存为-1)。
排查思路:
-
查看库存扣减代码,发现未加行锁,仅靠事务隔离级别控制并发;
-
分析并发流程:两个事务同时查询库存(快照读,都得到1),同时扣减,导致超卖;
-
解决方案:
-
方案1:业务允许时,将隔离级别改为 READ_COMMITTED,配合乐观锁(如版本号);
-
方案2:在查询库存时加行锁(select ... for update),强制使用当前读,防止幻读;
-
方案3:使用分布式锁(如Redisson),控制库存扣减的并发执行。
-
修正代码(加行锁):
@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
public void deductBalance(Long userId, BigDecimal amount) {
// 加行锁,强制当前读,防止幻读
User user = userMapper.selectByIdForUpdate(userId);
if (user.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
user.setBalance(user.getBalance().subtract(amount));
userMapper.updateById(user);
}
(8)、高频面试追问(必背)
Q1:Spring 事务隔离级别和数据库隔离级别,哪个优先级更高?
A1:Spring 事务隔离级别优先级更高。Spring 会在获取数据库连接后,通过执行 SQL 语句 set transaction isolation level XXX,覆盖数据库的默认隔离级别,确保事务隔离级别符合配置。
Q2:READ_COMMITTED 和 REPEATABLE_READ 在实际项目中如何选择?
A2:① 追求性能、允许不可重复读:选择 READ_COMMITTED(如电商商品详情、用户列表等非核心数据查询);② 追求数据一致性、不允许不可重复读:选择 REPEATABLE_READ(如金融转账、库存扣减、订单支付等核心场景)。
Q3:SERIALIZABLE 隔离级别为什么性能低?
A3:因为 SERIALIZABLE 会将所有事务串行执行,禁止并发操作,相当于单线程执行,会导致大量锁等待、超时,严重影响高并发场景的性能,因此仅适用于并发量极低、数据一致性要求极高的场景。
5. @Transactional 失效场景有哪些?(非常高频)
(1)、核心答案(面试直接说)
@Transactional 失效的核心原因是:事务方法未被 Spring 动态代理拦截,导致事务拦截器无法执行,进而无法开启、提交、回滚事务。资深工程师需掌握至少8种失效场景,涵盖权限、代理、异常、配置等维度。
(2)、10种失效场景(必背,含原理+示例)
1. 方法不是 public 修饰
原理:Spring AOP 动态代理(JDK 动态代理、CGLIB)仅能代理 public 方法,private、protected、default 修饰的方法无法被代理,事务拦截器无法拦截,事务失效。
失效代码:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// private 方法,事务失效
@Transactional(rollbackFor = Exception.class)
private void updateUser(Long userId) {
userMapper.updateById(userId);
throw new RuntimeException(); // 不会回滚
}
}
2. 同类内部 this 调用
原理:同类内部方法调用时,使用的是 this 关键字,this 指向原始对象,而非 Spring 生成的代理对象,因此事务拦截器无法拦截,事务失效。
失效代码:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 外部方法(无事务)
public void outerMethod() {
// this 调用,指向原始对象,事务失效
this.innerMethod();
}
// 内部事务方法
@Transactional(rollbackFor = Exception.class)
public void innerMethod() {
userMapper.updateById(1L);
throw new RuntimeException(); // 不会回滚
}
}
3. try-catch 吞掉异常,未抛出
原理:Spring 事务回滚的触发条件是:方法抛出未被捕获的异常(RuntimeException/Error,或配置的 rollbackFor 异常)。如果用 try-catch 捕获所有异常,且未重新抛出,Spring 无法感知异常,事务不会回滚。
失效代码:
@Transactional(rollbackFor = Exception.class)
public void updateUser(Long userId) {
try {
userMapper.updateById(userId);
int i = 1 / 0; // 模拟异常
} catch (Exception e) {
// 吞掉异常,未抛出,事务不回滚
e.printStackTrace();
}
}
4. 异常类型不匹配(未配置 rollbackFor)
原理:@Transactional 默认只回滚 RuntimeException 和 Error,对于受检异常(如 IOException、SQLException),默认不回滚。如果业务方法抛出受检异常,且未配置rollbackFor = Exception.class,事务失效。
失效代码:
@Transactional
public void updateUser(Long userId) throws IOException {
userMapper.updateById(userId);
throw new IOException("受检异常"); // 默认不回滚,事务失效
}
5. 类未被 Spring 管理
原理:@Transactional 依赖 Spring 容器的 bean 管理,只有被 Spring 管理的类(加了 @Service、@Component、@Controller 等注解),才能生成代理对象。如果类未被 Spring 管理,事务失效。
失效代码:
// 未加 @Service,未被 Spring 管理,事务失效
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional(rollbackFor = Exception.class)
public void updateUser(Long userId) {
userMapper.updateById(userId);
}
}
6. 多线程调用
原理:Spring 事务依赖 ThreadLocal 绑定数据库连接,线程隔离。多线程调用时,新线程无法获取到原线程绑定的事务连接,因此新线程中的事务方法无法被事务增强,事务失效。
失效代码:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional(rollbackFor = Exception.class)
public void updateUser(Long userId) {
// 多线程调用内部事务方法,事务失效
new Thread(() -> {
userMapper.updateById(userId);
throw new RuntimeException(); // 不会回滚
}).start();
}
}
7. 数据库引擎不支持事务
原理:事务的实现依赖数据库引擎支持。MySQL 的 MyISAM 引擎不支持事务,InnoDB 引擎支持事务。如果数据库表使用 MyISAM 引擎,即使配置了@Transactional,事务也会失效。
解决方案:将数据库表引擎改为 InnoDB(ALTER TABLE 表名 ENGINE = InnoDB)。
8. 传播机制配置错误
原理:如果将传播机制配置为 NOT_SUPPORTED 或 NEVER,会导致方法以非事务方式执行,事务失效。
失效代码:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 传播机制为 NOT_SUPPORTED,非事务执行,事务失效
@Transactional(propagation = Propagation.NOT_SUPPORTED, rollbackFor = Exception.class)
public void updateUser(Long userId) {
userMapper.updateById(userId);
throw new RuntimeException(); // 不会回滚
}
}
9. 方法被 final / static 修饰
原理:CGLIB 动态代理通过继承目标类生成代理类,final 方法无法被继承重写;static 方法属于类,不属于实例,无法被代理。因此 final / static 修饰的事务方法,无法被代理增强,事务失效。
失效代码:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// final 方法,事务失效
@Transactional(rollbackFor = Exception.class)
public final void updateUser(Long userId) {
userMapper.updateById(userId);
throw new RuntimeException(); // 不会回滚
}
}
10. 未开启事务自动配置
原理:Spring Boot 项目默认开启事务自动配置(@EnableTransactionManagement),但如果是 Spring 纯 XML 项目,或手动关闭了自动配置,未手动开启 @EnableTransactionManagement,事务会失效。
解决方案:在启动类或配置类上添加 @EnableTransactionManagement 注解。
(3)、生产踩坑案例
踩坑场景:电商批量导入用户数据,开发人员写了 private 方法并添加 @Transactional 注解,线上运行时,部分数据导入失败,但事务未回滚,导致数据不一致(部分导入成功,部分失败)。
排查思路:
-
查看方法修饰符:发现是 private 方法,无法被 Spring 代理;
-
查看日志:无事务相关日志(如“Creating new transaction”“Rolling back transaction”),确认事务未被触发;
-
修改方案:将 private 方法改为 public 方法,或通过注入自身代理调用该方法;
-
验证:修改后,异常时事务正常回滚,数据一致性得到保证。
(4)、高频面试追问(必背)
Q1:private 方法加 @Transactional 为什么一定失效?有没有办法让其生效?
A1:① 原因:Spring AOP 动态代理(JDK/CGLIB)仅能代理 public 方法,private 方法无法被代理,事务拦截器无法拦截;② 解决方案:理论上可以用 AspectJ 编译期织入(不依赖动态代理),但企业开发中几乎不用,成本高、维护复杂,推荐直接将 private 方法改为 public 方法。
Q2:如何快速排查 @Transactional 失效问题?
A2:4步排查:① 检查方法是否为 public,是否被 final/static 修饰;② 检查类是否被 Spring 管理(加了 @Service 等注解);③ 检查是否有 try-catch 吞异常,是否配置 rollbackFor;④ 检查是否是同类内部调用、多线程调用,或传播机制配置错误。
Q3:@Transactional(rollbackFor = Exception.class) 为什么要加?
A3:因为默认情况下,@Transactional 只回滚 RuntimeException 和 Error,对于受检异常(如 IOException)不回滚。添加 rollbackFor = Exception.class 后,所有异常(包括受检异常)都会触发事务回滚,避免因异常类型不匹配导致事务失效,是企业开发的最佳实践。
6. 同类方法调用事务不生效为什么?怎么解决?
(1)、核心答案(面试直接说)
同类方法调用事务不生效的核心原因:Spring 声明式事务基于动态代理实现,同类内部调用时,this 关键字指向原始对象,而非 Spring 生成的代理对象,导致事务拦截器无法拦截目标方法,事务无法生效。解决方案有4种,优先使用“注入自身代理”,稳定且易维护。
(2)、底层原理(深入讲解)
结合Spring AOP动态代理的工作机制,彻底搞懂同类调用失效的本质,面试时能讲清底层逻辑,加分项!
-
代理对象生成逻辑:Spring容器启动时,会为带有@Transactional注解的类生成动态代理对象(JDK或CGLIB),代理对象会封装事务拦截器(TransactionInterceptor),负责事务的开启、提交、回滚。
-
this关键字的指向问题:同类内部方法调用时,使用this调用的是当前类的原始对象(target object),而非Spring容器生成的代理对象(proxy object)。原始对象中没有事务拦截器的增强逻辑,因此事务无法被触发。
-
拦截器触发条件:事务拦截器只有在“外部调用代理对象的方法”时才会生效,内部this调用跳过了代理对象,自然无法触发事务拦截器,事务也就失效了。
简单类比:代理对象相当于“事务开关”,外部调用代理对象的方法,开关会自动触发(开启事务);而内部this调用直接绕过开关,直接执行原始方法,事务自然不生效。
(3)、4种解决方案(按推荐度排序,企业实战常用)
方案1:注入自身代理对象(推荐,最稳定、易维护)
核心思路:在当前类中注入自身的代理对象,通过代理对象调用内部事务方法,触发事务拦截器。需配合@EnableAspectJAutoProxy(exposeProxy = true)开启代理暴露。
步骤:
-
在Spring Boot启动类或配置类上添加注解,暴露代理对象:
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露代理对象,关键配置
public class SpringTransactionApplication {
public static void main(String[] args) {
SpringApplication.run(SpringTransactionApplication.class, args);
}
}
-
在当前类中注入自身代理对象,调用内部事务方法:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 注入自身代理对象(注意:不是注入this,是注入代理对象)
@Autowired
private UserService userService;
// 外部无事务方法
public void outerMethod() {
// 关键:通过代理对象调用内部事务方法,触发事务
userService.innerMethod();
}
// 内部事务方法
@Transactional(rollbackFor = Exception.class)
public void innerMethod() {
userMapper.updateById(1L);
throw new RuntimeException("测试回滚"); // 会正常回滚
}
}
优点:代码侵入性低,符合Spring开发规范,稳定可靠,企业实战中最常用。
注意:不要用this调用,必须用注入的代理对象(userService)调用。
方案2:通过AopContext获取当前代理对象(简洁,无需注入)
核心思路:利用Spring提供的AopContext工具类,直接获取当前线程的代理对象,再调用内部事务方法。同样需要开启代理暴露(@EnableAspectJAutoProxy(exposeProxy = true))。
代码示例:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 外部无事务方法
public void outerMethod() {
// 获取当前代理对象,调用内部事务方法
UserService proxy = (UserService) AopContext.currentProxy();
proxy.innerMethod();
}
// 内部事务方法
@Transactional(rollbackFor = Exception.class)
public void innerMethod() {
userMapper.updateById(1L);
throw new RuntimeException("测试回滚"); // 会正常回滚
}
}
优点:无需注入自身,代码更简洁,适合简单场景。
注意:必须开启exposeProxy = true,否则会抛出IllegalStateException异常;不适合多线程场景(AopContext基于ThreadLocal,多线程无法获取当前代理)。
方案3:拆分方法到不同类(彻底解决,解耦)
核心思路:将内部事务方法拆分到另一个Service类中,通过注入该类的对象,实现跨类调用,自然触发代理对象的事务拦截器。
代码示例:
// 拆分后的事务方法所在类
@Service
public class UserTransactionService {
@Autowired
private UserMapper userMapper;
// 事务方法
@Transactional(rollbackFor = Exception.class)
public void innerMethod() {
userMapper.updateById(1L);
throw new RuntimeException("测试回滚"); // 会正常回滚
}
}
// 原类,调用拆分后的事务方法
@Service
public class UserService {
@Autowired
private UserTransactionService transactionService;
// 外部无事务方法
public void outerMethod() {
// 跨类调用,触发事务
transactionService.innerMethod();
}
}
优点:彻底避免同类调用问题,同时实现业务解耦,符合单一职责原则。
缺点:需要新增类,适合业务逻辑复杂、方法可拆分的场景。
方案4:使用AspectJ编译期织入(不推荐,成本高)
核心思路:放弃Spring AOP动态代理,使用AspectJ的编译期织入,直接在编译阶段将事务增强逻辑织入到原始类中,无需代理对象,this调用也能触发事务。
实现步骤:
-
引入AspectJ相关依赖(spring-aspects、aspectjweaver);
-
配置AspectJ织入(如使用@EnableAspectJAutoProxy,或通过XML配置织入器);
-
正常使用@Transactional注解,this调用也会生效。
缺点:配置复杂,需要修改编译流程,企业开发中极少使用,仅适用于特殊场景(如无法修改代码结构、必须使用private方法做事务)。
(4)、生产踩坑案例(实战重点)
踩坑场景:电商订单支付流程中,开发人员在OrderService的payOrder(无事务)方法中,用this调用了doPay(加@Transactional)方法,线上出现支付成功但订单状态未更新,且事务未回滚的问题(因支付接口异常,doPay方法未回滚)。
排查思路:
-
查看代码调用关系:发现是同类内部this调用doPay方法,跳过了代理对象;
-
查看事务日志:无“Creating new transaction”日志,确认事务未触发;
-
修改方案:采用方案1(注入自身代理对象),将this.doPay()改为orderService.doPay();
-
验证:修改后,支付异常时doPay方法正常回滚,订单状态和支付记录保持一致。
(5)、高频面试追问(必背,区分初级/资深)
Q1:为什么注入自身代理对象能解决同类调用事务失效问题?
A1:注入的自身对象是Spring生成的代理对象,而非原始对象;通过代理对象调用内部方法时,会触发代理对象中的事务拦截器,进而开启、控制事务,解决了this指向原始对象导致的拦截器失效问题。
Q2:方案1和方案2的区别是什么?实际开发中优先选哪个?
A2:① 区别:方案1是注入自身代理对象,依赖Spring依赖注入;方案2是通过AopContext获取代理对象,依赖ThreadLocal;② 优先选方案1:方案1更稳定,无需担心多线程场景下AopContext获取不到代理对象的问题,且代码可读性更高。
Q3:拆分方法到不同类,除了解决事务问题,还有什么好处?
A3:主要有2个好处:① 解耦:将事务逻辑与业务逻辑拆分到不同类,符合单一职责原则;② 复用:拆分后的事务方法可被多个类调用,提高代码复用性;③ 便于维护:后续修改事务逻辑时,无需改动原业务类。
Q4:为什么不推荐使用AspectJ编译期织入?
A4:核心原因有3点:① 配置复杂,需要引入额外依赖,修改编译流程;② 维护成本高,后续代码迭代时,织入逻辑可能出现冲突;③ 不符合Spring默认的动态代理机制,团队协作时需要统一技术规范,学习成本高。
(6)、总结(面试速记)
核心原因:this指向原始对象,跳过代理对象,事务拦截器无法触发;
优先方案:注入自身代理对象(开启exposeProxy = true);
备用方案:AopContext获取代理(简单场景)、拆分方法到不同类(解耦场景);
避坑重点:避免同类内部this调用事务方法,优先使用代理对象调用。
四. Spring MVC / Spring Boot
1. Spring MVC 执行流程?(面试必背,核心10步)
1.1 面试回答(资深版)
Spring MVC 是基于 MVC 设计模式的 Web 框架,核心是DispatcherServlet(前端控制器),负责统一调度,将请求分发到对应组件,执行流程如下(按顺序):
-
客户端发送 HTTP 请求(如 GET/POST),请求首先到达前端控制器 DispatcherServlet(所有请求的入口)。
-
DispatcherServlet 调用 HandlerMapping(处理器映射器),根据请求 URL、请求方法,匹配对应的 Handler(控制器方法,如 @RequestMapping 标注的方法),并返回 HandlerExecutionChain(包含 Handler 和拦截器)。
-
DispatcherServlet 调用HandlerAdapter(处理器适配器),适配不同类型的 Handler(如注解式 Controller、XML 配置的 Controller),执行 Handler 方法。
-
Handler 方法执行完成后,返回 ModelAndView(包含模型数据 Model 和视图名称 View)。
-
DispatcherServlet 调用 ViewResolver(视图解析器),根据 ModelAndView 中的视图名称,解析出具体的 View 实例(如 JSP、Thymeleaf 视图)。
-
View 实例接收 Model 中的数据,进行视图渲染(将模型数据填充到视图中)。
-
视图渲染完成后,将渲染结果(HTML、JSON 等)响应给客户端。
核心组件总结:DispatcherServlet(核心调度)、HandlerMapping(找 Handler)、HandlerAdapter(执行 Handler)、ViewResolver(解析视图)、Handler(业务逻辑)、View(视图渲染)。
1.2 代码实战演示(注解式 Controller 示例)
// 1. 配置 DispatcherServlet(Spring Boot 自动配置,无需手动编写)
// 2. 编写 Controller(Handler)
@Controller
@RequestMapping("/user")
public class UserController {
// Handler 方法(对应请求:GET /user/{id})
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ModelAndView getUser(@PathVariable Integer id) {
// 模拟业务逻辑:查询用户信息
User user = new User(id, "张三", 25);
// 返回 ModelAndView(模型数据 + 视图名称)
ModelAndView mav = new ModelAndView();
mav.addObject("user", user); // 模型数据
mav.setViewName("userDetail"); // 视图名称(由 ViewResolver 解析)
return mav;
}
// 响应 JSON 数据(配合 @ResponseBody,无需视图渲染)
@RequestMapping(value = "/list", method = RequestMethod.GET)
@ResponseBody
public List<User> getUserList() {
List<User> userList = Arrays.asList(
new User(1, "张三", 25),
new User(2, "李四", 28)
);
return userList; // 直接返回 JSON,跳过 ViewResolver 步骤
}
}
// 实体类
@Data
public class User {
private Integer id;
private String name;
private Integer age;
public User(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
}
1.3 生产踩坑
踩坑1:Handler 方法参数绑定错误,导致请求报错(400 Bad Request) 问题现象:客户端请求 /user/abc(路径参数为字符串),但 Handler 方法参数为 Integer id,导致报错“Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'”。 排查思路:查看请求 URL 中的参数类型与 Handler 方法参数类型是否匹配;检查参数绑定注解(如 @PathVariable、@RequestParam)是否正确使用;查看日志中的类型转换异常信息。 解决方案:1. 确保请求参数类型与 Handler 方法参数类型一致;2. 若参数可能为非数字,添加参数校验(如 @Valid + @Pattern),或使用 String 类型接收后手动转换;3. 对可能为空的参数,添加 required = false(如 @RequestParam(required = false))。
踩坑2:拦截器未配置排除路径,导致静态资源(CSS、JS、图片)无法访问 问题现象:自定义拦截器后,页面无法加载 CSS、JS 等静态资源,排查发现拦截器拦截了所有请求,包括静态资源请求。 排查思路:查看拦截器的 addPathPatterns(拦截路径)和 excludePathPatterns(排除路径)配置;检查 Spring MVC 静态资源映射是否生效(默认映射 /static、/public 等目录)。 解决方案:1. 配置拦截器排除静态资源路径(如 excludePathPatterns("/static/**", "/css/**", "/js/**"));2. 若使用 Spring Boot,可通过 spring.mvc.static-path-pattern 配置静态资源路径,确保不被拦截。
1.4 面试追问
-
DispatcherServlet 的核心作用是什么?它是如何接收请求的?(答:核心作用:统一接收所有 HTTP 请求,调度其他组件(HandlerMapping、HandlerAdapter 等),完成请求处理和响应。接收请求:Tomcat 启动时,DispatcherServlet 作为 Servlet 被初始化,配置的 url-pattern(默认 /)会拦截所有请求,请求到达后,由其 doDispatch() 方法进行后续调度。)
-
HandlerMapping 和 HandlerAdapter 的区别是什么?为什么需要 HandlerAdapter?(答:区别:HandlerMapping 负责“找 Handler”(根据请求匹配控制器方法),HandlerAdapter 负责“执行 Handler”(适配不同类型的 Handler,调用其方法)。需要 HandlerAdapter 的原因:Handler 类型多样(注解式、XML 配置式、实现 Controller 接口式),不同 Handler 的执行方式不同,HandlerAdapter 统一适配,让 DispatcherServlet 无需关注具体 Handler 的执行细节,符合开闭原则。)
-
Spring MVC 如何处理 JSON 响应?核心注解是什么?(答:通过 @ResponseBody 注解(标注在方法或类上),表示方法返回值直接作为响应体,不进行视图渲染;Spring MVC 会通过 HttpMessageConverter(如 MappingJackson2HttpMessageConverter)将返回值(如 List、实体类)转换为 JSON 格式。补充:@RestController = @Controller + @ResponseBody,类上标注后,所有方法均返回 JSON。)
-
Spring MVC 的拦截器(Interceptor)和过滤器(Filter)的区别?(答:1. 作用范围:Filter 是 Servlet 规范,作用于整个 Web 应用,拦截所有请求(包括静态资源);Interceptor 是 Spring MVC 框架提供,仅作用于 Spring MVC 管理的请求(即经过 DispatcherServlet 的请求)。2. 执行时机:Filter 在请求进入 Servlet 之前执行,在响应离开 Servlet 之后执行;Interceptor 在 Handler 方法执行前、执行中、执行后执行。3. 依赖环境:Filter 不依赖 Spring 容器;Interceptor 依赖 Spring 容器,可注入 Spring 中的 Bean。)
2. @Controller 和 @RestController 区别(高频基础题)
2.1 面试回答(资深版)
两者均用于标注 Spring MVC 中的控制器类,核心区别在于是否默认返回 JSON 响应,底层依赖 @ResponseBody 注解,具体区别如下:
-
@Controller:传统控制器注解,用于返回视图(如 JSP、Thymeleaf);若要返回 JSON,需在方法上单独添加 @ResponseBody 注解。
-
@RestController:Spring 4.0 新增注解,是 @Controller + @ResponseBody 的组合注解;类上标注后,所有方法默认返回 JSON 响应,无需单独添加 @ResponseBody,无法返回视图。
核心总结:返回视图用 @Controller + 方法返回 ModelAndView;返回 JSON 用 @RestController(或 @Controller + @ResponseBody),后者更简洁,是当前前后端分离项目的首选。
2.2 代码实战演示(对比示例)
// 1. @Controller:返回视图(需配合 ModelAndView)
@Controller
@RequestMapping("/page")
public class PageController {
// 返回视图(由 ViewResolver 解析为 userDetail.html)
@RequestMapping("/user")
public ModelAndView userPage() {
ModelAndView mav = new ModelAndView();
mav.addObject("title", "用户详情");
mav.setViewName("userDetail"); // 视图名称
return mav;
}
// @Controller + @ResponseBody:返回 JSON
@RequestMapping("/user/json")
@ResponseBody
public User userJson() {
return new User(1, "张三", 25); // 返回 JSON
}
}
// 2. @RestController:默认返回 JSON(无需 @ResponseBody)
@RestController
@RequestMapping("/api/user")
public class UserApiController {
// 直接返回 JSON,无需添加 @ResponseBody
@GetMapping("/{id}")
public User getUser(@PathVariable Integer id) {
return new User(id, "李四", 28);
}
// 无法返回视图(即使返回 String,也会被当作 JSON 字符串返回)
@GetMapping("/page")
public String userPage() {
return "userDetail"; // 响应结果:"userDetail"(JSON 字符串),而非视图
}
}
2.3 生产踩坑
踩坑3:误用 @RestController 想返回视图,导致页面无法访问(返回字符串字面量) 问题现象:控制器类标注 @RestController,方法返回 "index"(意图返回 index.html 视图),但客户端收到的响应是字符串 "index",而非视图页面。 排查思路:查看控制器类注解是否为 @RestController;确认方法是否返回 ModelAndView(@RestController 中返回 ModelAndView 也会被转换为 JSON,无法渲染视图)。 解决方案:1. 若需返回视图,将 @RestController 改为 @Controller;2. 若需同时返回视图和 JSON,在 @Controller 类中,视图方法返回 ModelAndView,JSON 方法添加 @ResponseBody。
踩坑4:@Controller 方法未添加 @ResponseBody,返回实体类导致报错 问题现象:@Controller 标注的方法直接返回 User 实体类,未添加 @ResponseBody,导致报错“Circular view path [user]: would dispatch back to the current handler URL [/api/user] again”。 原因:@Controller 方法默认返回视图名称,Spring MVC 会将实体类的 toString() 结果当作视图名称去解析,无法找到对应视图,导致循环跳转。 解决方案:在方法上添加 @ResponseBody 注解,让返回值作为 JSON 响应体;或改为返回 ModelAndView。
2.4 面试追问
-
@ResponseBody 的作用是什么?底层原理是什么?(答:作用:将方法返回值转换为 HTTP 响应体(如 JSON、XML),不进行视图渲染。底层原理:Spring MVC 通过 HttpMessageConverter 接口实现转换,默认使用 MappingJackson2HttpMessageConverter 将对象转换为 JSON(需引入 jackson-databind 依赖),可自定义 HttpMessageConverter 扩展转换格式。)
-
前后端分离项目中,为什么优先使用 @RestController?(答:前后端分离项目中,前端负责页面渲染,后端仅需提供 JSON 接口,无需返回视图;@RestController 可省略所有方法上的 @ResponseBody,简化代码,提升开发效率,避免遗漏 @ResponseBody 导致的报错。)
-
@Controller 可以和 @ResponseBody 一起使用吗?有什么场景?(答:可以。场景:控制器类中,部分方法返回视图(如后台管理系统的页面),部分方法返回 JSON(如接口调用),此时类上标注 @Controller,JSON 方法上单独添加 @ResponseBody,兼顾视图和接口需求。)
3. Spring Boot 自动配置原理?(核心高频,大厂必问)
3.1 面试回答(资深版)
Spring Boot 自动配置的核心是「约定优于配置」,通过注解和 SPI 机制,自动加载依赖的配置,无需手动编写 XML 或 Java 配置,底层核心是 @EnableAutoConfiguration 注解,具体原理如下:
-
核心注解触发:@SpringBootApplication 注解中包含 @EnableAutoConfiguration,该注解是自动配置的入口,用于开启自动配置功能。
-
SPI 机制扫描:@EnableAutoConfiguration 注解通过 @Import(AutoConfigurationImportSelector.class),加载 AutoConfigurationImportSelector 类,该类会扫描 classpath:/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件(Spring Boot 2.7+ 版本),该文件中包含所有自动配置类的全路径(如 RedisAutoConfiguration、DataSourceAutoConfiguration)。
-
条件注解筛选:每个自动配置类(如 RedisAutoConfiguration)都包含条件注解(如 @ConditionalOnClass、@ConditionalOnMissingBean),Spring 会根据当前项目的依赖、配置,筛选出符合条件的自动配置类,注入到 Spring 容器中。
-
配置绑定:自动配置类会读取 application.yml/application.properties 中的配置(如 spring.redis.host),通过 @ConfigurationProperties 注解将配置绑定到对应的实体类(如 RedisProperties),实现配置动态调整。
-
Bean 注入:符合条件的自动配置类会通过 @Bean 注解,自动向容器中注入所需的 Bean(如 RedisTemplate、DataSource),开发者无需手动配置。
核心总结:自动配置 = @EnableAutoConfiguration(入口) + SPI 扫描(找自动配置类) + 条件注解(筛选配置类) + 配置绑定(读取自定义配置) + Bean 自动注入(无需手动配置)。
3.2 代码实战演示(自动配置原理验证)
// 1. 模拟自动配置类(类似 RedisAutoConfiguration)
@Configuration
// 条件注解:当项目中存在 RedisTemplate 类(即引入 redis 依赖)时,该配置类生效
@ConditionalOnClass(RedisTemplate.class)
// 绑定配置文件中的 spring.redis 前缀配置
@EnableConfigurationProperties(RedisProperties.class)
public class MyRedisAutoConfiguration {
// 自动注入配置绑定的实体类
private final RedisProperties redisProperties;
// 构造器注入
public MyRedisAutoConfiguration(RedisProperties redisProperties) {
this.redisProperties = redisProperties;
}
// 自动向容器中注入 RedisTemplate Bean
@Bean
// 条件注解:当容器中没有 RedisTemplate Bean 时,才注入(允许开发者自定义 Bean 覆盖)
@ConditionalOnMissingBean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 读取配置文件中的 host、port 等配置
RedisConnectionFactory factory = new JedisConnectionFactory(
RedisStandaloneConfiguration.builder()
.hostName(redisProperties.getHost())
.port(redisProperties.getPort())
.build()
);
template.setConnectionFactory(factory);
return template;
}
}
// 2. 配置绑定实体类(绑定 application.yml 中的 spring.redis 配置)
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedisProperties {
// 对应配置:spring.redis.host
private String host = "localhost";
// 对应配置:spring.redis.port
private Integer port = 6379;
// 其他配置(如 password、database 等)
private String password;
private Integer database = 0;
}
// 3. 开启自动配置(@SpringBootApplication 中已包含 @EnableAutoConfiguration)
@SpringBootApplication
public class MySpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApplication.class, args);
}
}
说明:当项目引入 spring-boot-starter-data-redis 依赖后,MyRedisAutoConfiguration 会被自动扫描,且符合 @ConditionalOnClass 条件,自动注入 RedisTemplate Bean;开发者可在 application.yml 中配置 spring.redis.host 等参数,覆盖默认配置。
3.3 生产踩坑
踩坑5:引入依赖但自动配置未生效,导致 Bean 注入失败(NoSuchBeanDefinitionException) 问题现象:引入 spring-boot-starter-redis 依赖,但注入 RedisTemplate 时报错,排查发现 RedisAutoConfiguration 未生效。 排查思路:1. 检查依赖是否引入成功(查看 pom.xml/gradle 配置,确认依赖未缺失);2. 检查是否有条件注解不满足(如 @ConditionalOnClass 对应的类不存在,可能是依赖版本不匹配);3. 检查是否手动排除了自动配置类(如 @SpringBootApplication(exclude = RedisAutoConfiguration.class));4. 查看启动日志(DEBUG 级别),确认自动配置类是否被加载。 解决方案:1. 确认依赖引入正确,版本与 Spring Boot 版本匹配;2. 移除自动配置排除项;3. 若条件注解不满足,补充缺失的依赖;4. 手动添加 @Import(RedisAutoConfiguration.class),强制开启自动配置。
踩坑6:自定义 Bean 与自动配置 Bean 冲突,导致配置失效 问题现象:手动配置了 RedisTemplate Bean,但发现 application.yml 中 spring.redis 的配置不生效,排查发现自定义 Bean 未使用 RedisProperties 绑定配置。 原因:自动配置类中的 @ConditionalOnMissingBean 注解,当容器中已有自定义的 RedisTemplate Bean 时,自动配置的 Bean 不会被注入;若自定义 Bean 未绑定配置,会导致配置失效。 解决方案:1. 自定义 Bean 时,注入 RedisProperties,使用配置文件中的参数(如上述代码示例);2. 若无需自定义 Bean,删除自定义配置,使用自动配置的 Bean;3. 通过 @Primary 注解,指定自定义 Bean 为默认 Bean,同时绑定配置。
3.4 面试追问
-
@EnableAutoConfiguration 注解的作用是什么?它和 @Import 有什么关系?(答:作用:开启 Spring Boot 自动配置功能,触发自动配置类的扫描和加载。关系:@EnableAutoConfiguration 内部通过 @Import(AutoConfigurationImportSelector.class),导入 AutoConfigurationImportSelector 类,该类负责扫描和加载自动配置类,本质上是通过 @Import 实现配置类的导入。)
-
Spring Boot 自动配置中的条件注解有哪些?分别作用是什么?(答:常用条件注解:1. @ConditionalOnClass:当类路径下存在指定类时,配置类生效;2. @ConditionalOnMissingClass:当类路径下不存在指定类时,配置类生效;3. @ConditionalOnBean:当容器中存在指定 Bean 时,配置类生效;4. @ConditionalOnMissingBean:当容器中不存在指定 Bean 时,配置类生效(允许自定义 Bean 覆盖自动配置);5. @ConditionalOnProperty:当配置文件中存在指定属性(且值匹配)时,配置类生效(如 @ConditionalOnProperty(prefix = "spring.redis", name = "enabled", havingValue = "true"))。)
-
如何禁用某个自动配置类?(答:两种方式:1. 在 @SpringBootApplication 注解中排除(如 @SpringBootApplication(exclude = RedisAutoConfiguration.class));2. 在 application.yml 中配置(spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration);3. 针对特定环境,使用 @Profile 注解,仅在指定环境下禁用。)
-
Spring Boot 2.7+ 版本中,自动配置类的扫描文件为什么从 spring.factories 改为 spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports?(答:Spring Boot 2.7 之前,自动配置类通过 META-INF/spring.factories 文件配置,该文件是 SPI 机制的标准文件,但存在配置冗余、冲突难以排查的问题;2.7+ 版本改为 AutoConfiguration.imports 文件,仅需配置自动配置类的全路径,简化配置,同时提升扫描效率,避免冲突。)
4. @SpringBootApplication 由哪些注解组成?(基础必背)
4.1 面试回答(资深版)
@SpringBootApplication 是 Spring Boot 项目的核心注解,用于标注启动类,是三个核心注解的组合注解(Spring Boot 4.0+ 版本,底层无其他额外注解),具体组成如下:
-
@SpringBootConfiguration:本质是 @Configuration 注解的派生注解,标识当前类是一个 Spring 配置类,允许在类中使用 @Bean 注解注入 Bean。
-
@EnableAutoConfiguration:核心注解,开启 Spring Boot 自动配置功能(前文已详解,负责扫描和加载自动配置类)。
-
@ComponentScan:开启组件扫描,默认扫描当前启动类所在的包及其子包,将标注 @Component、@Service、@Repository、@Controller 等注解的类自动注入到 Spring 容器中。
补充说明:1. 三个注解缺一不可,组合后简化配置(无需单独标注三个注解);2. @ComponentScan 可通过 basePackages 属性自定义扫描范围,避免扫描无关包导致启动缓慢;3. @SpringBootConfiguration 与 @Configuration 功能完全一致,仅为了区分 Spring Boot 配置类和普通 Spring 配置类。
4.2 代码实战演示(注解拆解)
// 方式1:使用 @SpringBootApplication(推荐,简化写法)
@SpringBootApplication(
// 自定义组件扫描范围
scanBasePackages = "com.example",
// 排除指定自动配置类
exclude = RedisAutoConfiguration.class
)
public class MySpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApplication.class, args);
}
}
// 方式2:拆解为三个核心注解(与方式1完全等价)
@SpringBootConfiguration // 等价于 @Configuration
@EnableAutoConfiguration // 开启自动配置
@ComponentScan(basePackages = "com.example") // 自定义扫描范围
public class MySpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApplication.class, args);
}
// 配置类中可使用 @Bean 注入 Bean
@Bean
public String testBean() {
return "test";
}
}
4.3 生产踩坑
踩坑7:@ComponentScan 扫描范围配置错误,导致组件无法注入(NoSuchBeanDefinitionException) 问题现象:Controller、Service 类标注了对应注解,但注入时报错,排查发现 @SpringBootApplication 的 scanBasePackages 配置错误,未包含组件所在的包。 排查思路:查看启动类所在的包路径;检查组件(Controller、Service)所在的包路径;确认 scanBasePackages 是否包含组件所在的包(如启动类在 com.example,组件在 com.example.controller,默认扫描可覆盖;若组件在 com.test.controller,需手动配置 scanBasePackages = {"com.example", "com.test"})。 解决方案:1. 调整 scanBasePackages 配置,包含所有组件所在的包;2. 将启动类移到所有组件的父包下(如组件在 com.example.controller、com.example.service,启动类放在 com.example 下,默认扫描可覆盖);3. 单独添加 @ComponentScan 注解,扩展扫描范围。
踩坑8:误将 @SpringBootConfiguration 当作普通注解使用,导致配置类不生效 问题现象:在自定义配置类上标注 @SpringBootConfiguration,未标注 @Configuration,导致配置类中的 @Bean 注解不生效,Bean 无法注入。 原因:@SpringBootConfiguration 本质是 @Configuration 的派生注解(源码中包含 @Configuration),单独标注 @SpringBootConfiguration 即可生效,但部分开发者误以为需要同时标注 @Configuration,或误将其当作普通注解,导致配置类未被识别。 解决方案:1. 自定义配置类可标注 @SpringBootConfiguration 或 @Configuration,两者等价;2. 确保配置类在 @ComponentScan 的扫描范围内,否则配置类无法被加载。
4.4 面试追问
-
@SpringBootConfiguration 和 @Configuration 的区别是什么?(答:本质上无区别,@SpringBootConfiguration 是 @Configuration 的派生注解(源码:@Configuration + @Documented),功能完全一致;唯一区别是语义不同:@SpringBootConfiguration 用于标注 Spring Boot 项目的核心配置类(如启动类),@Configuration 用于标注普通 Spring 配置类,便于区分配置类型。)
-
@ComponentScan 除了扫描 @Component 及其派生注解,还会扫描哪些注解?(答:还会扫描 @Controller、@Service、@Repository(三者都是 @Component 的派生注解)、@RestController、@Configuration、@Bean 等注解;本质上是扫描所有被 Spring 识别为“组件”的类,只要类上有 Spring 可识别的组件注解,都会被扫描并注入容器。)
-
如果不使用 @SpringBootApplication 注解,启动 Spring Boot 项目需要哪些步骤?(答:1. 标注 @SpringBootConfiguration(或 @Configuration),标识启动类为配置类;2. 标注 @EnableAutoConfiguration,开启自动配置;3. 标注 @ComponentScan,配置组件扫描范围;4. 在 main 方法中调用 SpringApplication.run(启动类.class, args),启动 Spring 容器。)
5. Spring Boot 启动流程简述(核心流程,面试必背)
5.1 面试回答(资深版)
Spring Boot 启动流程核心是「初始化 Spring 容器 + 自动配置 + 启动嵌入式服务器」,整体分为 3 大阶段,共 7 个关键步骤,流程清晰且贴合源码逻辑:
阶段1:启动准备(初始化 SpringApplication)
-
执行启动类 main 方法,调用 SpringApplication.run(启动类.class, args),初始化 SpringApplication 实例。
-
SpringApplication 初始化时,会完成 3 件事:① 推断应用类型(Web 应用/非 Web 应用);② 加载所有初始化器(ApplicationContextInitializer);③ 加载所有监听器(ApplicationListener)。
阶段2:容器初始化(创建并初始化 ApplicationContext)
-
调用 SpringApplication 的 run() 方法,创建 ApplicationContext(Web 应用默认是 AnnotationConfigServletWebServerApplicationContext)。
-
初始化 ApplicationContext:① 执行初始化器(ApplicationContextInitializer),修改容器配置;② 注册监听器(ApplicationListener);③ 加载 Bean 定义(扫描 @Component 注解、自动配置类);④ 完成 Bean 的实例化、依赖注入和初始化(IOC 容器初始化)。
阶段3:启动完成(启动嵌入式服务器 + 发布事件)
-
初始化嵌入式服务器(如 Tomcat、Jetty):Spring Boot 自动配置会根据依赖(如 spring-boot-starter-web),自动注入 TomcatServletWebServerFactory,创建 Tomcat 实例并启动,绑定端口(默认 8080)。
-
发布容器启动完成事件(ContextRefreshedEvent),监听器接收事件后执行后续逻辑(如自定义初始化操作);启动完成,打印启动日志(如启动耗时、端口号)。
核心总结:启动流程 = 初始化 SpringApplication → 创建并初始化 ApplicationContext(IOC 容器) → 启动嵌入式服务器 → 发布启动完成事件,核心是“自动配置”和“容器初始化”。
5.2 代码实战演示(启动流程验证)
// 1. 自定义初始化器(观察容器初始化过程)
@Component
public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
// 容器初始化时执行,可修改容器配置
System.out.println("容器初始化:执行自定义初始化器");
// 示例:设置环境变量
applicationContext.getEnvironment().setActiveProfiles("dev");
}
}
// 2. 自定义监听器(监听启动完成事件)
@Component
public class MyApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 容器启动完成后执行
System.out.println("容器启动完成:发布 ContextRefreshedEvent 事件");
// 示例:获取容器中的 Bean
ConfigurableApplicationContext context = (ConfigurableApplicationContext) event.getSource();
String testBean = context.getBean("testBean", String.class);
System.out.println("容器中的 testBean:" + testBean);
}
}
// 3. 启动类(触发启动流程)
@SpringBootApplication
public class MySpringBootApplication {
public static void main(String[] args) {
System.out.println("启动开始:初始化 SpringApplication");
// 启动 Spring Boot,返回 ApplicationContext
ConfigurableApplicationContext context = SpringApplication.run(MySpringBootApplication.class, args);
System.out.println("启动完成:容器已就绪");
// 关闭容器(手动触发,正常启动后无需关闭)
// context.close();
}
@Bean
public String testBean() {
return "Spring Boot 启动成功";
}
}
启动日志顺序(对应流程):启动开始 → 执行自定义初始化器 → 容器启动完成 → 启动完成:容器已就绪,可直观看到启动流程的关键节点。
5.3 生产踩坑
踩坑9:启动时端口被占用,导致启动失败(BindException: Address already in use) 问题现象:Spring Boot 启动报错“Bind to port 8080 failed: Address already in use”,排查发现 8080 端口被其他进程占用。 排查思路:1. 查看报错日志,确认被占用的端口;2. 查看端口占用进程(Windows:netstat -ano | findstr 8080;Linux:netstat -tulpn | grep 8080);3. 确认是否有其他服务(如 Tomcat、其他 Spring Boot 项目)占用该端口。 解决方案:1. 关闭占用端口的进程(不推荐,若为必要进程);2. 修改 Spring Boot 端口(application.yml 配置:server.port=8081);3. 配置端口随机(server.port=0),由系统分配未占用端口。
踩坑10:启动时初始化器/监听器未生效,导致自定义逻辑无法执行 问题现象:自定义 ApplicationContextInitializer、ApplicationListener 后,启动时未执行对应逻辑,排查发现未将其注入容器。 原因:初始化器和监听器需被 Spring 容器扫描到(即标注 @Component,且在 @ComponentScan 扫描范围内),否则无法被 SpringApplication 加载。 解决方案:1. 在自定义初始化器/监听器上标注 @Component,确保被扫描到;2. 若未标注 @Component,可在 SpringApplication 初始化时手动添加(如 new SpringApplication(MySpringBootApplication.class).addInitializers(new MyApplicationContextInitializer()).run(args));3. 确认扫描范围包含自定义组件所在的包。
5.4 面试追问
-
Spring Boot 启动时,嵌入式服务器(Tomcat)是如何被启动的?(答:1. 引入 spring-boot-starter-web 依赖时,会自动引入 tomcat-spring-boot-starter 依赖;2. 自动配置类 TomcatServletWebServerFactory 被加载,注入到容器中;3. ApplicationContext 初始化完成后,会调用 WebServerFactory 的 getWebServer() 方法,创建 Tomcat 实例;4. 配置 Tomcat 端口、上下文路径等参数,调用 Tomcat 的 start() 方法启动服务器。)
-
SpringApplication 初始化时,如何推断应用类型?(答:通过类路径下是否存在特定类来推断:① 若存在 Servlet.class、Tomcat.class 等,推断为 Web 应用(Servlet Web 应用);② 若存在 ReactiveWebServerFactory.class,推断为响应式 Web 应用;③ 若均不存在,推断为非 Web 应用。)
-
Spring Boot 启动失败的常见原因有哪些?(答:1. 端口被占用;2. 依赖冲突(如不同版本的 Spring 依赖冲突);3. 自动配置类冲突(如自定义 Bean 与自动配置 Bean 冲突);4. 组件扫描范围错误(组件未被扫描到);5. 配置文件错误(如语法错误、参数错误);6. 依赖缺失(如引入 starter 但未引入对应依赖)。)
-
Spring Boot 启动完成后,如何执行自定义初始化逻辑?(答:3 种方式:1. 实现 ApplicationListener,监听 ContextRefreshedEvent 事件;2. 实现 CommandLineRunner 或 ApplicationRunner 接口,重写 run() 方法(启动完成后自动执行);3. 在 Bean 上添加 @PostConstruct 注解,在 Bean 初始化完成后执行。)
6. Spring Boot 如何自定义 starter?(实战高频,大厂加分题)
6.1 面试回答(资深版)
Spring Boot starter 是「依赖集合 + 自动配置」的封装,目的是简化依赖引入和配置,自定义 starter 核心是「编写自动配置类 + 配置 SPI 扫描路径」,整体分为 5 个步骤,可直接复用:
-
创建 Maven 项目:命名遵循 Spring Boot 规范(官方 starter 命名:spring-boot-starter-xxx,自定义 starter 命名:xxx-spring-boot-starter,避免与官方冲突)。
-
引入核心依赖:引入 spring-boot-starter(基础依赖,提供自动配置支持)、spring-boot-autoconfigure(自动配置核心依赖)、spring-boot-configuration-processor(可选,用于生成配置元数据,方便 IDE 提示)。
-
编写配置绑定实体类:通过 @ConfigurationProperties 注解,绑定 application.yml 中的自定义配置(如 xxx.xxx 参数)。
-
编写自动配置类:通过 @Configuration、@Conditional 等注解,编写自动配置逻辑,注入所需 Bean,实现“约定优于配置”。
-
配置 SPI 扫描路径:在 resources/META-INF 目录下,创建 spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,写入自动配置类的全路径,让 Spring Boot 扫描到该自动配置类。
核心总结:自定义 starter = 规范命名 + 核心依赖 + 配置绑定 + 自动配置类 + SPI 配置,本质是复用 Spring Boot 自动配置机制,封装可复用的组件。
6.2 代码实战演示(自定义 starter 完整示例)
步骤1:创建 Maven 项目,命名:my-log-spring-boot-starter(自定义 starter 规范命名)
步骤2:引入核心依赖(pom.xml)
<dependencies>
<!-- Spring Boot 基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.0</version>
<scope>provided</scope>
</dependency>
<!-- 自动配置核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.7.0</version>
</dependency>
<!-- 生成配置元数据(可选,IDE 提示用) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.7.0</version>
<optional>true</optional>
</dependency>
</dependencies>
步骤3:编写配置绑定实体类(绑定自定义配置)
// 绑定 application.yml 中 "my.log" 前缀的配置
@ConfigurationProperties(prefix = "my.log")
@Data
public class MyLogProperties {
// 默认配置:日志开关开启
private boolean enabled = true;
// 默认配置:日志级别 info
private String level = "info";
// 默认配置:日志输出路径
private String path = "logs/my-log.log";
}
步骤4:编写自动配置类(核心逻辑)
// 自动配置类
@Configuration
// 条件注解:当配置 my.log.enabled = true 时生效(默认开启)
@ConditionalOnProperty(prefix = "my.log", name = "enabled", havingValue = "true", matchIfMissing = true)
// 绑定配置类
@EnableConfigurationProperties(MyLogProperties.class)
public class MyLogAutoConfiguration {
// 注入配置绑定的实体类
private final MyLogProperties logProperties;
// 构造器注入
public MyLogAutoConfiguration(MyLogProperties logProperties) {
this.logProperties = logProperties;
}
// 自动注入自定义日志工具类 Bean
@Bean
// 条件注解:当容器中没有 MyLogUtil Bean 时,才注入
@ConditionalOnMissingBean
public MyLogUtil myLogUtil() {
MyLogUtil logUtil = new MyLogUtil();
// 将配置文件中的参数设置到日志工具类中
logUtil.setLevel(logProperties.getLevel());
logUtil.setPath(logProperties.getPath());
// 初始化日志(如创建日志文件)
logUtil.init();
return logUtil;
}
}
// 自定义日志工具类(starter 核心功能)
public class MyLogUtil {
private String level;
private String path;
// 初始化日志
public void init() {
System.out.println("自定义日志工具初始化:级别=" + level + ",路径=" + path);
}
// 日志输出方法
public void info(String message) {
if ("info".equals(level)) {
System.out.println("[INFO] " + message);
}
}
// getter/setter 方法
public void setLevel(String level) {
this.level = level;
}
public void setPath(String path) {
this.path = path;
}
}
步骤5:配置 SPI 扫描路径(关键步骤)
在 resources 目录下,创建目录 META-INF/spring,创建文件 org.springframework.boot.autoconfigure.AutoConfiguration.imports,写入自动配置类的全路径:
com.example.myLog.starter.MyLogAutoConfiguration
步骤6:使用自定义 starter(其他项目引入)
// 引入自定义 starter(本地安装到 Maven 仓库后引入)
<dependency>
<groupId>com.example</groupId>
<artifactId>my-log-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
// application.yml 中配置自定义参数(可选,覆盖默认配置)
my:
log:
enabled: true
level: debug
path: logs/custom-log.log
// 项目中注入使用
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(TestApplication.class, args);
// 注入自定义日志工具类
MyLogUtil logUtil = context.getBean(MyLogUtil.class);
logUtil.info("自定义 starter 测试成功");
}
}
6.3 生产踩坑
踩坑11:自定义 starter 未配置 SPI 路径,导致自动配置类未被扫描,Bean 无法注入 问题现象:其他项目引入自定义 starter 后,注入 MyLogUtil 报错,排查发现 MyLogAutoConfiguration 未被 Spring Boot 扫描到,原因是未配置 AutoConfiguration.imports 文件。 排查思路:1. 检查自定义 starter 的 resources/META-INF/spring 目录下,是否存在 AutoConfiguration.imports 文件;2. 检查文件中是否正确写入自动配置类的全路径(无拼写错误、包路径正确);3. 查看启动日志(DEBUG 级别),确认自动配置类是否被加载。 解决方案:1. 按规范创建 AutoConfiguration.imports 文件,写入自动配置类全路径;2. 若使用 Spring Boot 2.7 之前版本,需配置 META-INF/spring.factories 文件(org.springframework.boot.autoconfigure.EnableAutoConfiguration=自动配置类全路径);3. 重新打包 starter,安装到 Maven 仓库后重新引入。
踩坑12:自定义 starter 依赖冲突,导致引入项目启动失败 问题现象:其他项目引入自定义 starter 后,启动报错“NoClassDefFoundError”,排查发现 starter 中引入的 spring-boot-starter 版本与项目中 Spring Boot 版本不一致,导致依赖冲突。 原因:自定义 starter 中引入的依赖(如 spring-boot-starter)未指定 scope 为 provided,导致引入项目时,starter 中的依赖覆盖了项目本身的依赖,出现版本冲突。 解决方案:1. 将 starter 中 spring-boot-starter、spring-boot-autoconfigure 等依赖的 scope 设为 provided(仅在编译时生效,不打包到 starter 中,使用引入项目的依赖版本);2. 统一 starter 与引入项目的 Spring Boot 版本,避免版本冲突;3. 使用 maven-enforcer-plugin 插件,强制统一依赖版本。
6.4 面试追问
-
自定义 starter 为什么要遵循命名规范?(答:1. 区分官方 starter 和自定义 starter,避免命名冲突(官方 starter 是 spring-boot-starter-xxx,自定义是 xxx-spring-boot-starter);2. 符合开发者使用习惯,便于其他开发者识别和引入;3. 避免 Spring Boot 自动配置时,误识别为官方 starter,导致配置冲突。)
-
spring-boot-configuration-processor 依赖的作用是什么?如果不引入会有什么影响?(答:作用:生成配置元数据(META-INF/spring-configuration-metadata.json),让 IDE(如 IDEA)在 application.yml 中编写自定义配置时,提供参数提示(如 my.log.enabled、my.log.level)。不引入的影响:无功能影响,仅 IDE 无配置提示,开发者需手动记忆配置参数,容易出现拼写错误。)
-
如何让自定义 starter 支持条件配置?(答:通过 Spring Boot 的条件注解实现,如 @ConditionalOnProperty(根据配置参数生效)、@ConditionalOnClass(根据依赖生效)、@ConditionalOnMissingBean(避免与自定义 Bean 冲突);在自动配置类上添加对应条件注解,实现“按需生效”,提升 starter 的灵活性。)
-
自定义 starter 如何发布和复用?(答:1. 打包:使用 Maven 的 package 命令,将 starter 打包为 jar 包;2. 发布:将 jar 包安装到本地 Maven 仓库(mvn install),或发布到远程 Maven 仓库(如 Nexus);3. 复用:其他项目在 pom.xml 中引入 starter 的依赖,即可自动加载其自动配置,注入相关 Bean,无需额外配置。)
五. 高阶 & 源码向
1. Spring 循环依赖怎么解决?三级缓存原理(源码级高频,大厂必问)
1.1 面试回答(源码版)
Spring 循环依赖指两个或多个 Bean 互相依赖(如 A 依赖 B,B 依赖 A),Spring 仅能解决「单例 Bean + Setter 注入/字段注入」的循环依赖,构造器注入的循环依赖无法解决(直接报错 BeanCurrentlyInCreationException),核心解决方案是 三级缓存机制,底层依赖 DefaultSingletonBeanRegistry 中的三个缓存容器。
(1)、先明确:Spring 能解决的循环依赖场景
-
Bean 作用域:必须是 单例(多例 Bean 每次获取都新建,无法缓存,无法解决循环依赖);
-
注入方式:Setter 注入(推荐)或字段注入(@Autowired),构造器注入无法解决(初始化顺序冲突)。
(2)、三级缓存核心定义(源码核心属性)
Spring 源码中,三级缓存定义在 DefaultSingletonBeanRegistry 类中,本质是三个 Map,作用是缓存不同初始化阶段的单例 Bean:
-
一级缓存(singletonObjects):key=BeanName,value=「初始化完成的单例 Bean」(完全就绪,可直接使用);
-
二级缓存(earlySingletonObjects):key=BeanName,value=「提前暴露的未初始化完成的单例 Bean」(仅实例化,未完成依赖注入和初始化,无代理);
-
三级缓存(singletonFactories):key=BeanName,value=「Bean 工厂对象(ObjectFactory)」,用于提前暴露 Bean 实例,可生成代理对象(解决 AOP 代理与循环依赖的冲突)。
(3)、三级缓存解决循环依赖的完整流程(以 A 依赖 B、B 依赖 A 为例)
-
容器启动,开始初始化 A Bean:调用 doCreateBean() 方法,先实例化 A(通过构造方法创建 A 的原始实例),此时 A 未完成依赖注入和初始化;
-
将 A 的实例封装为 ObjectFactory(工厂对象),存入三级缓存(singletonFactories),目的是“提前暴露 A 的实例”,避免后续 B 依赖 A 时无法获取;
-
A 开始进行依赖注入,发现依赖 B,容器开始初始化 B Bean;
-
调用 doCreateBean() 方法,实例化 B,同样将 B 的实例封装为 ObjectFactory,存入三级缓存;
-
B 开始进行依赖注入,发现依赖 A,容器尝试从缓存中获取 A:
-
先查一级缓存(singletonObjects):A 未初始化完成,无;
-
再查二级缓存(earlySingletonObjects):A 未提前暴露到二级缓存,无;
-
最后查三级缓存(singletonFactories):获取到 A 的 ObjectFactory,调用 getObject() 方法,获取 A 的原始实例,将 A 从三级缓存移到二级缓存(标记为“提前暴露”);
-
B 注入 A 的实例,完成依赖注入和自身初始化,B 初始化完成后,存入一级缓存,同时从二级、三级缓存中移除;
-
回到 A 的依赖注入,容器从一级缓存中获取已初始化完成的 B,注入到 A 中;
-
A 完成依赖注入和自身初始化,存入一级缓存,同时从二级、三级缓存中移除,循环依赖解决。
(4)、关键补充(源码细节)
1. 三级缓存的核心作用:解决「AOP 代理与循环依赖」的冲突——若 A 需要被 AOP 代理,ObjectFactory 会生成代理对象并暴露,而非原始实例,确保 B 注入的是 A 的代理对象,避免后续使用时出现代理失效;
2. 为什么需要三级缓存,而非二级缓存?:若仅用二级缓存,无法处理 AOP 代理场景——提前暴露的是原始实例,后续生成代理对象后,无法替换二级缓存中的原始实例,导致 B 注入的是原始实例,而非代理对象,出现业务异常;
3. 构造器注入无法解决循环依赖的原因:构造器注入时,Bean 实例化(构造方法执行)前就需要依赖对象,而此时依赖对象尚未实例化,无法提前暴露,缓存无法生效,直接报错。
1.2 源码片段(核心逻辑)
// 源码:DefaultSingletonBeanRegistry 中的三级缓存定义
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
// 一级缓存:初始化完成的单例 Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:提前暴露的未初始化 Bean
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:Bean 工厂对象,用于生成提前暴露的实例
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
// 核心方法:获取单例 Bean(缓存查询逻辑)
@Override
@Nullable
public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 先查一级缓存:获取初始化完成的 Bean
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 2. 一级缓存没有,且 Bean 正在创建中,查二级缓存
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 3. 二级缓存没有,查三级缓存,获取 ObjectFactory
synchronized (this.singletonObjects) {
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 4. 调用 ObjectFactory 的 getObject(),获取提前暴露的实例
singletonObject = singletonFactory.getObject();
// 5. 将实例从三级缓存移到二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
}
1.3 生产踩坑
踩坑1:构造器注入导致循环依赖,容器启动失败 问题现象:项目中 A 用构造器注入 B,B 用构造器注入 A,容器启动报错“BeanCurrentlyInCreationException: Bean with name 'a' has been injected into other beans [b] in its raw version as part of a circular reference”。 排查思路:查看报错堆栈,确认循环依赖的 Bean 名称;检查两个 Bean 的注入方式,是否均为构造器注入;确认 Bean 作用域是否为单例(多例即使是 Setter 注入也无法解决)。 解决方案:1. 将其中一个 Bean 的注入方式改为 Setter 注入(推荐);2. 对其中一个依赖添加 @Lazy 注解,延迟加载,打破循环初始化(如 @Lazy private B b);3. 拆分 Bean 职责,将公共逻辑提取到第三方 Bean,避免互相依赖;4. 若必须用构造器注入,使用 @Autowired(required = false) 配合 @Bean 手动配置,避免循环。
踩坑2:AOP 代理与循环依赖结合,导致代理对象失效 问题现象:A 依赖 B,B 依赖 A,A 被 AOP 代理(如 @Transactional),运行时发现 A 的代理失效(事务不生效),排查发现 B 注入的是 A 的原始实例,而非代理实例。 原因:三级缓存配置异常,或自定义 BeanPostProcessor 影响了 ObjectFactory 的代理生成逻辑,导致提前暴露的是 A 的原始实例,而非代理实例。 解决方案:1. 确认 Bean 是单例且使用 Setter/字段注入(符合三级缓存生效条件);2. 检查自定义 BeanPostProcessor,避免干扰 AOP 代理生成;3. 手动指定 A 的代理方式(如 @EnableAspectJAutoProxy(proxyTargetClass = true));4. 避免在循环依赖的 Bean 中,在 @PostConstruct 方法中调用依赖对象的代理方法(此时代理可能未生成完成)。
踩坑3:多例 Bean 循环依赖,导致容器启动失败或运行时异常 问题现象:A 和 B 均为多例(@Scope("prototype")),互相依赖,容器启动时要么报错,要么运行时获取到的 Bean 实例重复。 原因:多例 Bean 每次获取都新建,无法存入三级缓存,Spring 无法缓存提前暴露的实例,无法解决循环依赖。 解决方案:1. 将多例 Bean 改为单例(推荐,符合大多数业务场景);2. 用 ObjectFactory<T> 延迟获取多例依赖(如 @Autowired private ObjectFactory<B> bFactory),每次调用 bFactory.getObject() 获取新实例;3. 拆分循环依赖,避免多例 Bean 互相依赖。
1.4 面试追问
-
Spring 三级缓存中,ObjectFactory 的作用是什么?(答:核心作用有两个:1. 提前暴露 Bean 实例,解决循环依赖——在 Bean 未初始化完成时,通过 ObjectFactory 暴露实例,供依赖方获取;2. 处理 AOP 代理——若 Bean 需要被代理,ObjectFactory 的 getObject() 方法会生成代理对象并返回,确保依赖方注入的是代理对象,而非原始实例,避免代理失效。)
-
如果禁用三级缓存(只保留一级和二级),会有什么问题?(答:无法解决「AOP 代理与循环依赖」的冲突。因为二级缓存只能存储提前暴露的原始实例,若 Bean 需要被 AOP 代理,后续生成的代理对象无法替换二级缓存中的原始实例,导致依赖方注入的是原始实例,而非代理对象,出现 AOP 功能失效(如事务、日志不生效)。)
-
Spring Boot 中,如何排查循环依赖问题?(答:1. 查看启动日志,找到 BeanCurrentlyInCreationException 报错,确认循环依赖的 Bean 名称;2. 检查这些 Bean 的注入方式(是否为构造器注入)和作用域(是否为多例);3. 用 Spring 提供的工具类(如 BeanDefinitionRegistry)打印 Bean 的依赖关系;4. 开启 DEBUG 级别日志,查看 Bean 的初始化顺序和缓存操作流程,定位问题根源。)
-
Spring 5.x 对循环依赖的处理有什么优化?(答:Spring 5.x 主要优化了构造器注入循环依赖的报错信息,更清晰地提示循环依赖的 Bean 名称和注入方式;同时优化了三级缓存的性能,减少了锁竞争;此外,支持通过 @Lazy 注解更灵活地处理构造器注入的循环依赖,但本质上仍未解决构造器注入的循环依赖,只是延迟暴露问题。)
2. Bean 初始化前后扩展点有哪些?(源码级,高频扩展题)
2.1 面试回答(源码版)
Spring 提供了多种Bean 初始化前后的扩展点,核心作用是在 Bean 实例化、依赖注入、初始化的前后,插入自定义逻辑(如增强 Bean、修改 Bean 属性、初始化资源等),底层依赖 Spring 的 BeanPostProcessor 接口及相关派生类,按执行顺序可分为 6 个核心扩展点,结合源码逻辑如下(按执行顺序排列):
核心扩展点(按执行顺序,结合 Bean 生命周期)
-
BeanFactoryPostProcessor(Bean 定义后置处理器)
-
执行时机:Bean 实例化之前(BeanDefinition 加载完成后,Bean 未实例化、未注入依赖);
-
核心作用:修改 BeanDefinition 的属性(如修改 Bean 的作用域、初始化方法、依赖关系等),不修改 Bean 实例本身;
-
源码关联:由 Spring 容器在 BeanDefinition 注册完成后,主动调用其 postProcessBeanFactory() 方法;
-
常用实现:PropertySourcesPlaceholderConfigurer(处理配置文件占位符 ${})、CustomAutowireConfigurer(自定义自动注入规则)。
-
-
BeanPostProcessor#postProcessBeforeInitialization(Bean 初始化前增强)
-
执行时机:Bean 实例化、依赖注入完成后,初始化方法(@PostConstruct、InitializingBean)执行之前;
-
核心作用:对 Bean 实例进行增强(如修改 Bean 的属性、添加额外逻辑),返回修改后的 Bean 实例;
-
源码关联:所有 Bean 初始化前都会执行,由 AbstractAutowireCapableBeanFactory 的 applyBeanPostProcessorsBeforeInitialization() 方法调用;
-
注意:若返回 null,会导致 Bean 无法注入容器,抛出异常。
-
-
@PostConstruct(JSR-250 注解)
-
执行时机:Bean 初始化前增强(postProcessBeforeInitialization)之后,InitializingBean 之前;
-
核心作用:自定义 Bean 初始化逻辑(如加载配置、初始化资源),无需实现接口,仅需标注在方法上;
-
源码关联:由 CommonAnnotationBeanPostProcessor(BeanPostProcessor 的实现类)解析并调用。
-
-
InitializingBean 接口
-
执行时机:@PostConstruct 之后,自定义 init-method 之前;
-
核心作用:自定义 Bean 初始化逻辑,需实现 afterPropertiesSet() 方法;
-
源码关联:由 AbstractAutowireCapableBeanFactory 的 invokeInitMethods() 方法调用,优先级高于自定义 init-method。
-
-
自定义 init-method(XML/@Bean 配置)
-
执行时机:InitializingBean 之后,Bean 初始化后增强之前;
-
核心作用:自定义 Bean 初始化逻辑,通过 XML 配置(init-method)或 @Bean(initMethod = "xxx") 指定;
-
源码关联:由 AbstractAutowireCapableBeanFactory 的 invokeInitMethods() 方法调用,优先级最低。
-
-
BeanPostProcessor#postProcessAfterInitialization(Bean 初始化后增强)
-
执行时机:Bean 所有初始化方法(@PostConstruct、InitializingBean、init-method)执行完成后,Bean 就绪之前;
-
核心作用:对 Bean 实例进行最终增强(如 AOP 代理生成),返回最终的 Bean 实例(可能是代理对象);
-
源码关联:由 AbstractAutowireCapableBeanFactory 的 applyBeanPostProcessorsAfterInitialization() 方法调用;
-
常用实现:AnnotationAwareAspectJAutoProxyCreator(生成 AOP 代理对象)。
-
补充:销毁阶段的扩展点(关联记忆)
虽然题目问的是初始化前后,但面试中常追问销毁阶段扩展点,核心有 3 个:@PreDestroy(JSR-250 注解)→ DisposableBean 接口 → 自定义 destroy-method,执行顺序与初始化阶段对应。
2.2 代码实战(自定义扩展点示例)
// 1. 自定义 BeanFactoryPostProcessor(修改 BeanDefinition)
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 修改 "userService" 的 BeanDefinition:将单例改为多例
BeanDefinition beanDefinition = beanFactory.getBeanDefinition("userService");
beanDefinition.setScope("prototype");
System.out.println("BeanFactoryPostProcessor:修改 userService 的作用域为多例");
}
}
// 2. 自定义 BeanPostProcessor(初始化前后增强)
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
// 初始化前增强
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if ("userService".equals(beanName)) {
// 修改 Bean 的属性
UserService userService = (UserService) bean;
userService.setUserName("测试");
System.out.println("BeanPostProcessor 初始化前:修改 userService 的 userName");
}
return bean; // 返回修改后的 Bean
}
// 初始化后增强(模拟 AOP 代理)
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if ("userService".equals(beanName)) {
System.out.println("BeanPostProcessor 初始化后:对 userService 进行增强");
// 模拟生成代理对象(实际由 AOP 框架自动生成)
return Proxy.newProxyInstance(
bean.getClass().getClassLoader(),
bean.getClass().getInterfaces(),
(proxy, method, args) -> {
System.out.println("代理增强:方法执行前");
Object result = method.invoke(bean, args);
System.out.println("代理增强:方法执行后");
return result;
}
);
}
return bean;
}
}
// 3. 测试 Bean(包含初始化扩展点)
@Service
public class UserService implements InitializingBean {
private String userName;
// 1. @PostConstruct 初始化
@PostConstruct
public void postConstruct() {
System.out.println("@PostConstruct:执行初始化,userName = " + userName);
}
// 2. InitializingBean 初始化
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("InitializingBean:执行 afterPropertiesSet");
}
// 3. 自定义 init-method(通过 @Bean 指定)
public void initMethod() {
System.out.println("自定义 init-method:执行初始化");
}
// 业务方法(会被代理增强)
public void sayHello() {
System.out.println("Hello," + userName);
}
// getter/setter
public void setUserName(String userName) {
this.userName = userName;
}
}
// 4. 配置类(指定自定义 init-method)
@Configuration
public class AppConfig {
@Bean(initMethod = "initMethod")
public UserService userService() {
return new UserService();
}
}
执行顺序日志:BeanFactoryPostProcessor → BeanPostProcessor 初始化前 → @PostConstruct → InitializingBean → 自定义 init-method → BeanPostProcessor 初始化后 → 代理增强执行。
2.3 生产踩坑
踩坑4:自定义 BeanPostProcessor 未返回 Bean 实例,导致 Bean 注入失败问题现象:自定义 BeanPostProcessor 的 postProcessBeforeInitialization 或 postProcessAfterInitialization 方法返回 null,导致容器启动报错“BeanPostProcessor returned null”,对应的 Bean 无法注入。 排查思路:查看自定义 BeanPostProcessor 的实现,确认两个增强方法是否有返回值;检查日志中的报错信息,定位到返回 null 的 BeanPostProcessor。 解决方案:确保 BeanPostProcessor 的两个增强方法,始终返回 Bean 实例(即使不修改,也返回原 bean 对象),禁止返回 null。
踩坑5:BeanFactoryPostProcessor 中依赖其他 Bean,导致依赖注入失败 问题现象:在 MyBeanFactoryPostProcessor 中用 @Autowired 注入 UserService,启动时报错“NoSuchBeanDefinitionException”,排查发现 UserService 尚未实例化。 原因:BeanFactoryPostProcessor 执行时机是 Bean 实例化之前,此时依赖的 Bean 还未创建,无法注入。 解决方案:1. 避免在 BeanFactoryPostProcessor 中依赖其他 Bean;2. 若必须依赖,通过 BeanFactory 获取(beanFactory.getBean("userService")),但需确保该 Bean 已提前注册(如通过 @Bean 配置在配置类中);3. 将依赖逻辑移到 BeanPostProcessor 中(执行时机较晚,Bean 已实例化)。
踩坑6:多个 BeanPostProcessor 执行顺序混乱,导致增强逻辑失效 问题现象:自定义了两个 BeanPostProcessor(A 和 B),期望 A 先执行、B 后执行,但实际执行顺序相反,导致 B 的增强逻辑覆盖了 A 的逻辑。 原因:未指定 BeanPostProcessor 的执行顺序,默认按 Bean 的注册顺序执行,无法保证顺序。 解决方案:1. 让 BeanPostProcessor 实现 Ordered 接口,重写 getOrder() 方法(值越小,优先级越高);2. 标注 @Order 注解,指定执行顺序(如 @Order(1)、@Order(2));3. 按需求调整 Bean 的注册顺序(如配置类中先定义优先级高的 BeanPostProcessor)。
2.4 面试追问
-
BeanFactoryPostProcessor 和 BeanPostProcessor 的核心区别是什么?(答:1. 执行时机不同:BeanFactoryPostProcessor 在 Bean 实例化之前执行(操作 BeanDefinition);BeanPostProcessor 在 Bean 实例化、依赖注入之后执行(操作 Bean 实例);2. 作用对象不同:BeanFactoryPostProcessor 作用于 BeanDefinition(修改配置);BeanPostProcessor 作用于 Bean 实例(增强实例);3. 执行次数不同:BeanFactoryPostProcessor 整个容器启动时仅执行一次;BeanPostProcessor 对每个 Bean 都执行一次(两个增强方法各执行一次)。)
-
Spring 中哪些核心功能依赖 BeanPostProcessor 实现?(答:1. 依赖注入(AutowiredAnnotationBeanPostProcessor 处理 @Autowired、@Value);2. AOP 代理(AnnotationAwareAspectJAutoProxyCreator 生成代理对象);3. 注解解析(CommonAnnotationBeanPostProcessor 处理 @PostConstruct、@PreDestroy、@Resource);4. Bean 增强(自定义逻辑注入)。)
-
@PostConstruct 和 InitializingBean 的区别是什么?为什么推荐用 @PostConstruct?(答:区别:1. 实现方式:@PostConstruct 是注解,无需实现接口,代码简洁;InitializingBean 是接口,需实现 afterPropertiesSet() 方法,有侵入性;2. 优先级:@PostConstruct 先执行,InitializingBean 后执行;3. 灵活性:@PostConstruct 可标注在任意方法上,InitializingBean 只能实现固定方法。推荐 @PostConstruct 的原因:无侵入性、代码简洁、符合 JSR 规范,可独立于 Spring 框架使用。)
-
如何自定义一个 BeanPostProcessor,实现对指定 Bean 的增强?(答:1. 实现 BeanPostProcessor 接口,重写 postProcessBeforeInitialization 和 postProcessAfterInitialization 方法;2. 在方法中通过 beanName 或 bean 的类型,判断是否是需要增强的 Bean;3. 对目标 Bean 进行属性修改、逻辑增强,或生成代理对象;4. 将自定义 BeanPostProcessor 标注 @Component,确保被 Spring 扫描到并注入容器。)
3. @Configuration 和 @Component 区别(源码级,基础高阶结合题)
3.1 面试回答(源码版)
@Configuration 和 @Component 均是 Spring 中用于标注“组件”的注解,均可被 @ComponentScan 扫描到并注入容器,但两者的核心区别在于是否支持完整的配置类特性,底层依赖 Spring 对配置类的特殊处理(CGLIB 代理),具体区别如下(从源码、功能、使用场景三个维度):
一、核心区别(源码+功能)
|
对比维度 |
@Configuration |
@Component |
|
底层处理 |
Spring 会为其生成 CGLIB 代理对象,拦截 @Bean 方法的调用,确保每次调用都返回同一个单例 Bean(即使手动调用方法);源码中包含 @Component,本质是 @Component 的派生注解。 |
仅作为普通组件注入容器,不生成代理对象,@Bean 方法调用时,每次都会新建一个实例(无法保证单例)。 |
|
@Bean 方法特性 |
支持 @Bean 方法间依赖调用(如 A 方法返回 BeanA,B 方法调用 A() 获取 BeanA),确保依赖的是容器中的单例 Bean,而非新建实例;支持 @Import、@ImportResource 等配置类专属注解。 |
@Bean 方法间调用时,会直接执行方法,新建实例,无法复用容器中的 Bean;不支持配置类专属注解(如 @ImportResource 无效)。 |
|
核心作用 |
作为 Spring 配置类,用于集中管理 Bean 的定义(通过 @Bean 注入 Bean)、导入配置、指定扫描范围等,是 Spring 纯注解配置的核心。 |
作为普通组件(如 Service、Controller、工具类),用于标识需要被 Spring 管理的 Bean,无需集中管理 Bean 定义。 |
|
使用场景 |
用于配置类(如 AppConfig),集中注入第三方 Bean(如 RedisTemplate、DataSource)、导入配置文件、自定义 Bean 定义。 |
用于业务组件(Service、Controller、Repository)、工具类等,无需集中配置,仅需被容器管理即可。 |
二、源码细节补充
-
@Configuration 源码:包含 @Component 注解,因此能被 @ComponentScan 扫描到;同时包含 @Configuration(proxyBeanMethods = true)(默认 true),proxyBeanMethods = true 表示开启 CGLIB 代理,确保 @Bean 方法调用返回单例 Bean;若设为 false,不生成代理,与 @Component 效果一致(用于提升性能,适合无 @Bean 方法依赖的场景)。
-
@Component 源码:仅包含 @Component 注解,无其他特殊属性,Spring 按普通组件处理,不进行代理增强,@Bean 方法仅作为普通方法执行。
3.2 代码实战(区别验证)
// 1. @Configuration 配置类(开启 CGLIB 代理,默认 proxyBeanMethods = true)
@Configuration
public class ConfigClass {
// @Bean 方法:注入 BeanA
@Bean
public BeanA beanA() {
return new BeanA();
}
// @Bean 方法:依赖 BeanA,调用 beanA() 方法
@Bean
public BeanB beanB() {
// 由于 CGLIB 代理,此处调用 beanA() 不会新建实例,而是获取容器中的单例 BeanA
return new BeanB(beanA());
}
}
// 2. @Component 标注的类(无代理)
@Component
public class ComponentClass {
// @Bean 方法:注入 BeanC
@Bean
public BeanC beanC() {
return new BeanC();
}
// @Bean 方法:依赖 BeanC,调用 beanC() 方法
@Bean
public BeanD beanD() {
// 无代理,此处调用 beanC() 会新建一个 BeanC 实例,而非容器中的单例
return new BeanD(beanC());
}
}
// 测试类
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(TestApplication.class, args);
// 测试 @Configuration:beanA() 调用两次,获取的是同一个实例
ConfigClass configClass = context.getBean(ConfigClass.class);
BeanA beanA1 = configClass.beanA();
BeanA beanA2 = configClass.beanA();
System.out.println("ConfigClass 中 beanA 两次调用是否相同:" + (beanA1 == beanA2)); // true
// 测试 @Component:beanC() 调用两次,获取的是不同实例
ComponentClass componentClass = context.getBean(ComponentClass.class);
BeanC beanC1 = componentClass.beanC();
BeanC beanC2 = componentClass.beanC();
System.out.println("ComponentClass 中 beanC 两次调用是否相同:" + (beanC1 == beanC2)); // false
}
}
// 实体类(省略 getter/setter)
class BeanA {}
class BeanB { private BeanA beanA; public BeanB(BeanA beanA) { this.beanA = beanA; } }
class BeanC {}
class BeanD { private BeanC beanC; public BeanD(BeanC beanC) { this.beanC = beanC; } }
关键结论:@Configuration 中,@Bean 方法间调用返回同一个实例(CGLIB 代理拦截);@Component 中,@Bean 方法间调用返回不同实例(无代理)。
3.3 生产踩坑
踩坑7:用 @Component 标注配置类,导致 @Bean 方法依赖调用时生成多实例,出现数据错乱 问题现象:将配置类标注为 @Component,其中 beanB() 调用 beanA() 获取依赖,运行时发现每次调用 beanB() 都会新建一个 BeanA 实例,导致 BeanA 中的状态数据错乱(如成员变量被多次修改)。 排查思路:查看配置类的注解是否为 @Component(而非 @Configuration);检查 @Bean 方法间的调用逻辑,确认是否存在多次调用;打印 Bean 实例的 hashcode,确认是否为不同实例。 解决方案:将配置类的 @Component 改为 @Configuration,开启 CGLIB 代理,确保 @Bean 方法调用返回单例 Bean;若无需代理(无 @Bean 方法依赖),可设置 @Configuration(proxyBeanMethods = false),提升性能。
踩坑8:@Configuration(proxyBeanMethods = false) 场景下,@Bean 方法依赖调用出现异常 问题现象:配置类设置 @Configuration(proxyBeanMethods = false),beanB() 调用 beanA() 获取依赖,运行时发现 BeanB 中的 BeanA 与容器中的 BeanA 不是同一个实例,导致依赖注入异常。 原因:proxyBeanMethods = false 会关闭 CGLIB 代理,@Bean 方法调用时会新建实例,而非获取容器中的单例 Bean,导致依赖的 Bean 与容器中的 Bean 不一致。 解决方案:1. 若有 @Bean 方法依赖,将 proxyBeanMethods 改为 true(默认值);2. 避免 @Bean 方法间直接调用,改为通过 @Autowired 注入依赖(如在 beanB() 方法参数中注入 BeanA)。
踩坑9:误将 @Configuration 当作普通组件使用,导致配置类被多次实例化 问题现象:将 @Configuration 标注的类,通过 @Autowired 注入到多个 Bean 中,发现配置类被多次实例化(非单例),导致 @Bean 方法被多次执行,生成多个实例。 原因:@Configuration 标注的类默认是单例,但若手动设置 @Scope("prototype"),或被其他组件强制修改作用域,会导致多次实例化。 解决方案:1. 确保 @Configuration 类的作用域为单例(默认),不手动修改 @Scope;2. 避免将 @Configuration 类作为普通组件注入,配置类仅用于集中管理 @Bean,不参与业务逻辑。
3.4 面试追问
-
@Configuration 的 proxyBeanMethods 属性有什么作用?true 和 false 有什么区别?(答:作用:控制是否为配置类生成 CGLIB 代理,决定 @Bean 方法调用的行为。区别:1. proxyBeanMethods = true(默认):生成 CGLIB 代理,@Bean 方法间调用返回容器中的单例 Bean,支持依赖注入;2. proxyBeanMethods = false:不生成代理,@Bean 方法间调用返回新实例,不依赖容器,启动速度更快,适合无 @Bean 方法依赖的场景(如纯导入配置、无依赖的 @Bean 定义)。)
-
为什么 @Configuration 能被 @ComponentScan 扫描到?(答:因为 @Configuration 源码中包含 @Component 注解(@Configuration 是 @Component 的派生注解),@ComponentScan 扫描时,会识别所有标注 @Component 及其派生注解(@Controller、@Service、@Repository、@Configuration)的类,因此 @Configuration 类能被扫描到并注入容器。)
-
@Component 标注的类中,如何确保 @Bean 方法间调用返回单例 Bean?(答:两种方式:1. 将 @Component 改为 @Configuration(推荐),利用 CGLIB 代理确保单例;2. 避免 @Bean 方法间直接调用,通过 @Autowired 注入依赖(如在 beanD() 方法参数中注入 BeanC,而非调用 beanC() 方法),确保获取的是容器中的单例 Bean。)
-
Spring Boot 中的 @SpringBootConfiguration 和 @Configuration 有什么关系?(答:@SpringBootConfiguration 是 @Configuration 的派生注解(源码中包含 @Configuration),功能完全一致;唯一区别是语义不同:@SpringBootConfiguration 用于标注 Spring Boot 项目的核心启动配置类,@Configuration 用于标注普通 Spring 配置类,便于区分配置类型。)
4. Spring 事件驱动模型(源码级,高阶扩展题)
4.1 面试回答(源码版)
Spring 事件驱动模型基于观察者模式实现,核心作用是解耦组件间的通信(如 A 组件执行完成后,通知 B、C 组件执行后续逻辑,无需 A 直接调用 B、C),底层由「事件(Event)、事件发布者(Publisher)、事件监听器(Listener)」三部分组成,支持同步/异步事件,源码核心依赖 ApplicationEvent 和 ApplicationContext 接口。
一、核心组成(源码+功能)
-
事件(ApplicationEvent)
-
定义:所有 Spring 事件的父类,继承自 EventObject,包含事件源(source)和事件发生时间(timestamp);
-
分类:① 内置事件(Spring 自带,如 ContextRefreshedEvent(容器初始化完成)、ContextClosedEvent(容器关闭));② 自定义事件(继承 ApplicationEvent,按需定义业务事件);
-
源码细节:ApplicationEvent 是抽象类,自定义事件需重写构造方法,传入事件源。
-
-
事件发布者(ApplicationEventPublisher)
-
定义:负责发布事件的接口,核心方法是 publishEvent(Object event);
-
源码关联:ApplicationContext 接口继承 ApplicationEventPublisher,因此 Spring 容器(如 AnnotationConfigApplicationContext)本身就是事件发布者;
-
发布逻辑:调用 publishEvent() 方法后,Spring 会将事件广播给所有注册的事件监听器。
-
-
事件监听器(ApplicationListener)
-
定义:负责监听特定事件的接口,核心方法是 onApplicationEvent(E event),事件发布后会自动执行该方法;
-
实现方式:① 实现 ApplicationListener 接口(指定监听的事件类型);② 标注 @EventListener 注解(无需实现接口,更简洁,推荐);
-
源码关联:Spring 容器启动时,会扫描所有 ApplicationListener 实现类和 @EventListener 标注的方法,注册到容器中。
-
二、事件驱动模型的完整流程(源码逻辑)
-
定义自定义事件(继承 ApplicationEvent),封装事件相关数据;
-
编写事件监听器(实现 ApplicationListener 或标注 @EventListener),定义事件触发后的业务逻辑;
-
通过 ApplicationEventPublisher(或 ApplicationContext)调用 publishEvent() 方法,发布事件;
-
Spring 容器接收事件后,通过 EventMulticaster(事件广播器),将事件广播给所有匹配的监听器;
-
监听器接收事件,执行 onApplicationEvent() 方法(或 @EventListener 标注的方法),完成业务逻辑;
-
(可选)若配置异步事件,监听器会在独立线程中执行,不阻塞事件发布者。
三、关键补充:同步 vs 异步事件
-
同步事件(默认):事件发布者发布事件后,会阻塞等待所有监听器执行完成,再继续执行后续逻辑;优点是简单、有序,缺点是阻塞性能差,适合监听器逻辑简单的场景。
-
异步事件:事件发布者发布事件后,立即继续执行后续逻辑,监听器在独立线程中执行;需通过 @EnableAsync 开启异步支持,在监听器方法上标注 @Async;优点是不阻塞,提升性能,适合监听器逻辑复杂、耗时的场景。
4.2 代码实战(自定义事件+同步/异步监听)
// 1. 自定义事件(继承 ApplicationEvent)
public class OrderCreatedEvent extends ApplicationEvent {
// 事件携带的数据(订单ID)
private Long orderId;
// 构造方法:传入事件源和订单ID
public OrderCreatedEvent(Object source, Long orderId) {
super(source);
this.orderId = orderId;
}
// getter
public Long getOrderId() {
return orderId;
}
}
// 2. 事件发布者(Service 中发布事件)
@Service
public class OrderService {
// 注入事件发布者(ApplicationContext 实现了 ApplicationEventPublisher)
@Autowired
private ApplicationEventPublisher eventPublisher;
// 创建订单,发布事件
public void createOrder(Long orderId) {
// 1. 业务逻辑:创建订单
System.out.println("订单创建成功,订单ID:" + orderId);
// 2. 发布事件
eventPublisher.publishEvent(new OrderCreatedEvent(this, orderId));
System.out.println("订单事件发布完成,继续执行后续逻辑");
}
}
// 3. 事件监听器(方式1:实现 ApplicationListener 接口)
@Component
public class OrderCreatedListener implements ApplicationListener<OrderCreatedEvent> {
// 事件触发后执行
@Override
public void onApplicationEvent(OrderCreatedEvent event) {
Long orderId = event.getOrderId();
System.out.println("监听器1(同步):接收订单创建事件,订单ID:" + orderId + ",执行后续逻辑(如发送短信)");
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 4. 事件监听器(方式2:@EventListener 注解,异步)
@Component
@EnableAsync // 开启异步支持(可放在启动类上)
public class OrderLogListener {
// 标注 @EventListener,指定监听的事件类型
@Async // 异步执行,不阻塞发布者
@EventListener(OrderCreatedEvent.class)
public void handleOrderCreatedEvent(OrderCreatedEvent event) {
Long orderId = event.getOrderId();
System.out.println("监听器2(异步):接收订单创建事件,订单ID:" + orderId + ",执行日志记录逻辑");
// 模拟耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 启动类
@SpringBootApplication
@EnableAsync // 开启异步支持(全局生效)
public class EventTestApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(EventTestApplication.class, args);
OrderService orderService = context.getBean(OrderService.class);
orderService.createOrder(1001L);
}
}
执行日志顺序(同步+异步):
1. 订单创建成功,订单ID:1001 2. 监听器1(同步):接收订单创建事件...(阻塞1秒) 3. 订单事件发布完成,继续执行后续逻辑 4. 监听器2(异步):接收订单创建事件...(独立线程,阻塞2秒,不影响发布者)
4.3 生产踩坑
踩坑10:异步事件未开启 @EnableAsync,导致异步失效,变为同步执行 问题现象:监听器方法标注 @Async,但事件发布后,监听器仍同步执行,阻塞发布者,排查发现未在启动类或监听器类上标注 @EnableAsync。 排查思路:查看启动类和监听器类,确认是否有 @EnableAsync 注解;检查日志,确认监听器执行线程与发布者线程是否一致(同步执行时线程相同,异步执行时线程不同);检查 @Async 注解是否标注在正确的方法上(需标注在监听器方法上,而非类上)。 解决方案:1. 在 Spring Boot 启动类上标注 @EnableAsync,开启全局异步支持;2. 确保 @Async 注解标注在监听器的事件处理方法上(如 handleOrderCreatedEvent 方法);3. 若需自定义异步线程池,可通过 @Async("threadPoolName") 指定线程池,避免使用默认线程池(默认线程池核心线程数少,易出现阻塞)。
踩坑11:事件监听器未被 Spring 扫描到,导致事件发布后无响应 问题现象:自定义事件发布后,监听器未执行任何逻辑,排查发现监听器未被 Spring 容器管理。 原因:1. 监听器类未标注 @Component、@Service 等组件注解,无法被 @ComponentScan 扫描到;2. 监听器所在包未在 Spring 扫描范围内(如未配置 @ComponentScan 或扫描路径不包含监听器包);3. 自定义监听器未手动注册到容器中(非组件方式实现的监听器)。 解决方案:1. 给监听器类标注 @Component 注解,确保被 Spring 扫描;2. 检查 @ComponentScan 配置,确保监听器所在包在扫描范围内;3. 若为非组件方式(如手动创建监听器),通过 ApplicationContext 的 addApplicationListener() 方法手动注册监听器。
踩坑12:事件广播器(EventMulticaster)配置异常,导致事件无法广播 问题现象:事件正常发布,但所有监听器均未接收事件,排查发现事件广播器配置异常或自定义广播器未生效。 原因:1. 自定义了 ApplicationEventMulticaster 实现类,但未正确注入容器,导致 Spring 无法使用该广播器;2. 自定义广播器未重写 multicastEvent() 方法,导致事件无法广播;3. 广播器被手动关闭或销毁。 解决方案:1. 若自定义广播器,需标注 @Component 确保注入容器,且正确重写 multicastEvent() 方法(实现事件广播逻辑);2. 若无需自定义广播器,删除自定义实现,使用 Spring 默认的 SimpleApplicationEventMulticaster;3. 检查广播器相关配置,确保未被手动关闭。
踩坑13:异步事件异常未处理,导致线程泄漏或业务逻辑中断 问题现象:异步监听器执行过程中抛出异常,未被捕获,导致线程异常终止,后续异步任务无法正常执行,甚至出现线程泄漏。 原因:异步事件的执行线程与发布者线程分离,监听器抛出的异常不会被发布者捕获,若未单独处理,会导致线程异常终止,影响线程池稳定性。 解决方案:1. 在异步监听器方法中添加 try-catch 捕获异常,手动处理异常(如记录日志、重试逻辑);2. 自定义 AsyncUncaughtExceptionHandler,全局处理异步事件的未捕获异常;3. 配置线程池的异常处理机制,确保线程异常后能正常回收。
源码片段(核心逻辑)
// 1. 事件发布核心逻辑(ApplicationContext 实现类中的 publishEvent 方法)
// 源码:AbstractApplicationContext.java
@Override
public void publishEvent(Object event) {
publishEvent(event, null);
}
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
Assert.notNull(event, "Event must not be null");
// 1. 包装事件(若传入的不是 ApplicationEvent,自动包装为 PayloadApplicationEvent)
ApplicationEvent applicationEvent = event instanceof ApplicationEvent ? (ApplicationEvent) event : new PayloadApplicationEvent<>(this, event);
if (eventType == null) {
eventType = ResolvableType.forInstance(applicationEvent);
}
// 2. 获取事件广播器,广播事件
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
// 3. 若存在父容器,向父容器广播事件
if (this.parent != null) {
this.parent.publishEvent(event, eventType);
}
}
// 2. 事件广播器核心逻辑(默认 SimpleApplicationEventMulticaster)
// 源码:SimpleApplicationEventMulticaster.java
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : ResolvableType.forInstance(event));
// 1. 获取所有匹配当前事件的监听器
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
// 2. 若配置异步执行,使用线程池执行监听器方法
Executor executor = getTaskExecutor();
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
} else {
// 3. 同步执行监听器方法
invokeListener(listener, event);
}
}
}
// 3. 调用监听器方法(核心逻辑)
protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
// 调用监听器的 onApplicationEvent 方法
doInvokeListener(listener, event);
} catch (Throwable err) {
// 异常处理
errorHandler.handleError(err);
}
} else {
doInvokeListener(listener, event);
}
}
private void doInvokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
try {
// 强转监听器类型,调用事件处理方法
((ApplicationListener<ApplicationEvent>) listener).onApplicationEvent(event);
} catch (ClassCastException ex) {
// 类型不匹配时的异常处理(如监听器监听的事件类型与发布的事件不一致)
if (logger.isTraceEnabled()) {
logger.trace("Listener [" + listener + "] is not of type [" + event.getClass().getName() + "]", ex);
}
}
}
4.4 面试追问
-
Spring 事件驱动模型中,EventMulticaster 的作用是什么?默认实现类是什么?(答:作用:作为事件广播器,负责将发布的事件广播给所有匹配的事件监听器,是事件发布者与监听器之间的桥梁。默认实现类是 SimpleApplicationEventMulticaster,支持同步和异步事件广播,可通过配置线程池实现异步广播。)
-
Spring 内置事件有哪些?分别在什么时机触发?(答:核心内置事件:1. ContextRefreshedEvent:Spring 容器初始化完成(所有 Bean 实例化、依赖注入完成)后触发;2. ContextClosedEvent:Spring 容器关闭(如应用停止)时触发;3. ContextStartedEvent:容器启动(调用 start() 方法)时触发;4. ContextStoppedEvent:容器停止(调用 stop() 方法)时触发;5. RequestHandledEvent:Spring MVC 处理完请求后触发(仅 Web 环境)。)
-
@EventListener 注解和 ApplicationListener 接口相比,有什么优势?(答:1. 无需实现接口,代码简洁,侵入性低;2. 可直接标注在方法上,一个类可定义多个监听器方法,监听不同事件;3. 支持指定事件类型(通过注解参数),无需强转事件类型;4. 支持条件监听(结合 @Conditional 注解),可根据条件决定监听器是否生效;5. 支持异步监听(配合 @Async 注解),无需额外配置。)
-
如何自定义事件广播器(ApplicationEventMulticaster)?(答:1. 实现 ApplicationEventMulticaster 接口,重写 multicastEvent() 方法(核心,实现事件广播逻辑)、addApplicationListener() 方法(添加监听器)、removeApplicationListener() 方法(移除监听器);2. 给自定义广播器标注 @Component,确保被 Spring 注入容器,替代默认的 SimpleApplicationEventMulticaster;3. (可选)配置线程池,实现异步事件广播,提升性能;4. (可选)自定义异常处理器(ErrorHandler),处理监听器执行过程中的异常。)
-
Spring 事件驱动模型和消息队列(如 RabbitMQ、Kafka)的区别是什么?什么时候用 Spring 事件,什么时候用消息队列?(答:区别:1. 范围不同:Spring 事件仅在单个应用内部通信,无法跨应用;消息队列支持跨应用、跨服务通信;2. 可靠性不同:Spring 事件默认同步执行,异常会影响发布者,且无持久化机制(事件发布后若监听器未执行,事件丢失);消息队列支持持久化、重试、死信队列,可靠性更高;3. 性能不同:Spring 事件无网络开销,性能高;消息队列有网络开销,性能取决于队列性能。使用场景:1. 单应用内部组件解耦,且逻辑简单、无需持久化,用 Spring 事件;2. 跨应用、跨服务通信,或需要保证消息可靠性(持久化、重试),用消息队列。
5. Spring 整合 MyBatis 过程(源码级,高阶实战题)
5.1 面试回答(源码版)
Spring 整合 MyBatis 的核心目的是让 MyBatis 组件(SqlSessionFactory、Mapper 接口)被 Spring 容器管理,实现依赖注入和统一生命周期管控,底层通过 Spring 提供的 MyBatis 整合组件(mybatis-spring 依赖),将 MyBatis 的核心对象(SqlSessionFactory、SqlSession)与 Spring 的 IOC、AOP 机制结合,本质是“Spring 接管 MyBatis 的对象创建和管理”,无需手动创建 SqlSession、Mapper 实例。
整合核心依赖:mybatis(MyBatis 核心)、mybatis-spring(Spring 与 MyBatis 整合核心,提供 SqlSessionFactoryBean、MapperScannerConfigurer 等关键组件)、spring-jdbc(Spring 数据库连接相关)、数据库驱动(如 mysql-connector-java)、德鲁伊/ hikari 连接池(可选,推荐)。
一、整合核心逻辑(源码层面)
Spring 整合 MyBatis 的核心是两个关键 Bean:
-
SqlSessionFactoryBean:Spring 提供的 FactoryBean 实现类,核心作用是创建 MyBatis 的 SqlSessionFactory(MyBatis 核心对象,负责创建 SqlSession);源码中通过 afterPropertiesSet() 方法初始化 SqlSessionFactory,将 Spring 管理的 DataSource 注入 MyBatis,同时加载 MyBatis 配置文件、Mapper 映射文件。
-
MapperScannerConfigurer:Spring 提供的 BeanPostProcessor 实现类,核心作用是扫描指定包下的 Mapper 接口,将其注册为 Spring Bean(代理对象),底层通过 MyBatis 的 MapperProxyFactory 生成 Mapper 接口的代理对象,注入到 Service 中使用。
核心流程:Spring 容器启动 → 初始化 DataSource(数据库连接池)→ 初始化 SqlSessionFactoryBean → 生成 SqlSessionFactory → 初始化 MapperScannerConfigurer → 扫描 Mapper 接口并生成代理 Bean → Service 注入 Mapper 代理对象 → 调用 Mapper 方法(底层通过 SqlSession 执行 SQL)。
二、完整整合步骤(XML 配置+注解配置,实战落地)
以下整合分为两种方式(企业常用,覆盖源码细节):XML 配置方式(传统,易理解源码)、Spring Boot 注解配置方式(主流,简化配置)。
方式1:XML 配置方式(Spring 纯 XML 整合,源码视角清晰)
-
步骤1:导入核心依赖(pom.xml)
<!-- MyBatis 核心 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.15</version>
</dependency>
<!-- Spring 与 MyBatis 整合 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<!-- Spring JDBC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.28</version>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.36</version>
<scope>runtime</scope>
</dependency>
<!-- 德鲁伊连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.20</version>
</dependency>
-
步骤2:配置 Spring 核心 XML(applicationContext.xml)
<?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">
<!-- 1. 配置数据源(DataSource),交给 Spring 管理 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_db?useSSL=false&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<!-- 连接池配置(可选) -->
<property name="initialSize" value="5"/>
<property name="maxActive" value="20"/>
</bean>
<!-- 2. 配置 SqlSessionFactoryBean,生成 SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注入数据源(必须,关联 Spring 管理的 DataSource) -->
<property name="dataSource" ref="dataSource"/>
<!-- 配置 MyBatis 核心配置文件路径(可选,也可在此处配置全局参数) -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- 配置 Mapper 映射文件路径(可选,若Mapper接口与映射文件同包,可省略) -->
<property name="mapperLocations" value="classpath:com/xxx/mapper/*.xml"/>
<!-- 配置别名(可选,简化Mapper映射文件中的全类名) -->
<property name="typeAliasesPackage" value="com.xxx.pojo"/>
</bean>
<!-- 3. 配置 MapperScannerConfigurer,扫描 Mapper 接口,生成代理 Bean -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 配置 Mapper 接口所在包(核心,必须配置) -->
<property name="basePackage" value="com.xxx.mapper"/>
<!-- 关联 SqlSessionFactory(可选,若容器中只有一个 SqlSessionFactory,可省略) -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!-- 4. 配置 Service 层 Bean(将 Mapper 注入 Service) -->
<bean id="userService" class="com.xxx.service.impl.UserServiceImpl">
<property name="userMapper" ref="userMapper"/> <!-- userMapper 是扫描生成的代理 Bean -->
</bean>
</beans>
-
步骤3:配置 MyBatis 核心配置文件(mybatis-config.xml,可选)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 全局配置(可选,可在 SqlSessionFactoryBean 中配置) -->
<settings>
<!-- 开启驼峰命名映射(如数据库 user_name → 实体类 userName) -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启日志(可选,便于调试) -->
<setting name="logImpl" value="SLF4J"/>
</settings>
</configuration>
-
步骤4:编写 Mapper 接口、映射文件、实体类、Service
// 实体类(User.java)
public class User {
private Integer id;
private String userName;
private String password;
// getter/setter/toString 省略
}
// Mapper 接口(UserMapper.java,无需实现类,由 Spring 生成代理)
public interface UserMapper {
// 注解方式写 SQL(可选,可替代映射文件)
@Select("select * from user where id = #{id}")
User selectById(Integer id);
// 映射文件方式写 SQL(需在 SqlSessionFactoryBean 中配置映射路径)
int insert(User user);
}
// Mapper 映射文件(UserMapper.xml,与接口同包或配置路径)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.mapper.UserMapper">
<insert id="insert" parameterType="User">
insert into user(user_name, password) values(#{userName}, #{password})
</insert>
</mapper>
// Service 接口(UserService.java)
public interface UserService {
User getUserById(Integer id);
int addUser(User user);
}
// Service 实现类(UserServiceImpl.java)
public class UserServiceImpl implements UserService {
// 注入 Mapper 代理对象(Spring 扫描生成,直接注入)
private UserMapper userMapper;
// Setter 注入(XML 配置中配置)
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public User getUserById(Integer id) {
// 直接调用 Mapper 方法,底层由 Spring 管理 SqlSession
return userMapper.selectById(id);
}
@Override
public int addUser(User user) {
return userMapper.insert(user);
}
}
// 测试类
public class Test {
public static void main(String[] args) {
// 加载 Spring 容器
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 获取 Service Bean
UserService userService = context.getBean("userService", UserService.class);
// 调用方法
User user = userService.getUserById(1);
System.out.println(user);
}
}
方式2:Spring Boot 注解配置方式(主流,简化配置,源码本质一致)
-
步骤1:导入 Spring Boot 整合依赖(pom.xml)
<!-- Spring Boot 父工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<dependencies>
<!-- Spring Boot Web(可选,若有接口需求) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot 整合 MyBatis(核心,自动配置 SqlSessionFactory、MapperScanner) -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.2</version>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 德鲁伊连接池(可选,Spring Boot 自动配置) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.20</version>
</dependency>
</dependencies>
-
步骤2:配置 application.yml(替代 XML 配置,核心配置)
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis_db?useSSL=false&serverTimezone=UTC
username: root
password: 123456
# 德鲁伊连接池配置(可选)
druid:
initial-size: 5
max-active: 20
# MyBatis 配置(替代 mybatis-config.xml)
mybatis:
# 别名包扫描(简化映射文件中的全类名)
type-aliases-package: com.xxx.pojo
# Mapper 映射文件路径(若与接口同包,可省略)
mapper-locations: classpath:com/xxx/mapper/*.xml
# 全局配置(驼峰命名映射)
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl # 日志配置
# 可选:指定 Mapper 扫描包(也可在启动类上用 @MapperScan 注解)
mybatis-plus: # 若用 MyBatis-Plus,可配置此节点,原生 MyBatis 无需
mapper-locations: classpath:com/xxx/mapper/*.xml
-
步骤3:启动类添加注解,开启 Mapper 扫描
// 启动类(核心:@MapperScan 扫描 Mapper 接口,替代 MapperScannerConfigurer)
@SpringBootApplication
@MapperScan("com.xxx.mapper") // 扫描 Mapper 接口所在包,生成代理 Bean
public class MyBatisSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(MyBatisSpringBootApplication.class, args);
}
}
// 补充:也可在 Mapper 接口上标注 @Mapper 注解,无需 @MapperScan(适合少量 Mapper)
@Mapper // 单个 Mapper 接口标注,Spring 会扫描该接口并生成代理
public interface UserMapper {
// 方法省略
}
-
步骤4:编写 Mapper、Service、Controller(与 XML 方式一致,简化配置)
// Service 层(用 @Service 标注,自动注入 Mapper)
@Service
public class UserServiceImpl implements UserService {
// 直接注入 Mapper 代理对象(Spring 自动扫描生成)
@Autowired
private UserMapper userMapper;
@Override
public User getUserById(Integer id) {
return userMapper.selectById(id);
}
}
// Controller 层(可选,用于接口测试)
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User getUser(@PathVariable Integer id) {
return userService.getUserById(id);
}
}
三、整合核心源码片段(关键组件源码解析)
重点解析 Spring 整合 MyBatis 的两个核心组件:SqlSessionFactoryBean、MapperScannerConfigurer,理解底层实现逻辑。
// 1. SqlSessionFactoryBean 核心源码(FactoryBean 实现,生成 SqlSessionFactory)
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
private DataSource dataSource; // 注入 Spring 管理的 DataSource
private Resource configLocation; // MyBatis 核心配置文件路径
private Resource[] mapperLocations; // Mapper 映射文件路径
private String typeAliasesPackage; // 别名包
// 核心方法:InitializingBean 的 afterPropertiesSet(),初始化 SqlSessionFactory
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.dataSource, "Property 'dataSource' is required");
Assert.notNull(this.sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
// 构建 SqlSessionFactory
this.sqlSessionFactory = buildSqlSessionFactory();
}
// 构建 SqlSessionFactory 的核心逻辑
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
// 1. 创建 MyBatis 的 Configuration 对象(MyBatis 核心配置)
final Configuration configuration = new Configuration();
// 2. 加载 MyBatis 配置文件(若有)
if (this.configLocation != null) {
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
xmlConfigBuilder.parse();
}
// 3. 配置别名、驼峰映射等全局参数
if (this.typeAliasesPackage != null) {
configuration.getTypeAliasRegistry().registerAliases(this.typeAliasesPackage);
}
// 4. 配置数据源(将 Spring 的 DataSource 注入 MyBatis)
configuration.setEnvironment(new Environment(
this.environment,
this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
this.dataSource
));
// 5. 加载 Mapper 映射文件
if (this.mapperLocations != null) {
for (Resource mapperLocation : this.mapperLocations) {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), configuration, mapperLocation.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse();
}
}
// 6. 生成并返回 SqlSessionFactory
return this.sqlSessionFactoryBuilder.build(configuration);
}
// FactoryBean 核心方法:返回 SqlSessionFactory 实例(Spring 容器获取该 Bean 时调用)
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
// 省略 setter/getter 方法
}
// 2. MapperScannerConfigurer 核心源码(BeanPostProcessor 实现,扫描 Mapper 接口)
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
private String basePackage; // Mapper 接口所在包
private String sqlSessionFactoryBeanName; // 关联的 SqlSessionFactory Bean 名称
// 核心方法:BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry()
// 在 Spring BeanDefinition 注册完成后,扫描 Mapper 接口,注册为 BeanDefinition
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
// 创建 Mapper 扫描器,指定扫描包
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// 配置扫描器(如是否使用默认过滤器、是否允许接口带注解等)
scanner.setAnnotationClass(Mapper.class); // 扫描带 @Mapper 注解的接口
scanner.setMarkerInterface(Mapper.class); // 扫描实现 Mapper 接口的类(可选)
// 配置 SqlSessionFactory,用于生成 Mapper 代理对象
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
// 开始扫描指定包下的 Mapper 接口
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
// 省略其他方法和属性
}
// 3. Mapper 代理对象生成核心逻辑(MyBatis 的 MapperProxyFactory)
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface; // Mapper 接口
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
// 生成 Mapper 接口的代理对象(Spring 扫描时调用)
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
// 使用 JDK 动态代理,生成代理对象(MyBatis 默认使用 JDK 代理)
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[]{mapperInterface},
mapperProxy
);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
5.3 生产踩坑
踩坑14:Mapper 接口未被扫描到,导致注入失败(NoSuchBeanDefinitionException) 问题现象:Service 中注入 Mapper 时,启动报错“Could not autowire. No beans of 'UserMapper' type found”,排查发现 Mapper 接口未被 Spring 扫描。 原因:1. 未配置 @MapperScan 注解(Spring Boot 方式)或 MapperScannerConfigurer(XML 方式);2. @MapperScan 扫描路径错误,未包含 Mapper 接口所在包;3. Mapper 接口未标注 @Mapper 注解(未使用 @MapperScan 时)。 解决方案:1. Spring Boot 方式:在启动类上添加 @MapperScan("com.xxx.mapper"),确保扫描路径正确;2. XML 方式:配置 MapperScannerConfigurer 的 basePackage 属性,指定 Mapper 包路径;3. 单个 Mapper 接口标注 @Mapper 注解(适合少量 Mapper);4. 检查 Mapper 接口所在包是否在 Spring 组件扫描范围内。
踩坑15:SqlSessionFactory 初始化失败,提示“Could not find resource mybatis-config.xml” 问题现象:Spring 容器启动时,SqlSessionFactoryBean 初始化失败,报错找不到 mybatis-config.xml 文件。 原因:1. mybatis-config.xml 文件路径配置错误(如路径写错、文件未放在 classpath 下);2. 配置了 configLocation 属性,但未创建该文件;3. Mapper 映射文件路径配置错误,导致扫描不到。 解决方案:1. 确认 mybatis-config.xml 文件放在 resources 目录下(classpath 根目录);2. 检查 configLocation 配置(如 classpath:mybatis-config.xml),确保路径正确;3. 若无需 MyBatis 全局配置,删除 configLocation 属性,将全局配置(如驼峰映射)配置在 SqlSessionFactoryBean 或 application.yml 中;4. 检查 Mapper 映射文件路径,确保与配置一致。
踩坑16:数据源注入失败,导致 SqlSessionFactory 无法初始化 问题现象:启动报错“Property 'dataSource' is required”,排查发现 DataSource 未被 Spring 容器管理或注入失败。 原因:1. 未配置 DataSource Bean(XML 方式)或未在 application.yml 中配置数据源(Spring Boot 方式);2. 数据源配置错误(如 URL、用户名、密码错误);3. 连接池依赖缺失,导致 DataSource 无法实例化。 解决方案:1. XML 方式:配置 DataSource Bean,确保属性正确;2. Spring Boot 方式:在 application.yml 中配置 spring.datasource 相关属性;3. 检查数据库驱动、连接池依赖是否导入;4. 测试数据库连接,确保 URL、用户名、密码正确(避免端口错误、数据库未启动等问题)。
踩坑17:Mapper 接口与映射文件不匹配,导致 SQL 执行失败 问题现象:调用 Mapper 方法时,报错“Invalid bound statement (not found)”,提示找不到对应的 SQL 语句。 原因:1. Mapper 映射文件的 namespace 与 Mapper 接口的全类名不一致;2. 映射文件中 SQL 标签的 id 与 Mapper 接口的方法名不一致;3. Mapper 接口方法的参数类型、返回值类型与映射文件中 parameterType、resultType 不匹配;4. Mapper 映射文件未被 SqlSessionFactoryBean 扫描到。 解决方案:1. 确保映射文件的 namespace = Mapper 接口全类名(如 com.xxx.mapper.UserMapper);2. 确保 SQL 标签的 id = Mapper 接口方法名(大小写敏感);3. 匹配参数类型和返回值类型(如实体类别名是否配置);4. 检查 Mapper 映射文件路径,确保在 SqlSessionFactoryBean 的 mapperLocations 配置范围内。
踩坑18:Spring 事务与 MyBatis 整合失败,事务不生效 问题现象:Service 方法标注 @Transactional,但执行 Mapper 方法时,异常发生后无法回滚,事务不生效。 原因:1. 未开启 Spring 事务支持(XML 方式未配置事务管理器,Spring Boot 方式未标注 @EnableTransactionManagement);2. 事务管理器未注入 DataSource(与 MyBatis 数据源不一致);3. @Transactional 注解标注在非 public 方法上(事务注解仅对 public 方法生效);4. 异常被手动 catch,未抛出,导致事务无法感知异常。 解决方案:1. Spring Boot 方式:在启动类上标注 @EnableTransactionManagement,开启事务支持;2. 配置事务管理器(Spring Boot 自动配置 DataSourceTransactionManager,无需手动配置);3. 确保 @Transactional 标注在 public 方法上;4. 异常不要手动 catch,或 catch 后重新抛出(如 throw new RuntimeException(e));5. 检查事务传播机制是否配置正确(避免 propagation = Propagation.NOT_SUPPORTED 等不支持事务的机制)。
5.4 面试追问
-
Spring 整合 MyBatis 时,SqlSessionFactoryBean 的作用是什么?底层如何实现的?(答:作用:将 Spring 管理的 DataSource 注入 MyBatis,加载 MyBatis 配置文件和 Mapper 映射文件,生成 SqlSessionFactory(MyBatis 核心对象),交给 Spring 容器管理。底层实现:实现 FactoryBean 和 InitializingBean 接口,在 afterPropertiesSet() 方法中构建 SqlSessionFactory,通过 getObject() 方法向 Spring 容器提供 SqlSessionFactory 实例;核心是将 Spring 的 DataSource 与 MyBatis 的 Configuration 关联,完成 MyBatis 初始化。)
-
MapperScannerConfigurer 是如何扫描 Mapper 接口并生成代理对象的?(答:1. 实现 BeanDefinitionRegistryPostProcessor 接口,在 postProcessBeanDefinitionRegistry() 方法中创建 ClassPathMapperScanner 扫描器;2. 扫描指定包下的 Mapper 接口(通过 @Mapper 注解或 MarkerInterface 过滤);3. 为每个 Mapper 接口注册 BeanDefinition,指定 Bean 的类型为 MapperFactoryBean(FactoryBean 实现);4. MapperFactoryBean 底层通过 MyBatis 的 MapperProxyFactory 生成 JDK 动态代理对象,注入到 Service 中。)
-
Spring Boot 整合 MyBatis 时,自动配置做了哪些事情?(答:1. 自动配置 DataSource(根据 application.yml 中的数据源配置,生成 DataSource Bean);2. 自动配置 SqlSessionFactoryBean,注入 DataSource,加载 MyBatis 配置(如别名、驼峰映射)和 Mapper 映射文件;3. 自动配置 MapperScannerConfigurer,扫描 @Mapper 注解或 @MapperScan 指定包下的 Mapper 接口;4. 自动配置 DataSourceTransactionManager,支持事务管理;5. 简化配置,无需手动配置 XML,通过 application.yml 即可完成核心配置。)
-
MyBatis 的 Mapper 接口为什么不需要实现类?Spring 是如何让 Mapper 接口生效的?(答:因为 Spring 整合 MyBatis 时,通过 MapperProxyFactory 生成了 Mapper 接口的 JDK 动态代理对象,代理对象底层调用 SqlSession 的方法执行 SQL,因此无需手动实现接口。生效过程:1. MapperScannerConfigurer 扫描 Mapper 接口,注册为 Spring Bean;2. 当 Service 注入 Mapper 时,Spring 从容器中获取 Mapper 的代理对象;3. 调用 Mapper 方法时,代理对象通过 SqlSession 执行对应的 SQL 语句,返回结果。)
-
Spring 整合 MyBatis 时,事务是如何实现的?(答:核心是 Spring 的 DataSourceTransactionManager 事务管理器,结合 MyBatis 的 SpringManagedTransaction 实现。流程:1. 开启 Spring 事务支持(@EnableTransactionManagement);2. 事务管理器注入 Spring 管理的 DataSource(与 MyBatis 共用同一个数据源);3. Service 方法标注 @Transactional,Spring 会通过 AOP 生成代理对象;4. 调用 Mapper 方法时,Spring 会获取 SqlSession,并将其与当前事务绑定;5. 若方法执行正常,事务提交,SqlSession 关闭;若发生异常,事务回滚,SqlSession 回滚并关闭;6. 底层通过 SpringManagedTransaction 管理 MyBatis 的事务,与 Spring 事务同步。)
-
Spring 整合 MyBatis 和 Spring 整合 MyBatis-Plus 有什么区别?(答:1. 依赖不同:MyBatis-Plus 依赖 mybatis-plus-spring-boot-starter,底层包含 MyBatis 和 mybatis-spring 依赖,无需额外导入;2. 配置不同:MyBatis-Plus 扩展了 MyBatis 的配置(如 mybatis-plus.mapper-locations),支持更多特性(如分页、条件构造器);3. 功能不同:MyBatis-Plus 提供了 CRUD 接口封装(BaseMapper),无需编写基础 SQL;支持注解式 SQL(如 @TableName、@TableId),可省略 Mapper 映射文件;4. 底层一致:两者都是通过 SqlSessionFactoryBean 和 MapperScannerConfigurer 实现整合,MyBatis-Plus 是 MyBatis 的增强工具,不改变 MyBatis 核心逻辑。)
更多推荐



所有评论(0)