跳转至

Bean 生命周期与循环依赖


1. 类比:Bean 的一生就像员工入职到离职

招聘(实例化)→ 培训(属性注入)→ 报到(Aware 回调)→ 入职审查(BeanPostProcessor before)
→ 上岗(初始化)→ 转正(BeanPostProcessor after,AOP 代理在此创建)→ 工作(使用中)→ 离职(销毁)

2. 完整生命周期流程

flowchart LR
    A["① 实例化\n反射调用构造器"] --> B["② 属性注入\n@Autowired / @Value"]
    B --> C["③ Aware 回调\nBeanNameAware\nApplicationContextAware..."]
    C --> D["④ BPP#before\npostProcessBefore\nInitialization"]
    D --> E["⑤ 初始化\n@PostConstruct\nafterPropertiesSet\ninit-method"]
    E --> F["⑥ BPP#after\npostProcessAfter\nInitialization\n⚠️ AOP代理在此创建"]
    F --> G["⑦ 使用中\nIn Use"]
    G --> H["⑧ 销毁\n@PreDestroy\ndestroy\ndestroy-method"]

① 实例化 Instantiation 容器读取 BeanDefinition,通过反射调用无参构造器(或指定构造器)创建原始对象;此时所有字段均为默认值(null/0);若使用构造器注入,依赖在此步传入。

② 属性注入 Populate 容器扫描字段和 setter 上的 @Autowired / @Value / @Resource,从容器中查找匹配 Bean 并赋值;@Value 占位符由 BeanFactoryPostProcessor 在容器启动阶段提前解析,此步直接取值填入。

③ Aware 接口回调 若 Bean 实现了 Aware 子接口,容器依次回调: - BeanNameAware.setBeanName() → 注入 Bean 在容器中的 id - BeanClassLoaderAware → 注入加载该 Bean 的 ClassLoader - BeanFactoryAware → 注入所属 BeanFactory - ApplicationContextAware → 注入完整 ApplicationContext

普通业务 Bean 无需实现,主要供框架内部组件或工具类使用。

④ BeanPostProcessor#before 遍历所有已注册的 BeanPostProcessor,依次调用 postProcessBeforeInitialization();返回值即为后续流程使用的对象(可替换或包装原始 Bean);@PostConstructCommonAnnotationBeanPostProcessor 在此步触发执行。

为什么要有第④步? 初始化钩子(第⑤步)是 Bean 自己的逻辑,而第④步是给外部在初始化之前插手的机会。Spring 内部用它来处理 @PostConstruct@Autowired 校验等;开发者一般不直接实现 BeanPostProcessor,但可以通过它做统一的 Bean 增强,例如:对所有 Bean 打印初始化日志、校验某个注解是否配置正确等。

⑤ 初始化 Initialization 按固定顺序执行三种初始化钩子: 1. @PostConstruct 标注的方法(已在第④步由 BPP 触发) 2. InitializingBean.afterPropertiesSet() 3. XML / @Bean(initMethod) 指定的 init-method

三者语义相同,均在属性注入完成后执行业务初始化逻辑(建立连接、预热缓存等)。

为什么要有第⑤步? 构造器执行时依赖还未注入(字段注入场景),所以无法在构造器里做初始化。第⑤步是 Spring 专门留给开发者的"依赖就绪后的初始化入口"。

  • Spring 自身:用 afterPropertiesSet() 做框架组件的内部校验和准备,例如 SqlSessionFactoryBean 在此构建 SqlSessionFactory
  • 开发者:最常用 @PostConstruct,典型场景:预热本地缓存、建立长连接、加载配置数据到内存、启动后台线程等。

⑥ BeanPostProcessor#after ⚠️ AOP 代理在此创建 遍历所有 BeanPostProcessor,调用 postProcessAfterInitialization()AbstractAutoProxyCreator 在此检测 Bean 是否匹配切点,若匹配则用 JDK 动态代理或 CGLIB 生成代理对象,以代理替换原始 Bean 注册到容器单例缓存中。

