《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种,按推荐优先级排序

  1. 构造器注入(官方推荐,Spring 4.x+ 重点推荐)

    1. 优点:依赖不可变(用 final 修饰)、强制依赖(必须传入依赖,否则无法实例化)、线程安全、避免循环依赖(构造器注入无法解决循环依赖,会直接报错,提前暴露问题)。

    2. 缺点:依赖过多时,构造方法参数过长,代码不够简洁。

  2. Setter 方法注入

    1. 优点:可选依赖(可通过 @Autowired(required = false) 设置非强制依赖)、可动态修改依赖(通过 Setter 方法重新赋值)。

    2. 缺点:依赖可变,线程不安全(需手动保证线程安全)、无法保证依赖一定被注入(可能出现空指针)。

  3. 字段注入(@Autowired,最常用但不推荐)

    1. 优点:代码简洁,无需写构造器/Setter 方法,开发效率高。

    2. 缺点:难以单元测试(无法通过构造器注入 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 面试追问

  1. 为什么官方推荐构造器注入?(答:强制依赖、依赖不可变、线程安全、避免循环依赖隐患、便于单元测试)

  2. @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 面试追问

  1. ApplicationContext 启动时,预加载单例 Bean 的好处和弊端?(答:好处:提前初始化,避免第一次调用时的性能损耗;弊端:容器启动速度变慢,内存占用增加)

  2. 如何让 ApplicationContext 中的单例 Bean 实现懒加载?(答:在 Bean 上添加 @Lazy 注解,或在 @Bean 注解中设置 lazyInit = true)

5. Spring Bean 的生命周期?

5.1 标准流程(面试必背 11 步,结合源码逻辑)

  1. 加载 Bean 定义:Spring 容器扫描注解/配置,将 Bean 信息封装为 BeanDefinition,存入 BeanDefinitionRegistry。

  2. 实例化(Instantiation):容器通过构造方法创建 Bean 实例(无参/有参构造,优先无参)。

  3. 填充属性(DI 依赖注入):容器将依赖的 Bean 注入到当前 Bean 的字段/Setter 方法中。

  4. 执行 Aware 接口:如果 Bean 实现了 BeanNameAware、BeanFactoryAware、ApplicationContextAware 等接口,容器会调用对应的方法,注入 Bean 名称、BeanFactory、ApplicationContext 等信息。

  5. 执行 BeanPostProcessor#postProcessBeforeInitialization(初始化前增强):所有 Bean 初始化前都会执行该方法,可对 Bean 进行动态修改(如添加代理)。

  6. 执行 @PostConstruct 初始化方法:JSR-250 注解,在 Bean 初始化前执行(由 Spring 容器调用)。

  7. 执行 InitializingBean#afterPropertiesSet:如果 Bean 实现了该接口,调用该方法,完成自定义初始化逻辑。

  8. 执行自定义 init-method:通过 XML 配置(init-method)或 @Bean(initMethod = "xxx") 指定的初始化方法。

  9. 执行 BeanPostProcessor#postProcessAfterInitialization(初始化后增强):所有 Bean 初始化后执行,常用作动态代理(如 AOP 代理)。

  10. Bean 就绪(使用中):Bean 进入容器,供其他 Bean 调用或外部使用。

  11. 销毁(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 面试追问

  1. BeanPostProcessor 的作用是什么?有哪些常用实现类?(答:作用是对 Bean 进行初始化前后的增强,无需修改 Bean 本身;常用实现类:AutowiredAnnotationBeanPostProcessor(处理 @Autowired 注入)、AnnotationAwareAspectJAutoProxyCreator(AOP 代理创建))

  2. @PostConstruct 和 init-method 的执行顺序?为什么?(答:@PostConstruct 先执行,再执行 init-method;因为 @PostConstruct 是 JSR-250 注解,由 Spring 的 CommonAnnotationBeanPostProcessor 处理,在 BeanPostProcessor 初始化前阶段执行,而 init-method 是在 InitializingBean 之后执行)

  3. Bean 的销毁时机是什么?(答:单例 Bean:容器关闭时销毁;多例 Bean:容器不负责销毁,由开发者手动管理,或由 GC 回收)

6. Bean 的作用域有哪些?singleton 和 prototype 区别?

6.1 5 种作用域(Spring 5.x+ 支持)

  1. singleton(默认):单例,容器中只有一个 Bean 实例,所有请求都共享该实例。

  2. prototype:多例,每次调用 getBean() 或注入时,都会新建一个 Bean 实例。

  3. request:一次 HTTP 请求,创建一个 Bean 实例,请求结束后销毁(仅适用于 Spring MVC 环境)。

  4. session:一个 HTTP 会话,创建一个 Bean 实例,会话结束后销毁(仅适用于 Spring MVC 环境)。

  5. 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 面试追问

  1. singleton Bean 为什么线程不安全?如何保证 singleton Bean 的线程安全?(答:因为 singleton Bean 是共享实例,多线程同时修改其成员变量会出现并发问题;解决方案:1. 避免使用成员变量,用局部变量;2. 使用 ThreadLocal 存储线程私有数据;3. 对共享资源加锁(synchronized);4. 将有状态 Bean 改为 prototype)

  2. @Lookup 注解的作用是什么?原理是什么?(答:作用是在 singleton Bean 中获取 prototype Bean 的新实例;原理:Spring 会动态生成该抽象方法的实现,每次调用都会通过 BeanFactory 获取新的 prototype 实例)

  3. request 作用域的 Bean,为什么需要设置 proxyMode?(答:因为 singleton Bean 的生命周期比 request 长,直接注入会导致 request Bean 过期,设置 proxyMode 后,注入的是代理对象,每次调用时才会获取当前 request 对应的 Bean 实例)

二、AOP 相关

1. AOP 是什么?核心概念(切面、通知、切点、连接点、目标对象)

1.1 定义

AOP(Aspect-Oriented Programming,面向切面编程):在不修改业务代码的前提下,通过动态代理技术,统一增强横切逻辑(日志、事务、权限、限流、异常处理等),实现“业务逻辑”与“横切逻辑”的解耦。

1.2 核心 5 大概念(面试必背,结合实例理解)

  1. 连接点(JoinPoint):程序执行过程中可被拦截的点,Spring AOP 中仅支持方法连接点(即所有方法都可能是连接点)。

  2. 切点(Pointcut):真正要拦截的连接点,通过切点表达式(如 execution、@annotation)定义拦截规则。例如:拦截所有 Service 层的方法。

  3. 通知(Advice):拦截到切点后执行的增强逻辑,分为 5 种类型(前置、后置、环绕、异常、最终)。

  4. 切面(Aspect):切点(Pointcut) + 通知(Advice)的组合,是 AOP 的核心,负责定义“拦截哪些方法”和“拦截后做什么”。

  5. 目标对象(Target):被代理的原始业务对象(如 UserService 实例),AOP 增强的是目标对象的方法。

补充:织入(Weaving)

织入是将切面逻辑融入到目标对象方法中的过程,Spring AOP 采用动态织入(运行时通过动态代理实现),而 AspectJ 采用静态织入(编译期织入)。

1.3 生产踩坑

踩坑8:切点表达式编写错误,导致切面未生效或误拦截无关方法。 案例:想拦截 com.example.service 包下的所有方法,表达式写成 execution(* com.example.service.*(..)),漏写一个“..”,导致只拦截 service 包下的一级方法,子包下的方法未被拦截。 正确表达式:execution(* com.example.service..*(..))(“..”表示包含子包)。

1.4 面试追问

  1. AOP 和 OOP 的区别?(答:OOP 是面向对象,关注“对象”和“继承/封装/多态”,解决业务逻辑的纵向复用;AOP 是面向切面,关注“横切逻辑”,解决横切逻辑的横向复用,弥补 OOP 的不足)

  2. 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 会根据目标对象是否有接口,自动选择代理方式:

  1. 目标对象有接口:使用 JDK 动态代理(默认),生成接口的代理对象,代理对象实现目标接口,并重写目标方法,植入增强逻辑。

  2. 目标对象无接口:使用 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 面试追问

  1. JDK 动态代理为什么只能代理接口?(答:JDK 动态代理生成的代理类,默认继承了 Proxy 类,而 Java 不支持多继承,因此无法继承目标类,只能实现目标接口,从而代理接口方法)

  2. Spring AOP 中,如何强制使用 CGLIB 代理?(答:1. 配置文件中设置 <aop:aspectj-autoproxy proxy-target-class="true"/>;2. Spring Boot 中,配置 spring.aop.proxy-target-class=true;3. 目标类没有实现任何接口)

  3. 动态代理的核心思想是什么?(答:不修改目标对象的代码,通过生成代理对象,拦截目标方法的调用,植入增强逻辑,实现解耦)

3. 通知类型有哪些?@Before / @After / @Around 等执行顺序

3.1 5 种通知类型(按执行顺序排序)

  1. @Around(环绕通知):最强通知,包围目标方法,可在目标方法执行前、执行后、异常时、最终执行自定义逻辑,还可控制目标方法是否执行(通过 ProceedingJoinPoint.proceed())。

  2. @Before(前置通知):目标方法执行前执行,无法阻止目标方法执行(除非抛出异常)。

  3. @AfterReturning(返回后通知):目标方法正常执行完成后执行,可获取目标方法的返回值,异常时不执行。

  4. @AfterThrowing(异常后通知):目标方法执行抛出异常时执行,可获取异常信息,正常执行时不执行。

  5. @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 面试追问

  1. @Around 通知和其他通知的区别?(答:1. @Around 可控制目标方法是否执行,其他通知不能;2. @Around 可获取目标方法的返回值和异常,其他通知只能获取其中一种;3. @Around 执行顺序最早,结束顺序最晚,包围整个目标方法)

  2. JoinPoint 和 ProceedingJoinPoint 的区别?(答:ProceedingJoinPoint 继承自 JoinPoint,仅在 @Around 通知中可用,多了 proceed() 方法,用于执行目标方法;JoinPoint 可在其他所有通知中使用,用于获取方法信息、参数等)

  3. 如何在切面中获取请求参数、请求地址等 Web 相关信息?(答:通过 RequestContextHolder 获取当前请求的 HttpServletRequest 对象,进而获取请求信息)

4. Spring AOP 为什么不能拦截 static/private 方法?

4.1 根本原因(面试必背)

Spring AOP 基于动态代理实现,而动态代理的核心是“重写目标方法”,static/private 方法无法被重写,因此无法拦截:

  1. private 方法:访问权限为私有,动态代理类(JDK 代理/ CGLIB 代理)无法访问和重写目标类的 private 方法,因此无法植入增强逻辑。

  2. static 方法:静态方法属于类,不属于实例,动态代理是针对实例的代理(生成实例的代理对象),无法代理静态方法;且 static 方法无法被重写(子类可继承,但不能重写,只能隐藏)。

补充说明

即使使用 AspectJ 静态织入,也只能拦截 static 方法(通过编译期织入),但无法拦截 private 方法;Spring AOP 不支持 AspectJ 的静态织入(默认动态代理),因此 static/private 方法都无法拦截。

4.2 生产踩坑

踩坑12:将需要拦截的日志、权限逻辑写在 static 方法中,配置切面后发现未生效,排查后才知道 Spring AOP 无法拦截 static 方法。 解决方案:将 static 方法改为实例方法(public);若必须用 static 方法,改用 AspectJ 静态织入,或手动在 static 方法中调用增强逻辑。

4.3 面试追问

  1. 除了 static/private 方法,Spring AOP 还不能拦截哪些方法?(答:final 方法、构造方法、静态代码块;final 方法无法被重写,构造方法和静态代码块在实例化/类加载时执行,动态代理无法拦截)

  2. 如何实现对 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:老项目混合使用编程式和声明式事务,出现事务未提交、数据库连接泄漏,导致应用卡顿、数据不一致。

排查思路

  1. 查看应用日志,确认是否存在多个事务管理器(如同时配置了编程式事务管理器和声明式事务管理器);

  2. 检查编程式事务代码,确认是否手动获取了数据库连接,且未在异常时释放;

  3. 检查嵌套事务场景,确认是否存在事务未正常归还连接的情况;

  4. 统一事务管理器,避免混合使用,优先使用声明式事务,复杂场景单独使用编程式事务。

(5)、高频面试追问(必背)

Q1:编程式事务和声明式事务底层是否共用一套机制?

A1:是。二者底层都依赖 PlatformTransactionManager(事务管理器)和 TransactionSynchronizationManager(线程绑定资源),只是事务触发方式不同:编程式手动调用API,声明式通过AOP拦截自动触发。

Q2:什么时候优先使用编程式事务?

A2:3种场景:① 事务粒度小于方法级别(如循环内部分片段需要事务);② 动态条件事务(如根据业务逻辑结果决定是否回滚);③ 跨多个方法的复杂事务流程(无法用声明式事务统一控制)。


2. @Transactional 工作原理?

(1)、核心答案(面试直接说)

@Transactional 的本质是「Spring AOP动态代理 + 事务拦截器 + 线程绑定资源」,通过AOP拦截目标方法,由事务拦截器统一控制事务的开启、提交、回滚,并用ThreadLocal绑定数据库连接,保证事务内连接唯一。

(2)、完整执行流程(面试满分版)

  1. 容器启动阶段

    1. Spring自动配置类 TransactionAutoConfiguration 生效,初始化事务管理器 PlatformTransactionManager

    2. Spring扫描所有类上、方法上的 @Transactional 注解,标记需要被事务增强的方法;

    3. 根据目标类是否实现接口,选择动态代理方式:实现接口用JDK动态代理,未实现接口用CGLIB代理,生成代理对象。

  2. 方法调用阶段

    1. 外部调用目标方法时,实际调用的是代理对象的方法,而非原始对象;

    2. 代理对象触发 TransactionInterceptor(事务拦截器)的 invoke() 方法,开始事务控制。

  3. 获取事务阶段

    1. 事务拦截器调用 PlatformTransactionManager#getTransaction() 方法;

    2. 根据 @Transactional 配置的传播机制,判断当前是否需要新建事务、加入已有事务或挂起事务。

  4. 绑定线程资源阶段

    1. 通过 TransactionSynchronizationManager 类,使用ThreadLocal将数据库连接(Connection)与当前线程绑定;

    2. 保证整个事务执行过程中,所有数据库操作都使用同一个连接,确保事务原子性。

  5. 执行目标方法阶段:调用原始对象的目标方法,执行核心业务逻辑。

  6. 异常判断阶段

    1. 如果方法抛出 RuntimeExceptionError,事务拦截器触发事务回滚;

    2. 如果抛出受检异常(如IOException、SQLException),默认不回滚(需通过 rollbackFor 配置指定回滚异常)。

  7. 事务提交/回滚阶段

    1. 方法正常执行无异常:事务拦截器调用 PlatformTransactionManager#commit() 提交事务;

    2. 方法抛出异常:调用 PlatformTransactionManager#rollback() 回滚事务。

  8. 资源清理阶段:解除ThreadLocal与数据库连接的绑定,将连接归还到连接池,释放资源。

总结:

  1. Spring 启动时,通过 AnnotationTransactionAttributeSource 解析所有带有 @Transactional 注解的方法,获取事务属性(传播机制、隔离级别、回滚规则等)。

  2. 通过 AOP 动态代理,为目标对象生成代理对象,代理对象会拦截 @Transactional 注解的方法。

  3. 方法执行前:代理对象调用 PlatformTransactionManager 的 getTransaction() 方法,根据事务属性开启事务,生成 TransactionStatus 对象(事务状态)。

  4. 执行目标方法:若目标方法正常执行完成,代理对象调用 PlatformTransactionManager 的 commit() 方法,提交事务。

  5. 若目标方法抛出异常,代理对象根据 @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();
    }
}

