Spring:IoC、AOP、事务、生命周期与生产实践
前言
Spring 的价值在于把对象装配、横切能力和生命周期治理从业务代码中分离出来。本文从 IoC、AOP、事务和 Bean 生命周期串起运行机制,并结合常见的代理失效、循环依赖和事务边界问题说明生产中的使用原则。
Spring 的核心不是注解
Spring 的核心价值是把对象创建、依赖装配和横切能力从业务代码中抽离出来。业务类只描述“需要什么”,容器负责创建、连接和管理对象;日志、鉴权、事务、监控等不应散落在每个方法里,而是通过 AOP 在合适的位置统一织入。
以订单服务为例:OrderService 依赖库存、支付和订单仓储。没有 Spring 时,业务代码常常自己 new 依赖,难以替换实现、测试或统一治理;有 Spring 后,容器创建这些对象并注入依赖,事务代理围绕下单方法开启和提交事务。
flowchart LR
A[应用启动] --> B[读取 Bean 定义]
B --> C[创建 IoC 容器]
C --> D[实例化 Bean]
D --> E[注入依赖]
E --> F[执行初始化回调]
F --> G[生成可代理 Bean]
H[HTTP 请求] --> I[Controller]
I --> J[事务/AOP 代理]
J --> K[OrderService]
K --> L[Repository / 外部服务]
面试中可先用一句话概括:IoC 解决对象由谁创建和管理,DI 解决依赖如何提供,AOP 解决事务、日志等横切逻辑如何统一织入。
IoC、DI 与容器
IoC 和 DI 分别是什么
IoC(控制反转)指对象的创建和装配控制权从业务代码转交给容器。它不是“没有控制”,而是由应用代码控制变为框架容器控制。
DI(依赖注入)是实现 IoC 的主要方式:容器在创建 OrderService 时,将其依赖的 StockService、OrderRepository 注入进去。DI 常见方式如下。
| 注入方式 | 示例 | 推荐程度 | 原因 |
|---|---|---|---|
| 构造器注入 | OrderService(StockService stockService) |
推荐 | 依赖明确,可使用 final,对象创建后状态完整,单元测试方便 |
| Setter 注入 | setRuleEngine(...) |
适合可选依赖 | 可后置设置,但会让对象存在未完全初始化的窗口 |
| 字段注入 | @Autowired private StockService stockService |
不推荐用于新代码 | 隐藏依赖,不便于 final 和脱离容器的单元测试 |
单构造器从 Spring 4.3 起可以省略 @Autowired。当同一接口有多个实现时,用 @Primary 提供默认实现,用 @Qualifier("xxx") 精确选择;不要依赖字段名或注册顺序这种隐式规则。
1 |
|
BeanFactory、ApplicationContext 与 Bean 定义
BeanFactory 是最基础的 IoC 容器接口,提供 Bean 获取和依赖管理能力;ApplicationContext 在其基础上增加国际化、事件发布、资源加载、环境配置和自动识别后处理器等企业应用常用能力。Spring Boot 应用实际使用的是 ApplicationContext 的具体实现。
容器并不只管理 @Component。Bean 定义可来自 @Component、@Service、@Repository、@Controller 的组件扫描,也可来自 @Configuration 类中的 @Bean 方法、XML 或编程式注册。@Bean 适合第三方类库对象,例如 HTTP 客户端、SDK 客户端或复杂构造对象;不应为了“统一风格”把所有业务类都改成 @Bean。
| 概念 | 作用 | 面试易错点 |
|---|---|---|
| BeanDefinition | Bean 的元数据,如类型、作用域、依赖、初始化方法 | 它不是 Bean 实例 |
| BeanFactoryPostProcessor | 修改 BeanDefinition | 在 Bean 实例化前运行,不能直接依赖普通 Bean |
| BeanPostProcessor | 在 Bean 初始化前后处理实例 | AOP 代理常在此阶段生成 |
| FactoryBean | 一个“生产其他 Bean 的 Bean” | 获取 &name 才是 FactoryBean 本身,name 通常取得其产品 |
Bean 生命周期与作用域
单例 Bean 的关键流程
常见生命周期可以记为:定义解析 -> 实例化 -> 属性注入 -> Aware 回调 -> 初始化前处理 -> 初始化 -> 初始化后处理 -> 销毁。
1 | BeanDefinition |
初始化回调的选择上,业务代码通常使用 @PostConstruct 和 @PreDestroy 即可;实现 InitializingBean、DisposableBean 会耦合 Spring 接口;initMethod/destroyMethod 更适合第三方对象。不要在 @PostConstruct 中执行长时间远程调用或全量预热,否则会拖慢启动并放大发布失败风险。
| Scope | 含义 | 使用提醒 |
|---|---|---|
| singleton | 一个 ApplicationContext 一份实例,默认作用域 | 单例不等于线程安全,不要保存请求级可变状态 |
| prototype | 每次获取 Bean 新建一个实例 | 容器只负责创建,不负责其销毁;注入到 singleton 后不会自动每次新建 |
| request / session | 每个 HTTP 请求或会话一份 | 只用于 Web 上下文,优先传递参数而不是滥用请求作用域 |
如果 singleton 需要每次获取 prototype,可以注入 ObjectProvider<T>,或使用 scoped proxy;更常见的选择是把临时状态放在方法局部变量,避免扩大 Bean scope 的复杂度。
AOP 与动态代理
为什么事务、日志能“自动执行”
AOP 将分散在多个业务方法中的横切逻辑统一处理。术语要能对应到实际代码:
| 术语 | 含义 | 下单示例 |
|---|---|---|
| Aspect | 横切模块 | 事务切面、审计切面 |
| Advice | 具体增强逻辑 | 方法前校验、方法后提交事务、异常后回滚 |
| Pointcut | 哪些方法被匹配 | execution(* ..OrderService.place(..)) |
| Join Point | 可被增强的位置 | Spring AOP 中主要是方法执行 |
| Proxy | 容器交给调用方的代理对象 | 调用代理后再进入真实 OrderService |
Spring AOP 默认在运行期通过代理实现,并非修改目标类字节码。目标类实现接口时可使用 JDK 动态代理;没有接口时通常使用 CGLIB 子类代理。两者都是“外部调用先到代理,再到目标方法”,所以 final 类/方法不能被 CGLIB 子类覆写增强,private 方法也无法通过 Spring 代理拦截。
sequenceDiagram
participant C as Controller
participant P as Transaction Proxy
participant S as OrderService
participant D as Database
C->>P: placeOrder()
P->>P: 开启事务
P->>S: 调用目标方法
S->>D: 写订单和扣库存
D-->>S: 成功或异常
alt 正常返回
P->>P: 提交事务
else 抛出匹配异常
P->>P: 回滚事务
end
P-->>C: 返回结果或异常
自调用为什么导致 AOP 失效
代理只能拦截“经过代理对象的外部调用”。同一个对象中 this.inner() 是直接调用目标对象方法,没有经过代理,因此 @Transactional、@Async、自定义切面都可能失效。
1 |
|
优先把 createOrder 拆到另一个职责清晰的 Bean,让调用跨越代理边界。通过 AopContext.currentProxy() 绕过属于强耦合方案,只有理解其前提并显式开启代理暴露时才考虑;不要把它作为默认写法。
声明式事务与生产边界
@Transactional 实际做了什么
@Transactional 不是数据库功能本身。Spring 在代理进入目标方法前,根据事务传播行为向 PlatformTransactionManager 请求事务;对于 JDBC/JPA,事务管理器会把数据库连接绑定到当前线程。方法正常结束则提交,抛出满足规则的异常则回滚,并清理线程绑定资源。
默认规则:运行时异常(RuntimeException)和 Error 回滚;受检异常默认不回滚。需要时使用 rollbackFor = Exception.class,但更重要的是明确异常语义,不要为了“全部回滚”吞掉异常后继续返回成功。
| 传播行为 | 语义 | 典型场景 |
|---|---|---|
REQUIRED |
当前有事务则加入,没有则新建,默认值 | 创建订单、扣库存等同一原子操作 |
REQUIRES_NEW |
挂起外层事务,独立开启新事务 | 审计记录、独立失败记录;不能滥用来掩盖一致性问题 |
SUPPORTS |
有事务则加入,没有则非事务执行 | 纯查询或可选事务逻辑 |
MANDATORY |
必须在已有事务中运行 | 必须随主业务一起提交的内部方法 |
NESTED |
在同一物理事务内创建保存点 | 支持保存点的 JDBC 场景;不等同于独立事务 |
隔离级别要基于数据库能力理解。MySQL InnoDB 的默认隔离级别通常是 REPEATABLE_READ,PostgreSQL 默认是 READ_COMMITTED。设置 Spring 注解并不会让数据库支持不存在的语义;高隔离级别可能增加锁等待、序列化失败或重试成本。
最常见的事务失效原因
| 现象 | 常见原因 | 修复方式 |
|---|---|---|
| 方法没有事务 | 自调用、对象不是 Spring Bean、方法不可代理 | 拆分 Bean,通过代理调用,确认扫描与代理条件 |
| 抛异常却提交 | 捕获异常未再抛出;抛出受检异常 | 重新抛出,或按业务设置 rollbackFor |
| 异步方法不在原事务内 | @Async 切换了线程,线程绑定事务不会自动传播 |
明确异步边界,用独立事务、Outbox 或消息最终一致 |
| 外部 HTTP 调用拖住事务 | 事务内调用支付、物流等慢接口 | 缩短事务,只写本地事实和 Outbox,提交后异步调用 |
| 事务看起来存在但数据未提交 | 使用了错误数据源/事务管理器,或非事务资源 | 指定事务管理器,核对数据源与日志 |
事务应尽可能短:校验参数、查询远程数据、生成大文件、循环调用第三方接口都不应包在数据库事务里。跨服务一致性优先采用本地事务 + Outbox + 消息消费幂等 + 对账补偿;不要把分布式锁或长数据库事务当作跨系统事务的万能替代。
循环依赖、配置类与常见启动问题
为什么构造器循环依赖无法解决
A 构造器依赖 B,B 构造器又依赖 A 时,两者都需要对方实例才能完成创建,容器无法提前暴露完整对象,因此会启动失败。字段或 Setter 注入的 singleton 在某些版本和配置下可以利用“三级缓存”暴露早期引用来打破循环,但这不应视为设计能力。
三级缓存可简化理解为:一级缓存保存完整 singleton,二级缓存保存提前暴露的对象或代理,三级缓存保存生成早期引用的工厂。它主要解决 singleton 的属性注入循环依赖;prototype 循环依赖通常无法解决;涉及 AOP 代理时还可能拿到早期代理引用。Spring Boot 2.6 起默认不鼓励循环依赖,生产代码应按职责拆分,例如提取协调服务、事件发布或领域接口。
@Configuration 类中的 @Bean 方法默认会被 CGLIB 增强,以保证配置类内部多次调用 @Bean 方法时仍获取容器中的同一个 singleton。@Configuration(proxyBeanMethods = false) 可减少代理开销,但此时不要在同一个配置类里直接互相调用 @Bean 方法来依赖单例语义,应通过方法参数注入依赖。
启动失败排查顺序
- 先看异常栈最底层的
Caused by,不要只看最外层ApplicationContext启动失败。 NoSuchBeanDefinitionException:检查组件扫描范围、条件装配、Profile、Bean 名称和类型是否匹配。NoUniqueBeanDefinitionException:用@Primary或@Qualifier明确注入目标,而不是随意删实现类。BeanCurrentlyInCreationException:确认是否构造器循环依赖,优先重构职责。- 代理或事务问题:开启
org.springframework.transaction、org.springframework.aop的调试日志,确认调用是否经过代理。
Web 请求链路与异常处理
Spring MVC 中,一个请求通常经历:Servlet 容器 -> DispatcherServlet -> HandlerMapping 找 Controller 方法 -> HandlerInterceptor 前置处理 -> 参数解析和校验 -> Controller -> Service/AOP/事务 -> 返回值处理 -> HttpMessageConverter 序列化响应 -> 拦截器后置/完成回调。
| 扩展点 | 适合做什么 | 不适合做什么 |
|---|---|---|
| Filter | 编码、CORS、原始请求日志、安全框架前置处理 | 依赖具体 Controller 方法信息的业务逻辑 |
| HandlerInterceptor | 登录态、接口审计、轻量权限检查 | 在 preHandle 中做长耗时远程调用 |
@ControllerAdvice |
统一业务异常、参数校验异常到响应码映射 | 吞掉异常后仍返回 HTTP 200 成功 |
HttpMessageConverter |
JSON、文件等请求/响应编解码 | 承担业务字段转换和复杂规则 |
统一异常处理要区分客户端错误、可预期业务错误、系统错误和依赖错误。响应给客户端的信息应稳定、可理解且不泄漏堆栈;日志和链路中保留 traceId、业务主键和根因异常,便于排查。
高频面试题
| 问题 | 回答要点 |
|---|---|
| Spring 和 Spring Boot 的关系? | Spring 提供 IoC、AOP、事务、MVC 等基础能力;Spring Boot 基于它提供自动配置、Starter、内嵌容器和生产化约定。 |
| singleton Bean 线程安全吗? | 不一定。容器只保证实例数量,不保证可变字段并发安全;业务 Bean 应尽量无状态。 |
| BeanPostProcessor 和 BeanFactoryPostProcessor 区别? | 前者处理 Bean 实例,可创建代理;后者处理 BeanDefinition,发生在实例化前。 |
| JDK 动态代理和 CGLIB 区别? | JDK 基于接口生成代理;CGLIB 生成目标类子类。两者都依赖代理边界,无法自然拦截自调用。 |
@Transactional 为什么失效? |
先查是否经过代理,再查异常回滚规则、线程切换、事务管理器和数据库资源。 |
| 循环依赖为什么不推荐? | 它反映职责耦合,构造器循环依赖无法解决,早期引用还会带来代理和初始化时序风险。 |
| 为什么事务中不建议调远程服务? | 长事务占用连接和锁,远程超时会放大资源占用;应使用本地事务和异步可靠事件解耦。 |
总结
Spring 面试不能停留在“会用注解”。需要把容器创建 Bean、注入依赖、后处理器生成代理、代理开启事务、事务绑定资源和 Web 请求进入业务的链路连起来。生产设计上,优先使用构造器注入和无状态 singleton,缩短事务,避免自调用和循环依赖,并通过明确的异步事件与幂等补偿处理跨系统边界。