为什么要有第⑥步,而不是在第④步直接创建代理? 代理必须包装一个行为完整的对象——第⑤步的初始化逻辑必须先跑完,代理才能正确拦截方法调用。如果在第④步就创建代理,初始化钩子就会在代理对象上执行,行为不可控。因此 Spring 把"增强后处理"放在初始化完成之后。

  • Spring 自身:AOP 代理(@Transactional@Async、自定义切面)全部在此创建;@Async 的线程池绑定也在此完成。
  • 开发者:极少直接实现此接口;但如果需要对 Bean 做全局包装(如统一加监控埋点、动态替换实现类),可以在此返回一个新的包装对象。

⑦ 使用中 In Use Bean 以单例形式存活于容器;外部通过 getBean()@Autowired 拿到的是第⑥步最终返回的对象(可能是代理)。

⑧ 销毁 Destruction 容器关闭(close() / stop())时触发,执行顺序: 1. @PreDestroy 标注的方法 2. DisposableBean.destroy() 3. XML / @Bean(destroyMethod) 指定的 destroy-method

⚠️ prototype 作用域的 Bean 不会触发此阶段——容器不持有其引用,实例由 GC 负责回收。

为什么 AOP 代理在第⑥步创建: - 正常情况:Spring 默认在第⑥步(postProcessAfterInitialization)创建代理,此时 Bean 已完成属性注入和初始化,是最终状态,代理包装的是一个行为完整的对象。 - 循环依赖情况:若 A 依赖 B、B 又依赖 A,且 A 需要被代理,Spring 会提前在第②步就为 A 创建代理并放入三级缓存,让 B 能拿到 A 的代理引用。这样做的目的是保证一致性:无论谁持有 A 的引用,拿到的都是同一个代理对象,而不是有的地方拿到代理、有的地方拿到原始对象(否则 B 会绕过 AOP 直接调用原始 A)。


3. 关键扩展点说明

扩展点 作用 典型应用 执行时机
BeanPostProcessor 在初始化前后对 Bean 进行增强 AOP 代理就是在 postProcessAfterInitialization 中创建的 每个 Bean 初始化前后
BeanFactoryPostProcessor 在 Bean 实例化之前修改 BeanDefinition PropertySourcesPlaceholderConfigurer 解析 @Value("${...}") 占位符 容器启动,Bean 实例化前
@PostConstruct Bean 初始化完成后执行 初始化缓存、建立连接 属性注入完成后
@PreDestroy Bean 销毁前执行 释放资源、关闭连接 容器关闭时

4. 常见误区(深度分析)

误区1:在构造器中使用 @Autowired 注入的字段

// ❌ 误区:在构造器中使用 @Autowired 注入的字段(此时还未注入)
@Component
public class MyService {
    @Autowired
    private OtherService other;

    public MyService() {
        other.doSomething(); // NullPointerException!
        // 原因:生命周期第①步(实例化)在第②步(属性注入)之前
        // 构造器执行时,@Autowired 字段还是 null
    }
}

// ✅ 最佳方案:构造器注入(Spring 官方推荐,依赖在对象创建时就已就绪)
@Component
public class MyService {
    private final OtherService other;

    public MyService(OtherService other) {
        this.other = other;
        other.doSomething(); // 构造器注入时依赖已传入,安全
    }
}

// ✅ 次选:使用 @PostConstruct(在第⑤步执行,此时属性已注入)
@Component
public class MyService {
    @Autowired
    private OtherService other;

    @PostConstruct
    public void init() {
        other.doSomething(); // 此时依赖已注入完毕
    }
}

误区2:以为 @Scope("prototype") 的 Bean 也会执行 @PreDestroy

// ❌ 误区:以为 @Scope("prototype") 的 Bean 也会执行 @PreDestroy
// 原因:Spring 不管理 prototype Bean 的销毁,容器不会调用其 @PreDestroy
// 设计原因:prototype 每次都创建新对象,Spring 无法追踪所有实例,
//           如果追踪会导致内存泄漏(持有所有 prototype 实例的引用)
@Component
@Scope("prototype")
public class PrototypeBean {
    @PreDestroy
    public void destroy() {
        // ❌ 这个方法不会被 Spring 调用!
    }
}

5. 工作中常见问题

问题一:@Autowired 注入为 null

现象:调用某个 Service 的方法时,里面的依赖字段是 null,抛出 NullPointerException