排查思路

  1. 查看业务日志,确认方法内是否有异常抛出,以及异常是否被捕获;

  2. 打开Spring事务日志(配置logging.level.org.springframework.transaction=DEBUG),查看事务是否触发回滚;

  3. 修改代码:要么删除try-catch,让异常自动抛出;要么在catch块中重新抛出异常(throw new RuntimeException(e));

  4. 确认 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 传播机制,导致数据库连接池耗尽,应用假死,接口响应超时。

排查思路

  1. 通过监控工具(如Prometheus、Druid)查看数据库连接池活跃连接数,发现连接数瞬间拉满;

  2. 全局搜索代码中 REQUIRES_NEW 的使用场景,发现大量非核心方法(日志、通知)滥用 REQUIRES_NEW;

  3. 分析业务场景:日志、通知等非核心方法,无需独立事务,改为 REQUIRED 传播机制;

  4. 优化:非核心的日志、通知操作,可改为异步执行(如用@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,通过以下两种机制解决幻读:

  1. MVCC(多版本并发控制):普通查询(快照读)时,读取的是数据的历史版本,不会读到其他事务新增的“幻影”数据;

  2. 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. 查看库存扣减代码,发现未加行锁,仅靠事务隔离级别控制并发;

  2. 分析并发流程:两个事务同时查询库存(快照读,都得到1),同时扣减,导致超卖;

  3. 解决方案:

    1. 方案1:业务允许时,将隔离级别改为 READ_COMMITTED,配合乐观锁(如版本号);

    2. 方案2:在查询库存时加行锁(select ... for update),强制使用当前读,防止幻读;

    3. 方案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 注解,线上运行时,部分数据导入失败,但事务未回滚,导致数据不一致(部分导入成功,部分失败)。

排查思路

  1. 查看方法修饰符:发现是 private 方法,无法被 Spring 代理;

  2. 查看日志:无事务相关日志(如“Creating new transaction”“Rolling back transaction”),确认事务未被触发;

  3. 修改方案:将 private 方法改为 public 方法,或通过注入自身代理调用该方法;

  4. 验证:修改后,异常时事务正常回滚,数据一致性得到保证。

(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动态代理的工作机制,彻底搞懂同类调用失效的本质,面试时能讲清底层逻辑,加分项!

  1. 代理对象生成逻辑:Spring容器启动时,会为带有@Transactional注解的类生成动态代理对象(JDK或CGLIB),代理对象会封装事务拦截器(TransactionInterceptor),负责事务的开启、提交、回滚。

  2. this关键字的指向问题:同类内部方法调用时,使用this调用的是当前类的原始对象(target object),而非Spring容器生成的代理对象(proxy object)。原始对象中没有事务拦截器的增强逻辑,因此事务无法被触发。

  3. 拦截器触发条件:事务拦截器只有在“外部调用代理对象的方法”时才会生效,内部this调用跳过了代理对象,自然无法触发事务拦截器,事务也就失效了。

简单类比:代理对象相当于“事务开关”,外部调用代理对象的方法,开关会自动触发(开启事务);而内部this调用直接绕过开关,直接执行原始方法,事务自然不生效。

(3)、4种解决方案(按推荐度排序,企业实战常用)

方案1:注入自身代理对象(推荐,最稳定、易维护)

核心思路:在当前类中注入自身的代理对象,通过代理对象调用内部事务方法,触发事务拦截器。需配合@EnableAspectJAutoProxy(exposeProxy = true)开启代理暴露。

步骤:

  1. 在Spring Boot启动类或配置类上添加注解,暴露代理对象:

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露代理对象,关键配置
public class SpringTransactionApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringTransactionApplication.class, args);
    }
}
  1. 在当前类中注入自身代理对象,调用内部事务方法:

@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调用也能触发事务。

实现步骤:

  1. 引入AspectJ相关依赖(spring-aspects、aspectjweaver);

  2. 配置AspectJ织入(如使用@EnableAspectJAutoProxy,或通过XML配置织入器);

  3. 正常使用@Transactional注解,this调用也会生效。

缺点:配置复杂,需要修改编译流程,企业开发中极少使用,仅适用于特殊场景(如无法修改代码结构、必须使用private方法做事务)。

(4)、生产踩坑案例(实战重点)

踩坑场景:电商订单支付流程中,开发人员在OrderService的payOrder(无事务)方法中,用this调用了doPay(加@Transactional)方法,线上出现支付成功但订单状态未更新,且事务未回滚的问题(因支付接口异常,doPay方法未回滚)。

排查思路

  1. 查看代码调用关系:发现是同类内部this调用doPay方法,跳过了代理对象;

  2. 查看事务日志:无“Creating new transaction”日志,确认事务未触发;

  3. 修改方案:采用方案1(注入自身代理对象),将this.doPay()改为orderService.doPay();

  4. 验证:修改后,支付异常时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(前端控制器),负责统一调度,将请求分发到对应组件,执行流程如下(按顺序):

  1. 客户端发送 HTTP 请求(如 GET/POST),请求首先到达前端控制器 DispatcherServlet(所有请求的入口)。

  2. DispatcherServlet 调用 HandlerMapping(处理器映射器),根据请求 URL、请求方法,匹配对应的 Handler(控制器方法,如 @RequestMapping 标注的方法),并返回 HandlerExecutionChain(包含 Handler 和拦截器)。

  3. DispatcherServlet 调用HandlerAdapter(处理器适配器),适配不同类型的 Handler(如注解式 Controller、XML 配置的 Controller),执行 Handler 方法。

  4. Handler 方法执行完成后,返回 ModelAndView(包含模型数据 Model 和视图名称 View)。

  5. DispatcherServlet 调用 ViewResolver(视图解析器),根据 ModelAndView 中的视图名称,解析出具体的 View 实例(如 JSP、Thymeleaf 视图)。

  6. View 实例接收 Model 中的数据,进行视图渲染(将模型数据填充到视图中)。

  7. 视图渲染完成后,将渲染结果(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 面试追问

  1. DispatcherServlet 的核心作用是什么?它是如何接收请求的?(答:核心作用:统一接收所有 HTTP 请求,调度其他组件(HandlerMapping、HandlerAdapter 等),完成请求处理和响应。接收请求:Tomcat 启动时,DispatcherServlet 作为 Servlet 被初始化,配置的 url-pattern(默认 /)会拦截所有请求,请求到达后,由其 doDispatch() 方法进行后续调度。)

  2. HandlerMapping 和 HandlerAdapter 的区别是什么?为什么需要 HandlerAdapter?(答:区别:HandlerMapping 负责“找 Handler”(根据请求匹配控制器方法),HandlerAdapter 负责“执行 Handler”(适配不同类型的 Handler,调用其方法)。需要 HandlerAdapter 的原因:Handler 类型多样(注解式、XML 配置式、实现 Controller 接口式),不同 Handler 的执行方式不同,HandlerAdapter 统一适配,让 DispatcherServlet 无需关注具体 Handler 的执行细节,符合开闭原则。)

  3. Spring MVC 如何处理 JSON 响应?核心注解是什么?(答:通过 @ResponseBody 注解(标注在方法或类上),表示方法返回值直接作为响应体,不进行视图渲染;Spring MVC 会通过 HttpMessageConverter(如 MappingJackson2HttpMessageConverter)将返回值(如 List、实体类)转换为 JSON 格式。补充:@RestController = @Controller + @ResponseBody,类上标注后,所有方法均返回 JSON。)

  4. 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 注解,具体区别如下:

  1. @Controller:传统控制器注解,用于返回视图(如 JSP、Thymeleaf);若要返回 JSON,需在方法上单独添加 @ResponseBody 注解。

  2. @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 面试追问

  1. @ResponseBody 的作用是什么?底层原理是什么?(答:作用:将方法返回值转换为 HTTP 响应体(如 JSON、XML),不进行视图渲染。底层原理:Spring MVC 通过 HttpMessageConverter 接口实现转换,默认使用 MappingJackson2HttpMessageConverter 将对象转换为 JSON(需引入 jackson-databind 依赖),可自定义 HttpMessageConverter 扩展转换格式。)

  2. 前后端分离项目中,为什么优先使用 @RestController?(答:前后端分离项目中,前端负责页面渲染,后端仅需提供 JSON 接口,无需返回视图;@RestController 可省略所有方法上的 @ResponseBody,简化代码,提升开发效率,避免遗漏 @ResponseBody 导致的报错。)

  3. @Controller 可以和 @ResponseBody 一起使用吗?有什么场景?(答:可以。场景:控制器类中,部分方法返回视图(如后台管理系统的页面),部分方法返回 JSON(如接口调用),此时类上标注 @Controller,JSON 方法上单独添加 @ResponseBody,兼顾视图和接口需求。)

3. Spring Boot 自动配置原理?(核心高频,大厂必问)

3.1 面试回答(资深版)

Spring Boot 自动配置的核心是「约定优于配置」,通过注解和 SPI 机制,自动加载依赖的配置,无需手动编写 XML 或 Java 配置,底层核心是 @EnableAutoConfiguration 注解,具体原理如下:

  1. 核心注解触发:@SpringBootApplication 注解中包含 @EnableAutoConfiguration,该注解是自动配置的入口,用于开启自动配置功能。

  2. SPI 机制扫描:@EnableAutoConfiguration 注解通过 @Import(AutoConfigurationImportSelector.class),加载 AutoConfigurationImportSelector 类,该类会扫描 classpath:/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件(Spring Boot 2.7+ 版本),该文件中包含所有自动配置类的全路径(如 RedisAutoConfiguration、DataSourceAutoConfiguration)。

  3. 条件注解筛选:每个自动配置类(如 RedisAutoConfiguration)都包含条件注解(如 @ConditionalOnClass、@ConditionalOnMissingBean),Spring 会根据当前项目的依赖、配置,筛选出符合条件的自动配置类,注入到 Spring 容器中。

  4. 配置绑定:自动配置类会读取 application.yml/application.properties 中的配置(如 spring.redis.host),通过 @ConfigurationProperties 注解将配置绑定到对应的实体类(如 RedisProperties),实现配置动态调整。

  5. 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&lt;String, Object&gt; 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 面试追问

  1. @EnableAutoConfiguration 注解的作用是什么?它和 @Import 有什么关系?(答:作用:开启 Spring Boot 自动配置功能,触发自动配置类的扫描和加载。关系:@EnableAutoConfiguration 内部通过 @Import(AutoConfigurationImportSelector.class),导入 AutoConfigurationImportSelector 类,该类负责扫描和加载自动配置类,本质上是通过 @Import 实现配置类的导入。)

  2. Spring Boot 自动配置中的条件注解有哪些?分别作用是什么?(答:常用条件注解:1. @ConditionalOnClass:当类路径下存在指定类时,配置类生效;2. @ConditionalOnMissingClass:当类路径下不存在指定类时,配置类生效;3. @ConditionalOnBean:当容器中存在指定 Bean 时,配置类生效;4. @ConditionalOnMissingBean:当容器中不存在指定 Bean 时,配置类生效(允许自定义 Bean 覆盖自动配置);5. @ConditionalOnProperty:当配置文件中存在指定属性(且值匹配)时,配置类生效(如 @ConditionalOnProperty(prefix = "spring.redis", name = "enabled", havingValue = "true"))。)

  3. 如何禁用某个自动配置类?(答:两种方式:1. 在 @SpringBootApplication 注解中排除(如 @SpringBootApplication(exclude = RedisAutoConfiguration.class));2. 在 application.yml 中配置(spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration);3. 针对特定环境,使用 @Profile 注解,仅在指定环境下禁用。)

  4. 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+ 版本,底层无其他额外注解),具体组成如下:

  1. @SpringBootConfiguration:本质是 @Configuration 注解的派生注解,标识当前类是一个 Spring 配置类,允许在类中使用 @Bean 注解注入 Bean。

  2. @EnableAutoConfiguration:核心注解,开启 Spring Boot 自动配置功能(前文已详解,负责扫描和加载自动配置类)。

  3. @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 面试追问

  1. @SpringBootConfiguration 和 @Configuration 的区别是什么?(答:本质上无区别,@SpringBootConfiguration 是 @Configuration 的派生注解(源码:@Configuration + @Documented),功能完全一致;唯一区别是语义不同:@SpringBootConfiguration 用于标注 Spring Boot 项目的核心配置类(如启动类),@Configuration 用于标注普通 Spring 配置类,便于区分配置类型。)

  2. @ComponentScan 除了扫描 @Component 及其派生注解,还会扫描哪些注解?(答:还会扫描 @Controller、@Service、@Repository(三者都是 @Component 的派生注解)、@RestController、@Configuration、@Bean 等注解;本质上是扫描所有被 Spring 识别为“组件”的类,只要类上有 Spring 可识别的组件注解,都会被扫描并注入容器。)

  3. 如果不使用 @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)

  1. 执行启动类 main 方法,调用 SpringApplication.run(启动类.class, args),初始化 SpringApplication 实例。

  2. SpringApplication 初始化时,会完成 3 件事:① 推断应用类型(Web 应用/非 Web 应用);② 加载所有初始化器(ApplicationContextInitializer);③ 加载所有监听器(ApplicationListener)。

