前言

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 时,将其依赖的 StockServiceOrderRepository 注入进去。DI 常见方式如下。

注入方式 示例 推荐程度 原因
构造器注入 OrderService(StockService stockService) 推荐 依赖明确,可使用 final,对象创建后状态完整,单元测试方便
Setter 注入 setRuleEngine(...) 适合可选依赖 可后置设置,但会让对象存在未完全初始化的窗口
字段注入 @Autowired private StockService stockService 不推荐用于新代码 隐藏依赖,不便于 final 和脱离容器的单元测试

单构造器从 Spring 4.3 起可以省略 @Autowired。当同一接口有多个实现时,用 @Primary 提供默认实现,用 @Qualifier("xxx") 精确选择;不要依赖字段名或注册顺序这种隐式规则。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class OrderService {
private final StockService stockService;
private final OrderRepository orderRepository;

public OrderService(
@Qualifier("dbStockService") StockService stockService,
OrderRepository orderRepository) {
this.stockService = stockService;
this.orderRepository = orderRepository;
}
}

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
2
3
4
5
6
7
8
9
BeanDefinition
-> 构造器实例化
-> populateBean: 注入字段、Setter 和依赖
-> BeanNameAware / BeanFactoryAware / ApplicationContextAware
-> BeanPostProcessor#postProcessBeforeInitialization
-> @PostConstruct / InitializingBean#afterPropertiesSet / initMethod
-> BeanPostProcessor#postProcessAfterInitialization
-> 可被替换为 AOP 代理对象
-> 容器关闭时: @PreDestroy / DisposableBean / destroyMethod

初始化回调的选择上,业务代码通常使用 @PostConstruct@PreDestroy 即可;实现 InitializingBeanDisposableBean 会耦合 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
2
3
4
5
6
7
8
9
10
11
@Service
public class OrderService {
public void submit() {
createOrder(); // 自调用,不经过事务代理
}

@Transactional
public void createOrder() {
// 事务可能不会按预期创建
}
}

优先把 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 构造器依赖 BB 构造器又依赖 A 时,两者都需要对方实例才能完成创建,容器无法提前暴露完整对象,因此会启动失败。字段或 Setter 注入的 singleton 在某些版本和配置下可以利用“三级缓存”暴露早期引用来打破循环,但这不应视为设计能力。

三级缓存可简化理解为:一级缓存保存完整 singleton,二级缓存保存提前暴露的对象或代理,三级缓存保存生成早期引用的工厂。它主要解决 singleton 的属性注入循环依赖;prototype 循环依赖通常无法解决;涉及 AOP 代理时还可能拿到早期代理引用。Spring Boot 2.6 起默认不鼓励循环依赖,生产代码应按职责拆分,例如提取协调服务、事件发布或领域接口。

@Configuration 类中的 @Bean 方法默认会被 CGLIB 增强,以保证配置类内部多次调用 @Bean 方法时仍获取容器中的同一个 singleton。@Configuration(proxyBeanMethods = false) 可减少代理开销,但此时不要在同一个配置类里直接互相调用 @Bean 方法来依赖单例语义,应通过方法参数注入依赖。

启动失败排查顺序

  1. 先看异常栈最底层的 Caused by,不要只看最外层 ApplicationContext 启动失败。
  2. NoSuchBeanDefinitionException:检查组件扫描范围、条件装配、Profile、Bean 名称和类型是否匹配。
  3. NoUniqueBeanDefinitionException:用 @Primary@Qualifier 明确注入目标,而不是随意删实现类。
  4. BeanCurrentlyInCreationException:确认是否构造器循环依赖,优先重构职责。
  5. 代理或事务问题:开启 org.springframework.transactionorg.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,缩短事务,避免自调用和循环依赖,并通过明确的异步事件与幂等补偿处理跨系统边界。