根本原因:该对象不是由 Spring 容器创建的,而是通过 new 手动创建的。Spring 只对自己管理的 Bean 进行依赖注入,手动 new 出来的对象完全绕过了容器,@Autowired 注解不会生效。

// ❌ 错误:手动 new,Spring 不会注入任何依赖
MyService service = new MyService();
service.doSomething(); // service 内部的 @Autowired 字段全是 null

// ✅ 正确:从 Spring 容器中获取,或者通过 @Autowired 注入
@Autowired
private MyService service; // Spring 管理,依赖正常注入

问题二:@PostConstruct 方法中出现 NPE

现象:应用启动时,@PostConstruct 标注的初始化方法抛出 NullPointerException

根本原因:这个问题的本质是在错误的生命周期阶段使用了依赖。最典型的场景是:在构造器中使用了 @Autowired 字段注入的依赖——构造器执行时处于生命周期第①步(实例化),而 @Autowired 字段注入发生在第②步(属性注入),此时字段还是 null,所以报 NPE。

注意:问题不是出在 @PostConstruct 方法本身,而是出在构造器里提前使用了还未注入的字段@PostConstruct 恰恰是解决这个问题的正确方案——它在第⑤步执行,此时属性注入已经完成。

// ❌ 错误:在构造器中使用 @Autowired 字段,此时字段还未注入
@Component
public class CacheService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public CacheService() {
        // 构造器在第①步执行,@Autowired 注入在第②步
        // 此时 redisTemplate 还是 null,必然 NPE
        redisTemplate.opsForValue().set("init", "true");
    }
}

// ✅ 正确方案一:将初始化逻辑移到 @PostConstruct 方法中
// @PostConstruct 在第⑤步执行,此时 @Autowired 字段已经注入完毕
@Component
public class CacheService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @PostConstruct
    public void init() {
        // 此时 redisTemplate 已经注入,可以安全使用
        redisTemplate.opsForValue().set("init", "true");
    }
}

// ✅ 正确方案二(更推荐):改用构造器注入,依赖在构造器执行时就已传入
@Component
public class CacheService {
    private final RedisTemplate<String, Object> redisTemplate;

    public CacheService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate; // 构造器注入,依赖已就绪
        redisTemplate.opsForValue().set("init", "true"); // 安全
    }
}

问题三:Bean 重复定义冲突

现象:启动时报错 ConflictingBeanDefinitionException,或者注入时报错 NoUniqueBeanDefinitionException: expected single matching bean but found 2

根本原因:容器中存在多个同类型同名的 Bean,Spring 不知道该注入哪一个。

解决方案一:@Primary —— 标记首选 Bean

当存在多个同类型 Bean 时,在希望默认被注入的那个 Bean 上加 @Primary。注入时如果没有特别指定,Spring 会优先选择带 @Primary 的那个。

public interface MessageSender {
    void send(String msg);
}

@Component
@Primary // 标记为首选,不指定时默认注入这个
public class EmailSender implements MessageSender { ... }

@Component
public class SmsSender implements MessageSender { ... }

// 注入时不指定,自动选择 @Primary 的 EmailSender
@Autowired
private MessageSender messageSender; // 注入的是 EmailSender

解决方案二:@Qualifier —— 精确指定注入哪个 Bean

当需要注入特定的某个 Bean 时,在注入点用 @Qualifier("beanName") 明确指定 Bean 的名称(默认 Bean 名称是类名首字母小写)。

@Component
public class NotificationService {

    // 明确指定注入 emailSender 这个 Bean
    @Autowired
    @Qualifier("emailSender")
    private MessageSender emailSender;

    // 明确指定注入 smsSender 这个 Bean
    @Autowired
    @Qualifier("smsSender")
    private MessageSender smsSender;

    public void notifyAll(String msg) {
        emailSender.send(msg);
        smsSender.send(msg);
    }
}

两者的区别: - @Primary 是在 Bean 定义侧声明"我是默认的",适合有一个明显主选项的场景。 - @Qualifier 是在 注入侧声明"我要那个特定的",适合需要同时使用多个同类型 Bean 的场景。 - 两者可以同时存在:@Qualifier 的优先级高于 @Primary,指定了 @Qualifier 就会忽略 @Primary


