Spring:Bean 生命周期、AOP 与事务

从 IoC 容器、三级缓存到 AOP 代理和事务失效,梳理 Spring 与 Spring Boot 的高频面试链路

Spring 题有一条很自然的链路:容器怎么创建 Bean、怎么给 Bean 加能力、事务为什么依赖代理、Boot 又怎么把这一切自动化。按这条线准备,比孤立背注解有效。以下默认 Spring Framework 6 / Spring Boot 3。

1. 怎么理解 IoC 和 DI?BeanFactory 与 ApplicationContext 有什么区别?

IoC 是控制反转:对象的创建、依赖装配和生命周期交给容器,业务代码只声明需要什么。DI 是它的实现方式,通过构造器、setter 或字段注入把依赖传进来。

BeanFactory 是最基础的容器接口,提供 getBean 和 Bean 生命周期管理,默认懒加载。ApplicationContext 继承它,额外提供资源加载、事件发布、国际化、AOP 集成和注解支持,单例默认在启动时实例化。

实践上优先构造器注入:依赖不可变、便于单元测试、能暴露循环依赖问题。字段注入写法简单,但会让类难以脱离容器测试,也掩盖了职责过多的信号。

2. Bean 的生命周期是怎样的?

大致分四段:实例化、属性填充、初始化、销毁。

实例化通过构造器或工厂方法完成;随后注入依赖并回调各类 Aware 接口,例如 BeanNameAware、ApplicationContextAware。接着执行 BeanPostProcessor 的前置处理,再调用 @PostConstruct、InitializingBean.afterPropertiesSet 和自定义 init-method,最后执行后置处理——AOP 代理通常就在这一步生成。

使用阶段结束后,容器关闭时依次调用 @PreDestroy、DisposableBean.destroy 和 destroy-method。注意原型 Bean 的销毁方法不会由容器调用,需要调用方自己负责。

BeanPostProcessor 是理解 Spring 扩展性的关键:它能在初始化前后改写甚至替换 Bean,@Autowired、@Async、事务代理都依赖这一类扩展点。

3. 循环依赖是怎么解决的?为什么构造器注入不行?

单例 Bean 的字段或 setter 循环依赖,靠三级缓存解决:一级放成品 Bean,二级放提前暴露的早期引用,三级放能生成早期引用的工厂。

创建 A 时先实例化并放入三级缓存,填充属性发现依赖 B,转去创建 B;B 依赖 A 时从缓存拿到 A 的早期引用,B 完成后 A 继续填充并最终完成。三级缓存的意义在于:如果 A 需要被 AOP 代理,工厂能在暴露早期引用时就生成代理,而不是暴露原始对象。

构造器注入要求在实例化阶段就拿到依赖,此时对象还不存在,无法提前暴露,因此会抛 BeanCurrentlyInCreationException。Spring Boot 2.6 起默认禁止循环依赖,需要显式开启才恢复旧行为;更好的做法是重构依赖或使用 @Lazy、ObjectProvider 延迟获取。

4. Bean 是线程安全的吗?单例要注意什么?

Spring 不保证 Bean 线程安全,它只负责创建和管理。单例 Bean 被所有请求共享,只要存在可变的成员状态,就可能出现并发问题。

无状态的 Service、DAO 天然安全;有状态的对象要么改成方法内局部变量,要么用 ThreadLocal(并记得清理),要么把作用域改成原型。给单例加 synchronized 通常只是把瓶颈集中到一个点上。

常见误区是把 @Scope("prototype") 注到单例里就以为每次调用都会新建:注入只发生一次,单例持有的是同一个原型实例。需要每次获取新对象时应注入 ObjectProvider 或使用作用域代理。

5. Spring AOP 的原理是什么?为什么自调用会失效?

Spring AOP 基于动态代理:目标类实现了接口时可用 JDK 代理,否则用 CGLIB 生成子类。Spring Boot 默认 proxyTargetClass = true,即优先 CGLIB,所以 final 类和 final 方法无法被代理。

调用方拿到的是代理对象,代理在方法前后织入通知。所谓自调用失效,是指同类中 this.otherMethod() 绕过了代理,通知自然不会执行;事务、缓存、异步注解都会遇到这个问题。

解决方式:拆到另一个 Bean、注入自身代理、使用 AopContext.currentProxy()(需开启 exposeProxy),或者改用 AspectJ 编译期/加载期织入。生产上更推荐前两种,侵入性小。

6. @Transactional 在哪些情况下会失效?传播行为怎么选?

失效的常见原因:自调用绕过代理;方法不是 public(Spring 6 起类代理支持 protected 和包可见方法,但仍有边界);Bean 不是由容器管理;异常被 catch 后没有抛出;抛出的是受检异常而默认只回滚 RuntimeException 和 Error;数据库表本身不支持事务;在另一个线程里执行。

传播行为中,REQUIRED 是默认值,加入现有事务;REQUIRES_NEW 挂起当前事务、独立提交,适合记日志等「必须落库」的操作;NESTED 使用保存点,只对 JDBC 等支持保存点的资源有效。

还要注意只读事务和超时设置,以及事务内不要做远程调用——长事务会长期占用连接和锁。

7. Spring Boot 自动配置的原理?

@SpringBootApplication 组合了配置类、组件扫描和 @EnableAutoConfiguration。后者通过 AutoConfigurationImportSelector 读取候选配置类:Boot 2.7 起使用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,更早的版本用 spring.factories。

候选类并不会全部生效,而是由条件注解筛选:类路径上是否存在某个类、容器里是否已有同类型 Bean、配置项是否开启。这就是「引入 starter 就自动装配,同时允许自定义覆盖」的机制。

自定义覆盖之所以生效,是因为自动配置类通常带 @ConditionalOnMissingBean,且用户配置先于自动配置处理。排查自动配置问题时,可以开启 debug=true 查看条件评估报告。

8. 一个 HTTP 请求在 Spring MVC 里是怎么走完的?

请求先经过 Servlet 容器的过滤器链,进入 DispatcherServlet。它向 HandlerMapping 询问由哪个处理器处理,拿到处理器和拦截器链后,交给匹配的 HandlerAdapter。

HandlerAdapter 调用参数解析器把请求参数、路径变量、请求体转换成方法参数,执行 Controller 方法;返回值再由返回值处理器处理,@ResponseBody 走 HttpMessageConverter 序列化,否则交给视图解析器渲染。

拦截器的 preHandle 在处理器执行前调用,postHandle 在渲染前调用,afterCompletion 在完成后调用。过滤器属于 Servlet 规范,能拿到原始请求但不能感知处理器;拦截器属于 Spring MVC,能访问处理器信息。

易错点与自测

  • 三级缓存只解决单例的字段/setter 循环依赖,不解决构造器注入和原型作用域。
  • AOP 的一切「注解不生效」几乎都可以归结为「没走代理」。
  • 自动配置不是「无条件装配」,条件注解和加载顺序才是重点。

自测:① 画出 Bean 从实例化到销毁的关键回调顺序;② 说出三种让 @Transactional 恢复生效的改法;③ 解释 REQUIRES_NEW 与 NESTED 在回滚行为上的差异。

参考资料

评论

为保障内容质量,评论需站长审核通过后才会展示;提交后没立刻看到属正常,请勿重复提交。

还没有评论,来抢沙发~