阶段2:容器初始化(创建并初始化 ApplicationContext)

  1. 调用 SpringApplication 的 run() 方法,创建 ApplicationContext(Web 应用默认是 AnnotationConfigServletWebServerApplicationContext)。

  2. 初始化 ApplicationContext:① 执行初始化器(ApplicationContextInitializer),修改容器配置;② 注册监听器(ApplicationListener);③ 加载 Bean 定义(扫描 @Component 注解、自动配置类);④ 完成 Bean 的实例化、依赖注入和初始化(IOC 容器初始化)。

阶段3:启动完成(启动嵌入式服务器 + 发布事件)

  1. 初始化嵌入式服务器(如 Tomcat、Jetty):Spring Boot 自动配置会根据依赖(如 spring-boot-starter-web),自动注入 TomcatServletWebServerFactory,创建 Tomcat 实例并启动,绑定端口(默认 8080)。

  2. 发布容器启动完成事件(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 面试追问

  1. Spring Boot 启动时,嵌入式服务器(Tomcat)是如何被启动的?(答:1. 引入 spring-boot-starter-web 依赖时,会自动引入 tomcat-spring-boot-starter 依赖;2. 自动配置类 TomcatServletWebServerFactory 被加载,注入到容器中;3. ApplicationContext 初始化完成后,会调用 WebServerFactory 的 getWebServer() 方法,创建 Tomcat 实例;4. 配置 Tomcat 端口、上下文路径等参数,调用 Tomcat 的 start() 方法启动服务器。)

  2. SpringApplication 初始化时,如何推断应用类型?(答:通过类路径下是否存在特定类来推断:① 若存在 Servlet.class、Tomcat.class 等,推断为 Web 应用(Servlet Web 应用);② 若存在 ReactiveWebServerFactory.class,推断为响应式 Web 应用;③ 若均不存在,推断为非 Web 应用。)

  3. Spring Boot 启动失败的常见原因有哪些?(答:1. 端口被占用;2. 依赖冲突(如不同版本的 Spring 依赖冲突);3. 自动配置类冲突(如自定义 Bean 与自动配置 Bean 冲突);4. 组件扫描范围错误(组件未被扫描到);5. 配置文件错误(如语法错误、参数错误);6. 依赖缺失(如引入 starter 但未引入对应依赖)。)

  4. 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 个步骤,可直接复用:

  1. 创建 Maven 项目:命名遵循 Spring Boot 规范(官方 starter 命名:spring-boot-starter-xxx,自定义 starter 命名:xxx-spring-boot-starter,避免与官方冲突)。

  2. 引入核心依赖:引入 spring-boot-starter(基础依赖,提供自动配置支持)、spring-boot-autoconfigure(自动配置核心依赖)、spring-boot-configuration-processor(可选,用于生成配置元数据,方便 IDE 提示)。

  3. 编写配置绑定实体类:通过 @ConfigurationProperties 注解,绑定 application.yml 中的自定义配置(如 xxx.xxx 参数)。

  4. 编写自动配置类:通过 @Configuration、@Conditional 等注解,编写自动配置逻辑,注入所需 Bean,实现“约定优于配置”。

  5. 配置 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&gt;
    &lt;/dependency&gt;
    <!-- 自动配置核心依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
        <version>2.7.0</version>
    </dependency&gt;
    <!-- 生成配置元数据(可选,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 面试追问

  1. 自定义 starter 为什么要遵循命名规范?(答:1. 区分官方 starter 和自定义 starter,避免命名冲突(官方 starter 是 spring-boot-starter-xxx,自定义是 xxx-spring-boot-starter);2. 符合开发者使用习惯,便于其他开发者识别和引入;3. 避免 Spring Boot 自动配置时,误识别为官方 starter,导致配置冲突。)

  2. spring-boot-configuration-processor 依赖的作用是什么?如果不引入会有什么影响?(答:作用:生成配置元数据(META-INF/spring-configuration-metadata.json),让 IDE(如 IDEA)在 application.yml 中编写自定义配置时,提供参数提示(如 my.log.enabled、my.log.level)。不引入的影响:无功能影响,仅 IDE 无配置提示,开发者需手动记忆配置参数,容易出现拼写错误。)

  3. 如何让自定义 starter 支持条件配置?(答:通过 Spring Boot 的条件注解实现,如 @ConditionalOnProperty(根据配置参数生效)、@ConditionalOnClass(根据依赖生效)、@ConditionalOnMissingBean(避免与自定义 Bean 冲突);在自动配置类上添加对应条件注解,实现“按需生效”,提升 starter 的灵活性。)

  4. 自定义 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 能解决的循环依赖场景

  1. Bean 作用域:必须是 单例(多例 Bean 每次获取都新建,无法缓存,无法解决循环依赖);

  2. 注入方式:Setter 注入(推荐)或字段注入(@Autowired),构造器注入无法解决(初始化顺序冲突)。

(2)、三级缓存核心定义(源码核心属性)

Spring 源码中,三级缓存定义在 DefaultSingletonBeanRegistry 类中,本质是三个 Map,作用是缓存不同初始化阶段的单例 Bean:

  1. 一级缓存(singletonObjects):key=BeanName,value=「初始化完成的单例 Bean」(完全就绪,可直接使用);

  2. 二级缓存(earlySingletonObjects):key=BeanName,value=「提前暴露的未初始化完成的单例 Bean」(仅实例化,未完成依赖注入和初始化,无代理);

  3. 三级缓存(singletonFactories):key=BeanName,value=「Bean 工厂对象(ObjectFactory)」,用于提前暴露 Bean 实例,可生成代理对象(解决 AOP 代理与循环依赖的冲突)。

(3)、三级缓存解决循环依赖的完整流程(以 A 依赖 B、B 依赖 A 为例)

  1. 容器启动,开始初始化 A Bean:调用 doCreateBean() 方法,先实例化 A(通过构造方法创建 A 的原始实例),此时 A 未完成依赖注入和初始化;

  2. 将 A 的实例封装为 ObjectFactory(工厂对象),存入三级缓存(singletonFactories),目的是“提前暴露 A 的实例”,避免后续 B 依赖 A 时无法获取;

  3. A 开始进行依赖注入,发现依赖 B,容器开始初始化 B Bean;

  4. 调用 doCreateBean() 方法,实例化 B,同样将 B 的实例封装为 ObjectFactory,存入三级缓存;

  5. B 开始进行依赖注入,发现依赖 A,容器尝试从缓存中获取 A:

  6. 先查一级缓存(singletonObjects):A 未初始化完成,无;

  7. 再查二级缓存(earlySingletonObjects):A 未提前暴露到二级缓存,无;

  8. 最后查三级缓存(singletonFactories):获取到 A 的 ObjectFactory,调用 getObject() 方法,获取 A 的原始实例,将 A 从三级缓存移到二级缓存(标记为“提前暴露”);

  9. B 注入 A 的实例,完成依赖注入和自身初始化,B 初始化完成后,存入一级缓存,同时从二级、三级缓存中移除;

  10. 回到 A 的依赖注入,容器从一级缓存中获取已初始化完成的 B,注入到 A 中;

  11. 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 面试追问

  1. Spring 三级缓存中,ObjectFactory 的作用是什么?(答:核心作用有两个:1. 提前暴露 Bean 实例,解决循环依赖——在 Bean 未初始化完成时,通过 ObjectFactory 暴露实例,供依赖方获取;2. 处理 AOP 代理——若 Bean 需要被代理,ObjectFactory 的 getObject() 方法会生成代理对象并返回,确保依赖方注入的是代理对象,而非原始实例,避免代理失效。)

  2. 如果禁用三级缓存(只保留一级和二级),会有什么问题?(答:无法解决「AOP 代理与循环依赖」的冲突。因为二级缓存只能存储提前暴露的原始实例,若 Bean 需要被 AOP 代理,后续生成的代理对象无法替换二级缓存中的原始实例,导致依赖方注入的是原始实例,而非代理对象,出现 AOP 功能失效(如事务、日志不生效)。)

  3. Spring Boot 中,如何排查循环依赖问题?(答:1. 查看启动日志,找到 BeanCurrentlyInCreationException 报错,确认循环依赖的 Bean 名称;2. 检查这些 Bean 的注入方式(是否为构造器注入)和作用域(是否为多例);3. 用 Spring 提供的工具类(如 BeanDefinitionRegistry)打印 Bean 的依赖关系;4. 开启 DEBUG 级别日志,查看 Bean 的初始化顺序和缓存操作流程,定位问题根源。)

  4. Spring 5.x 对循环依赖的处理有什么优化?(答:Spring 5.x 主要优化了构造器注入循环依赖的报错信息,更清晰地提示循环依赖的 Bean 名称和注入方式;同时优化了三级缓存的性能,减少了锁竞争;此外,支持通过 @Lazy 注解更灵活地处理构造器注入的循环依赖,但本质上仍未解决构造器注入的循环依赖,只是延迟暴露问题。)

2. Bean 初始化前后扩展点有哪些?(源码级,高频扩展题)

2.1 面试回答(源码版)

Spring 提供了多种Bean 初始化前后的扩展点,核心作用是在 Bean 实例化、依赖注入、初始化的前后,插入自定义逻辑(如增强 Bean、修改 Bean 属性、初始化资源等),底层依赖 Spring 的 BeanPostProcessor 接口及相关派生类,按执行顺序可分为 6 个核心扩展点,结合源码逻辑如下(按执行顺序排列):

核心扩展点(按执行顺序,结合 Bean 生命周期)

  1. BeanFactoryPostProcessor(Bean 定义后置处理器)

    1. 执行时机:Bean 实例化之前(BeanDefinition 加载完成后,Bean 未实例化、未注入依赖);

    2. 核心作用:修改 BeanDefinition 的属性(如修改 Bean 的作用域、初始化方法、依赖关系等),不修改 Bean 实例本身;

    3. 源码关联:由 Spring 容器在 BeanDefinition 注册完成后,主动调用其 postProcessBeanFactory() 方法;

    4. 常用实现:PropertySourcesPlaceholderConfigurer(处理配置文件占位符 ${})、CustomAutowireConfigurer(自定义自动注入规则)。

  2. BeanPostProcessor#postProcessBeforeInitialization(Bean 初始化前增强)

    1. 执行时机:Bean 实例化、依赖注入完成后,初始化方法(@PostConstruct、InitializingBean)执行之前

    2. 核心作用:对 Bean 实例进行增强(如修改 Bean 的属性、添加额外逻辑),返回修改后的 Bean 实例;

    3. 源码关联:所有 Bean 初始化前都会执行,由 AbstractAutowireCapableBeanFactory 的 applyBeanPostProcessorsBeforeInitialization() 方法调用;

    4. 注意:若返回 null,会导致 Bean 无法注入容器,抛出异常。

  3. @PostConstruct(JSR-250 注解)

    1. 执行时机:Bean 初始化前增强(postProcessBeforeInitialization)之后,InitializingBean 之前;

    2. 核心作用:自定义 Bean 初始化逻辑(如加载配置、初始化资源),无需实现接口,仅需标注在方法上;

    3. 源码关联:由 CommonAnnotationBeanPostProcessor(BeanPostProcessor 的实现类)解析并调用。

  4. InitializingBean 接口

    1. 执行时机:@PostConstruct 之后,自定义 init-method 之前;

    2. 核心作用:自定义 Bean 初始化逻辑,需实现 afterPropertiesSet() 方法;

    3. 源码关联:由 AbstractAutowireCapableBeanFactory 的 invokeInitMethods() 方法调用,优先级高于自定义 init-method。

  5. 自定义 init-method(XML/@Bean 配置)

    1. 执行时机:InitializingBean 之后,Bean 初始化后增强之前;

    2. 核心作用:自定义 Bean 初始化逻辑,通过 XML 配置(init-method)或 @Bean(initMethod = "xxx") 指定;

    3. 源码关联:由 AbstractAutowireCapableBeanFactory 的 invokeInitMethods() 方法调用,优先级最低。

  6. BeanPostProcessor#postProcessAfterInitialization(Bean 初始化后增强)

    1. 执行时机:Bean 所有初始化方法(@PostConstruct、InitializingBean、init-method)执行完成后,Bean 就绪之前;

    2. 核心作用:对 Bean 实例进行最终增强(如 AOP 代理生成),返回最终的 Bean 实例(可能是代理对象);

    3. 源码关联:由 AbstractAutowireCapableBeanFactory 的 applyBeanPostProcessorsAfterInitialization() 方法调用;

    4. 常用实现: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 面试追问

  1. BeanFactoryPostProcessor 和 BeanPostProcessor 的核心区别是什么?(答:1. 执行时机不同:BeanFactoryPostProcessor 在 Bean 实例化之前执行(操作 BeanDefinition);BeanPostProcessor 在 Bean 实例化、依赖注入之后执行(操作 Bean 实例);2. 作用对象不同:BeanFactoryPostProcessor 作用于 BeanDefinition(修改配置);BeanPostProcessor 作用于 Bean 实例(增强实例);3. 执行次数不同:BeanFactoryPostProcessor 整个容器启动时仅执行一次;BeanPostProcessor 对每个 Bean 都执行一次(两个增强方法各执行一次)。)

  2. Spring 中哪些核心功能依赖 BeanPostProcessor 实现?(答:1. 依赖注入(AutowiredAnnotationBeanPostProcessor 处理 @Autowired、@Value);2. AOP 代理(AnnotationAwareAspectJAutoProxyCreator 生成代理对象);3. 注解解析(CommonAnnotationBeanPostProcessor 处理 @PostConstruct、@PreDestroy、@Resource);4. Bean 增强(自定义逻辑注入)。)

  3. @PostConstruct 和 InitializingBean 的区别是什么?为什么推荐用 @PostConstruct?(答:区别:1. 实现方式:@PostConstruct 是注解,无需实现接口,代码简洁;InitializingBean 是接口,需实现 afterPropertiesSet() 方法,有侵入性;2. 优先级:@PostConstruct 先执行,InitializingBean 后执行;3. 灵活性:@PostConstruct 可标注在任意方法上,InitializingBean 只能实现固定方法。推荐 @PostConstruct 的原因:无侵入性、代码简洁、符合 JSR 规范,可独立于 Spring 框架使用。)

  4. 如何自定义一个 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)、工具类等,无需集中配置,仅需被容器管理即可。

二、源码细节补充

  1. @Configuration 源码:包含 @Component 注解,因此能被 @ComponentScan 扫描到;同时包含 @Configuration(proxyBeanMethods = true)(默认 true),proxyBeanMethods = true 表示开启 CGLIB 代理,确保 @Bean 方法调用返回单例 Bean;若设为 false,不生成代理,与 @Component 效果一致(用于提升性能,适合无 @Bean 方法依赖的场景)。

  2. @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 面试追问

  1. @Configuration 的 proxyBeanMethods 属性有什么作用?true 和 false 有什么区别?(答:作用:控制是否为配置类生成 CGLIB 代理,决定 @Bean 方法调用的行为。区别:1. proxyBeanMethods = true(默认):生成 CGLIB 代理,@Bean 方法间调用返回容器中的单例 Bean,支持依赖注入;2. proxyBeanMethods = false:不生成代理,@Bean 方法间调用返回新实例,不依赖容器,启动速度更快,适合无 @Bean 方法依赖的场景(如纯导入配置、无依赖的 @Bean 定义)。)

  2. 为什么 @Configuration 能被 @ComponentScan 扫描到?(答:因为 @Configuration 源码中包含 @Component 注解(@Configuration 是 @Component 的派生注解),@ComponentScan 扫描时,会识别所有标注 @Component 及其派生注解(@Controller、@Service、@Repository、@Configuration)的类,因此 @Configuration 类能被扫描到并注入容器。)

  3. @Component 标注的类中,如何确保 @Bean 方法间调用返回单例 Bean?(答:两种方式:1. 将 @Component 改为 @Configuration(推荐),利用 CGLIB 代理确保单例;2. 避免 @Bean 方法间直接调用,通过 @Autowired 注入依赖(如在 beanD() 方法参数中注入 BeanC,而非调用 beanC() 方法),确保获取的是容器中的单例 Bean。)

  4. 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 接口。

一、核心组成(源码+功能)

  1. 事件(ApplicationEvent)

    1. 定义:所有 Spring 事件的父类,继承自 EventObject,包含事件源(source)和事件发生时间(timestamp);

    2. 分类:① 内置事件(Spring 自带,如 ContextRefreshedEvent(容器初始化完成)、ContextClosedEvent(容器关闭));② 自定义事件(继承 ApplicationEvent,按需定义业务事件);

    3. 源码细节:ApplicationEvent 是抽象类,自定义事件需重写构造方法,传入事件源。

  2. 事件发布者(ApplicationEventPublisher)

    1. 定义:负责发布事件的接口,核心方法是 publishEvent(Object event);

    2. 源码关联:ApplicationContext 接口继承 ApplicationEventPublisher,因此 Spring 容器(如 AnnotationConfigApplicationContext)本身就是事件发布者;

    3. 发布逻辑:调用 publishEvent() 方法后,Spring 会将事件广播给所有注册的事件监听器。

  3. 事件监听器(ApplicationListener)

    1. 定义:负责监听特定事件的接口,核心方法是 onApplicationEvent(E event),事件发布后会自动执行该方法;

    2. 实现方式:① 实现 ApplicationListener 接口(指定监听的事件类型);② 标注 @EventListener 注解(无需实现接口,更简洁,推荐);

    3. 源码关联:Spring 容器启动时,会扫描所有 ApplicationListener 实现类和 @EventListener 标注的方法,注册到容器中。

二、事件驱动模型的完整流程(源码逻辑)

  1. 定义自定义事件(继承 ApplicationEvent),封装事件相关数据;

  2. 编写事件监听器(实现 ApplicationListener 或标注 @EventListener),定义事件触发后的业务逻辑;

  3. 通过 ApplicationEventPublisher(或 ApplicationContext)调用 publishEvent() 方法,发布事件;

  4. Spring 容器接收事件后,通过 EventMulticaster(事件广播器),将事件广播给所有匹配的监听器;

  5. 监听器接收事件,执行 onApplicationEvent() 方法(或 @EventListener 标注的方法),完成业务逻辑;

  6. (可选)若配置异步事件,监听器会在独立线程中执行,不阻塞事件发布者。

三、关键补充:同步 vs 异步事件

  1. 同步事件(默认):事件发布者发布事件后,会阻塞等待所有监听器执行完成,再继续执行后续逻辑;优点是简单、有序,缺点是阻塞性能差,适合监听器逻辑简单的场景。

  2. 异步事件:事件发布者发布事件后,立即继续执行后续逻辑,监听器在独立线程中执行;需通过 @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 面试追问

  1. Spring 事件驱动模型中,EventMulticaster 的作用是什么?默认实现类是什么?(答:作用:作为事件广播器,负责将发布的事件广播给所有匹配的事件监听器,是事件发布者与监听器之间的桥梁。默认实现类是 SimpleApplicationEventMulticaster,支持同步和异步事件广播,可通过配置线程池实现异步广播。)

  2. Spring 内置事件有哪些?分别在什么时机触发?(答:核心内置事件:1. ContextRefreshedEvent:Spring 容器初始化完成(所有 Bean 实例化、依赖注入完成)后触发;2. ContextClosedEvent:Spring 容器关闭(如应用停止)时触发;3. ContextStartedEvent:容器启动(调用 start() 方法)时触发;4. ContextStoppedEvent:容器停止(调用 stop() 方法)时触发;5. RequestHandledEvent:Spring MVC 处理完请求后触发(仅 Web 环境)。)

  3. @EventListener 注解和 ApplicationListener 接口相比,有什么优势?(答:1. 无需实现接口,代码简洁,侵入性低;2. 可直接标注在方法上,一个类可定义多个监听器方法,监听不同事件;3. 支持指定事件类型(通过注解参数),无需强转事件类型;4. 支持条件监听(结合 @Conditional 注解),可根据条件决定监听器是否生效;5. 支持异步监听(配合 @Async 注解),无需额外配置。)

  4. 如何自定义事件广播器(ApplicationEventMulticaster)?(答:1. 实现 ApplicationEventMulticaster 接口,重写 multicastEvent() 方法(核心,实现事件广播逻辑)、addApplicationListener() 方法(添加监听器)、removeApplicationListener() 方法(移除监听器);2. 给自定义广播器标注 @Component,确保被 Spring 注入容器,替代默认的 SimpleApplicationEventMulticaster;3. (可选)配置线程池,实现异步事件广播,提升性能;4. (可选)自定义异常处理器(ErrorHandler),处理监听器执行过程中的异常。)

  5. 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:

  1. SqlSessionFactoryBean:Spring 提供的 FactoryBean 实现类,核心作用是创建 MyBatis 的 SqlSessionFactory(MyBatis 核心对象,负责创建 SqlSession);源码中通过 afterPropertiesSet() 方法初始化 SqlSessionFactory,将 Spring 管理的 DataSource 注入 MyBatis,同时加载 MyBatis 配置文件、Mapper 映射文件。

  2. 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. 步骤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>
  1. 步骤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"&gt;

    <!-- 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"/&gt;
    &lt;/bean&gt;

    <!-- 2. 配置 SqlSessionFactoryBean,生成 SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"&gt;
        <!-- 注入数据源(必须,关联 Spring 管理的 DataSource) -->
        &lt;property name="dataSource" ref="dataSource"/&gt;
        <!-- 配置 MyBatis 核心配置文件路径(可选,也可在此处配置全局参数) -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/&gt;
        <!-- 配置 Mapper 映射文件路径(可选,若Mapper接口与映射文件同包,可省略) -->
        <property name="mapperLocations" value="classpath:com/xxx/mapper/*.xml"/&gt;
        <!-- 配置别名(可选,简化Mapper映射文件中的全类名) -->
        <property name="typeAliasesPackage" value="com.xxx.pojo"/&gt;
    &lt;/bean&gt;

    <!-- 3. 配置 MapperScannerConfigurer,扫描 Mapper 接口,生成代理 Bean -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"&gt;
        <!-- 配置 Mapper 接口所在包(核心,必须配置) -->
        <property name="basePackage" value="com.xxx.mapper"/>
<!-- 关联 SqlSessionFactory(可选,若容器中只有一个 SqlSessionFactory,可省略) -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/&gt;
    &lt;/bean&gt;

    <!-- 4. 配置 Service 层 Bean(将 Mapper 注入 Service) -->
    <bean id="userService" class="com.xxx.service.impl.UserServiceImpl">
        <property name="userMapper" ref="userMapper"/&gt; <!-- userMapper 是扫描生成的代理 Bean -->
    </bean>

</beans>
  1. 步骤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"&gt;
&lt;configuration&gt;
    <!-- 全局配置(可选,可在 SqlSessionFactoryBean 中配置) -->
    <settings>
       <!-- 开启驼峰命名映射(如数据库 user_name → 实体类 userName) -->
        <setting name="mapUnderscoreToCamelCase" value="true"/&gt;
        <!-- 开启日志(可选,便于调试) -->
        <setting name="logImpl" value="SLF4J"/>
    </settings>
</configuration>
  1. 步骤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. 步骤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&gt;
    <!-- Spring Boot Web(可选,若有接口需求) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web&lt;/artifactId&gt;
    &lt;/dependency&gt;
    <!-- 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>
    &lt;/dependency&gt;
    <!-- 德鲁伊连接池(可选,Spring Boot 自动配置) -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.20</version>
    </dependency>
</dependencies>
  1. 步骤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
  1. 步骤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 {
    // 方法省略
}
  1. 步骤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 面试追问

  1. Spring 整合 MyBatis 时,SqlSessionFactoryBean 的作用是什么?底层如何实现的?(答:作用:将 Spring 管理的 DataSource 注入 MyBatis,加载 MyBatis 配置文件和 Mapper 映射文件,生成 SqlSessionFactory(MyBatis 核心对象),交给 Spring 容器管理。底层实现:实现 FactoryBean 和 InitializingBean 接口,在 afterPropertiesSet() 方法中构建 SqlSessionFactory,通过 getObject() 方法向 Spring 容器提供 SqlSessionFactory 实例;核心是将 Spring 的 DataSource 与 MyBatis 的 Configuration 关联,完成 MyBatis 初始化。)

  2. MapperScannerConfigurer 是如何扫描 Mapper 接口并生成代理对象的?(答:1. 实现 BeanDefinitionRegistryPostProcessor 接口,在 postProcessBeanDefinitionRegistry() 方法中创建 ClassPathMapperScanner 扫描器;2. 扫描指定包下的 Mapper 接口(通过 @Mapper 注解或 MarkerInterface 过滤);3. 为每个 Mapper 接口注册 BeanDefinition,指定 Bean 的类型为 MapperFactoryBean(FactoryBean 实现);4. MapperFactoryBean 底层通过 MyBatis 的 MapperProxyFactory 生成 JDK 动态代理对象,注入到 Service 中。)

  3. 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 即可完成核心配置。)

  4. 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 语句,返回结果。)

  5. 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 事务同步。)

  6. 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 核心逻辑。)

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