6. 循环依赖与三级缓存

6.1 什么是循环依赖?

A 需要 B 才能创建,B 需要 A 才能创建,互相等待,形成死锁。

@Component
public class A {
    @Autowired
    private B b; // A 依赖 B
}

@Component
public class B {
    @Autowired
    private A a; // B 依赖 A → 循环!
}

为什么会出现循环依赖?

循环依赖的根本原因是职责划分不清晰,几个典型场景:

场景一:Service 层互相调用(最常见)

// OrderService 处理订单,需要扣减库存
@Service
public class OrderService {
    @Autowired
    private InventoryService inventoryService; // 依赖库存服务
}

// InventoryService 处理库存,需要记录订单日志
@Service
public class InventoryService {
    @Autowired
    private OrderService orderService; // 又依赖回订单服务 → 循环
}

根本原因:两个 Service 承担了对方的部分职责,边界模糊。

场景二:公共服务被多方依赖,自己又依赖其中一方

@Service
public class UserService {
    @Autowired
    private MessageService messageService; // 发消息
}

@Service
public class MessageService {
    @Autowired
    private UserService userService; // 查用户信息 → 循环
}

场景三:事件发布者与监听者互相依赖

@Service
public class PayService {
    @Autowired
    private NotifyService notifyService; // 支付完成后通知
}

@Service
public class NotifyService {
    @Autowired
    private PayService payService; // 通知时需要查支付状态 → 循环
}

我们真正需要解决的是什么?

循环依赖是设计问题,不是技术问题。

Spring 的三级缓存只是一个"兜底机制",能让程序跑起来,但它解决不了根本问题——两个类之间职责边界不清晰。如果只是依赖 Spring 的机制让循环依赖"消失",代码会越来越难维护。

真正需要解决的是:为什么这两个类会互相需要对方?

优先考虑:重构解耦

遇到循环依赖时,应该优先考虑重构,而不是想办法让 Spring 绕过去。常见的重构思路:

思路一:提取公共依赖(最常用)

把两个类都需要的逻辑抽取到第三个类中,打破循环:

重构前:A ↔ B(互相依赖)

重构后:A → C ← B(A 和 B 都依赖 C,C 不依赖任何人)
// 把 OrderService 和 InventoryService 都需要的"库存扣减+订单记录"逻辑
// 抽取到 StockDeductionService 中
@Service
public class StockDeductionService {
    // 只做库存扣减和记录,不依赖 OrderService 或 InventoryService
}

@Service
public class OrderService {
    @Autowired
    private StockDeductionService stockDeductionService; // 不再依赖 InventoryService
}

@Service
public class InventoryService {
    @Autowired
    private StockDeductionService stockDeductionService; // 不再依赖 OrderService
}

思路二:用事件解耦(适合通知类场景)

把"主动调用"改为"发布事件",发布者不需要知道谁来处理:

@Service
public class PayService {
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public void pay() {
        // 支付完成,发布事件,不再直接依赖 NotifyService
        eventPublisher.publishEvent(new PaySuccessEvent(this, orderId));
    }
}

@Service
public class NotifyService {
    // 监听事件,不再被 PayService 依赖
    @EventListener
    public void onPaySuccess(PaySuccessEvent event) {
        // 处理通知逻辑
    }
}

选择策略总结:

遇到循环依赖
先问:为什么这两个类互相需要对方?
能重构 → 优先重构(提取公共类 or 事件解耦)
不能重构(历史遗留、第三方代码)
字段注入 → Spring 三级缓存自动解决
构造器注入 → 加 @Lazy 打破循环

原则@Lazy 和三级缓存是最后的手段,不是第一选择。循环依赖出现时,首先应该审视设计是否合理。


6.2 三级缓存解决原理

Spring 的解决方案是:先给 A 一个"半成品"的引用,让 B 先用着,等 A 完成初始化后,B 持有的引用自动指向完整的 A。

三级缓存结构:
┌──────────────────────────────────┬──────────────────────────────┐
│ 一级缓存 singletonObjects         │ 完整的 Bean(已初始化)        │
│ 二级缓存 earlySingletonObjects    │ 早期 Bean(已实例化,未初始化)│
│ 三级缓存 singletonFactories       │ Bean 工厂(可生成早期引用)    │
└──────────────────────────────────┴──────────────────────────────┘
flowchart TD
    A1["① 创建 A,放入三级缓存\n(此时 A 是半成品)"] --> A2["② 注入 B,发现 B 未创建"]
    A2 --> B1["③ 创建 B,放入三级缓存"]
    B1 --> B2["④ 注入 A,从三级缓存获取 A 的早期引用\n(通过 ObjectFactory 生成,可能是 AOP 代理)"]
    B2 --> B3["⑤ B 初始化完成,放入一级缓存"]
    B3 --> A3["⑥ A 完成 B 的注入,初始化完成,放入一级缓存"]

为什么需要三级缓存而不是两级:如果 A 有 AOP 代理,B 需要持有的是 A 的代理对象而非原始对象。三级缓存存的是 ObjectFactory(工厂),可以在需要时生成代理对象;如果只有二级缓存,存的是原始对象,B 持有的就是未被代理的 A,AOP 失效。

6.3 三级缓存详细说明

"早期暴露的 Bean"是什么意思?

"早期"的意思是:Bean 还没有完成完整的生命周期(属性注入、初始化都还没做完),但已经被其他 Bean 拿去用了。

正常情况下,一个 Bean 要走完全部 8 个步骤才算"完整",才会放进一级缓存供外部使用。但循环依赖时,A 还在创建过程中,B 就急着要用 A——这时候 Spring 只能把"还没做完的 A"提前暴露出去,这就叫早期暴露

什么时候判断需要 AOP 代理?

判断发生在三级缓存的 ObjectFactory.getObject() 被调用时,具体是在 AbstractAutoProxyCreator#getEarlyBeanReference() 方法里:

  • 检查 A 是否匹配任何切点(@Transactional、自定义 @Aspect 等)
  • → 提前创建 A 的 AOP 代理,返回代理对象
  • → 直接返回原始对象

注意:这个判断只在循环依赖场景下才会提前触发。正常情况下,代理是在第⑥步(postProcessAfterInitialization)才创建的。

各级缓存的放入与移出时机

缓存 名称 存储内容 何时放入 何时移出
三级缓存 singletonFactories ObjectFactory(Bean 工厂) Bean 刚实例化完(第①步之后,第②步之前) 有人来取早期引用时(结果移入二级缓存,三级删除)
二级缓存 earlySingletonObjects 早期暴露的 Bean(可能是代理) 三级缓存的工厂第一次被调用时(存结果防重复) Bean 完整初始化完成,放入一级缓存时
一级缓存 singletonObjects 完整的单例 Bean Bean 走完完整生命周期 容器关闭销毁时

查找顺序:一级缓存 → 二级缓存 → 三级缓存(通过工厂生成后放入二级缓存)

为什么需要二级缓存?(防止重复生成)

三级缓存里存的是 ObjectFactory,每次调用它的 getObject() 都会重新执行一次生成逻辑(包括判断是否需要 AOP 代理、生成代理对象等)。

如果没有二级缓存,每次有人来要 A 的早期引用,都要调用一次工厂,就会生成多个不同的对象——这会导致 B 和 C 拿到的 A 的代理对象不是同一个实例,破坏单例语义。

所以:第一次从三级缓存生成早期引用后,立刻把结果存入二级缓存,三级缓存中删掉对应条目。后续再有人来要,直接从二级缓存拿,不再重复生成。

第一次要 A 的早期引用:
  三级缓存 → 调用 ObjectFactory.getObject() → 生成早期引用(可能是代理)
  → 存入二级缓存,从三级缓存删除

第二次要 A 的早期引用:
  直接从二级缓存拿,不再调用工厂,保证拿到的是同一个对象

完整的 A、B 循环依赖流程

① 实例化 A(new 出来,字段全是 null)
② 立刻把 A 的 ObjectFactory 放入【三级缓存】
③ 开始属性注入,发现需要 B,去创建 B...

  ① 实例化 B
  ② 把 B 的 ObjectFactory 放入【三级缓存】
  ③ 属性注入,发现需要 A
  ④ 去三级缓存找 A → 调用 ObjectFactory.getObject()
     → 判断 A 是否需要 AOP 代理 → 生成早期引用(原始对象 or 代理)
     → 把结果放入【二级缓存】,从三级缓存删除 A 的条目
  ⑤ B 拿到 A 的早期引用,完成注入
  ⑥ B 走完完整生命周期 → 放入【一级缓存】,从三级缓存删除 B 的条目

回到 A:
④ A 完成 B 的注入(B 已在一级缓存)
⑤ A 走完完整生命周期(初始化等)
⑥ 正常情况下第⑥步会创建代理,但发现二级缓存里已经有 A 的代理了
   → 直接用二级缓存里的对象,不重复创建
⑦ 把最终对象放入【一级缓存】,从二级缓存删除 A 的条目

正常没有循环依赖时:二级缓存永远是空的;三级缓存里的工厂也不会被调用,Bean 直接从三级缓存"毕业"到一级缓存。

6.4 为什么构造器注入无法解决循环依赖?

// 构造器注入时,创建 A 必须先有 B,创建 B 必须先有 A
// 此时 A 还没有放入任何缓存,无法提前暴露引用 → 死锁
@Component
public class A {
    private final B b;
    public A(B b) { this.b = b; } // 构造时就需要 B,但 A 还没放入缓存
}

根本原因:三级缓存的核心是"提前暴露未完成的 Bean 引用",而构造器注入在实例化阶段就需要依赖,此时 Bean 还未放入任何缓存,无法提前暴露,所以无法解决。字段注入可以先创建空对象(放入缓存),再注入依赖,所以能解决。

6.5 循环依赖的解决方案

方案 适用场景 说明
字段注入(默认支持) 大多数循环依赖 Spring 三级缓存自动解决
@Lazy 延迟注入 构造器注入的循环依赖 注入代理对象,首次使用时才真正初始化
重构解耦 根本解决方案 循环依赖往往意味着设计问题,应该重构
// @Lazy 解决构造器注入的循环依赖
@Component
public class A {
    private final B b;
    public A(@Lazy B b) { this.b = b; } // 注入 B 的代理,首次调用时才真正初始化 B
}

7. Bean 注册控制

7.1 控制 Bean 的创建顺序:@DependsOn

默认情况下,Spring 按照自己的扫描顺序创建 Bean,不保证先后。如果某个 Bean 的初始化依赖另一个 Bean 的副作用(比如 A 的 @PostConstruct 会初始化一个全局资源,B 需要这个资源已就绪),但 A 和 B 之间没有直接的字段依赖关系,Spring 就不知道要先创建 A。

这时候用 @DependsOn 显式声明依赖顺序:

// 场景:DatabaseInitializer 的 @PostConstruct 会建表,OrderService 需要表已存在
@Component
public class DatabaseInitializer {
    @PostConstruct
    public void init() {
        // 执行建表 SQL,初始化数据库结构
        System.out.println("数据库表结构初始化完成");
    }
}

@Component
@DependsOn("databaseInitializer") // 声明:必须等 databaseInitializer 创建完才创建我
public class OrderService {
    @PostConstruct
    public void init() {
        // 此时可以安全地操作数据库,表结构已就绪
        System.out.println("OrderService 初始化,表已存在");
    }
}

注意@DependsOn 只控制创建顺序,不负责注入。A 和 B 之间仍然没有字段依赖,只是保证 A 先完成生命周期。销毁时顺序相反:B 先销毁,A 后销毁。

@DependsOn 也可以同时依赖多个 Bean:

@Component
@DependsOn({"databaseInitializer", "cacheWarmup"}) // 等这两个都完成再创建
public class OrderService { ... }

7.2 条件注册 Bean:@Conditional 系列

@ConditionalOnProperty —— 根据配置文件决定是否注册

最常用的条件注解,根据 application.properties / application.yml 中的配置项决定是否创建 Bean:

# application.yml
sms:
  enabled: true
@Component
@ConditionalOnProperty(
    name = "sms.enabled",       // 配置项名称
    havingValue = "true",       // 配置项的值等于 true 时才注册
    matchIfMissing = false      // 配置项不存在时不注册(默认 false)
)
public class SmsService {
    // 只有配置了 sms.enabled=true 时,这个 Bean 才会被创建
}

典型场景:功能开关、多环境差异化配置(开发环境用 Mock,生产环境用真实实现)。

@ConditionalOnBean —— 某个 Bean 存在时才注册

当容器中已经存在某个 Bean 时,才注册当前 Bean:

// 场景:只有注册了 DataSource Bean,才注册连接池监控
@Component
@ConditionalOnBean(DataSource.class) // 容器中存在 DataSource 才注册
public class DataSourceMonitor {
    @Autowired
    private DataSource dataSource; // 能安全注入,因为 DataSource 一定存在

    @PostConstruct
    public void init() {
        // 监控 DataSource 的连接池状态
    }
}

@ConditionalOnMissingBean —— 某个 Bean 不存在时才注册

当容器中不存在某个 Bean 时,才注册当前 Bean。这是 Spring Boot 自动配置的核心机制——用户自定义的 Bean 优先,没有才用默认的

// Spring Boot 内部的自动配置写法(简化)
@Configuration
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(RedisTemplate.class)
    // 如果用户自己定义了 RedisTemplate,就不注册这个默认的
    // 如果用户没有定义,才注册这个默认配置
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        return template;
    }
}

// 用户自定义(会覆盖自动配置)
@Configuration
public class MyRedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 自定义序列化方式等配置
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        return template;
    }
}

@ConditionalOnClass / @ConditionalOnMissingClass —— 根据类路径决定

当 classpath 中存在(或不存在)某个类时才注册,常用于 Spring Boot Starter 的自动配置:

@Configuration
@ConditionalOnClass(Jedis.class) // classpath 中有 Jedis 依赖才生效
public class JedisAutoConfiguration {
    // 只有引入了 jedis 依赖,才会自动配置 Jedis 相关 Bean
}

各条件注解对比

注解 触发条件 典型场景
@ConditionalOnProperty 配置项等于指定值 功能开关、环境差异化
@ConditionalOnBean 容器中存在某 Bean 依赖某 Bean 的扩展功能
@ConditionalOnMissingBean 容器中不存在某 Bean 提供默认实现,允许用户覆盖
@ConditionalOnClass classpath 中存在某类 Starter 自动配置
@ConditionalOnMissingClass classpath 中不存在某类 降级兼容处理
@ConditionalOnExpression SpEL 表达式为 true 复杂条件判断

7.3 延迟初始化:@Lazy

默认情况下,单例 Bean 在容器启动时就全部创建完毕(饿汉式)。@Lazy 让 Bean 推迟到第一次被使用时才创建(懒汉式):

@Component
@Lazy // 容器启动时不创建,第一次被注入或 getBean() 时才创建
public class HeavyReportService {
    @PostConstruct
    public void init() {
        // 耗时的初始化操作(加载大量数据、建立连接等)
        // 加了 @Lazy 后,这个操作推迟到第一次使用时才执行,不影响启动速度
    }
}

@Lazy 也可以加在注入点,只对这一处注入生效:

@Component
public class OrderController {
    @Autowired
    @Lazy // 只有这里的注入是懒加载,HeavyReportService 本身不需要加 @Lazy
    private HeavyReportService reportService;
}

注意@Lazy 加在注入点时,实际注入的是一个 CGLIB 代理对象,首次调用方法时才真正初始化目标 Bean。

注入点 @Lazy 的常见误区

误区:以为注入点加 @Lazy 能减少启动时间

这个想法是错的。注入点 @Lazy 的意义不是让"填充引用"变快,而是让目标 Bean 的创建推迟——但只要有任何一个地方正常注入了该 Bean,它就会在容器启动时被创建,注入点的 @Lazy 完全不起作用:

// HeavyReportService 类本身没有 @Lazy

@Component
public class OrderController {
    @Autowired
    @Lazy  // 这里懒加载,容器启动时 HeavyReportService 不会因为这里而创建
    private HeavyReportService reportService;
}

@Component
public class AdminController {
    @Autowired
    // 没有 @Lazy,正常注入 → 容器启动时就会创建 HeavyReportService
    private HeavyReportService reportService;
}
// 结论:AdminController 的正常注入已经触发了创建,OrderController 加 @Lazy 毫无意义

注入点 @Lazy 真正有意义的两个场景:

场景一:全局只有这一处注入(真正推迟甚至避免创建)

// HeavyReportService 只有 ReportController 用,且只在用户手动触发时才调用
@Component
public class ReportController {
    @Autowired
    @Lazy
    private HeavyReportService reportService;
    // 没有其他地方注入 HeavyReportService
    // → 如果用户从不触发报表功能,HeavyReportService 永远不会被创建
}

场景二:解决构造器循环依赖(最常见、最核心的用途)

// A 和 B 构造器互相依赖,Spring 无法解决 → 直接报错
// 注入点加 @Lazy,注入的是 B 的代理对象,打破循环
@Component
public class A {
    private final B b;

    public A(@Lazy B b) {  // 注入 B 的代理,此时 B 还没真正创建,不会死锁
        this.b = b;
    }
}
场景 注入点 @Lazy 有没有用
多处注入,其他地方正常注入 ❌ 没用,Bean 还是会被创建
全局只有这一处注入 ✅ 有用,推迟甚至避免创建
解决构造器循环依赖 ✅ 有用,这是最常见的用途

结论:注入点 @Lazy 用于性能优化的前提是全局只有这一处注入,否则意义不大。它更核心的价值是解决构造器循环依赖

适用场景:启动耗时长、占用资源多、但不是每次都用到的 Bean(如报表服务、数据导出服务)。


7.4 Bean 注册控制总览

flowchart TD
    A["需要控制 Bean 注册?"] --> B{"控制什么?"}
    B --> C["创建顺序\n(有副作用依赖)"]
    B --> D["是否创建\n(条件判断)"]
    B --> E["何时创建\n(启动 or 首次使用)"]

    C --> C1["@DependsOn\n声明前置依赖 Bean"]

    D --> D1["@ConditionalOnProperty\n根据配置项开关"]
    D --> D2["@ConditionalOnBean\n依赖某 Bean 存在"]
    D --> D3["@ConditionalOnMissingBean\n提供默认实现"]
    D --> D4["@ConditionalOnClass\nclasspath 有某类才生效"]

    E --> E1["@Lazy\n首次使用时才初始化"]

8. 面试高频问题

Q:Spring Bean 的生命周期是什么?

① 实例化(反射调用构造器)→ ② 属性注入(@Autowired)→ ③ Aware 回调 → ④ BeanPostProcessor before → ⑤ 初始化(@PostConstruct / afterPropertiesSet / init-method)→ ⑥ BeanPostProcessor after(AOP 代理在此创建)→ ⑦ 使用中 → ⑧ 销毁(@PreDestroy / destroy-method)

Q:@PostConstruct 和 init-method 的区别?

三者都在属性注入完成后执行,执行顺序:@PostConstructInitializingBean.afterPropertiesSetinit-method@PostConstruct 是 JSR-250 标准注解,init-method 是 Spring XML 配置方式,推荐使用 @PostConstruct

Q:Spring 如何解决循环依赖?

Spring 通过三级缓存解决字段注入的循环依赖:① 创建 A 时先将 A 的 ObjectFactory 放入三级缓存;② 注入 B 时发现 B 未创建,开始创建 B;③ B 需要注入 A,从三级缓存获取 A 的早期引用(可能是代理);④ B 初始化完成;⑤ A 完成 B 的注入,初始化完成。

Q:为什么需要三级缓存,二级缓存不够吗?

如果 A 有 AOP 代理,B 需要持有 A 的代理对象。三级缓存存的是 ObjectFactory,可以在需要时生成代理;如果只有二级缓存,存的是原始对象,B 持有的是未被代理的 A,导致 AOP 失效。

Q:构造器注入为什么不能解决循环依赖?

构造器注入在实例化阶段就需要依赖,此时 Bean 还未放入任何缓存,无法提前暴露引用,形成死锁。字段注入可以先创建空对象放入缓存,再注入依赖,所以能解决。

一句话口诀:实例化 → 注入 → Aware → BPP前 → 初始化 → BPP后(AOP代理)→ 使用 → 销毁;三级缓存提前暴露半成品,一级存完整 Bean,二级存早期引用,三级存工厂(支持 AOP 代理),构造器注入无法提前暴露所以不能解决循环依赖。