Spring @Lazy 懒加载与代理全过程
@Lazy 不是简单的“用到时才创建”。它至少有两种完全不同的工作方式:
- 标在 Bean 定义上,修改
BeanDefinition.lazyInit,让容器启动时不预实例化这个 singleton; - 标在注入点上,先注入一个代理,代理直到方法调用时才解析真实依赖。
如果不区分这两条链路,就会出现最常见的误判:目标类已经写了 @Lazy,但仍在启动时创建;或者系统启动很快,却在第一笔真实请求中初始化失败。
学习目标
学完本页,你应当能够:
- 说清 singleton 为什么默认在启动期创建,prototype 为什么本来就是按需创建;
- 区分 Bean 定义级懒加载和注入点懒代理;
- 解释
@Lazy在类、@Bean方法、配置类、构造器参数、字段上的不同效果; - 从
BeanDefinition、finishBeanFactoryInitialization、依赖解析和代理调用链解释原理; - 判断为什么标了
@Lazy仍被提前创建; - 比较
@Lazy、ObjectProvider、Supplier、作用域代理和 AOP 代理; - 理解
@Lazy如何打断一部分构造器循环依赖,以及为什么它不是首选设计; - 设计预热、健康检查、指标和生产排查方案;
- 运行 JDK 8 + Spring 5 兼容 Demo,并知道 Spring 6 / Boot 3 的版本边界。
一、先建立正确模型
| 写法 | 改变的对象 | 启动时注入什么 | 真正创建目标的时机 |
|---|---|---|---|
@Lazy @Component | BeanDefinition 的 lazyInit | 若无人依赖则不创建 | 首次 getBean 或依赖解析 |
@Lazy @Bean | 该工厂方法对应定义 | 若无人依赖则不创建 | 首次需要该 Bean |
@Lazy @Configuration | 配置类中 Bean 的默认策略 | 各 Bean 默认不预实例化 | 各自首次需要时 |
OrderService(@Lazy ReportService r) | 依赖描述符 | 注入延迟解析代理 | 第一次通过代理调用目标方法 |
@Autowired @Lazy ReportService r | 依赖描述符 | 注入延迟解析代理 | 第一次通过代理调用目标方法 |
最关键的区别是:
- 定义级
@Lazy回答“容器是否在启动末尾主动创建这个 Bean”; - 注入点
@Lazy回答“当前依赖解析是否先返回一个代理”。
两者可以单独使用,也可以组合使用。
二、为什么 singleton 默认会提前创建
ApplicationContext.refresh() 后半段会进入 finishBeanFactoryInitialization(),其中调用 preInstantiateSingletons()。容器遍历 BeanDefinition:
- 是 singleton;
- 不是 abstract;
lazyInit不是 true;
就会通过 getBean(beanName) 提前创建。提前创建的目的不是浪费资源,而是让配置错误、构造异常、缺失依赖尽量在发布启动阶段暴露,并让首个请求避免承担大量初始化成本。
flowchart TD
A["ApplicationContext.refresh"] --> B["finishBeanFactoryInitialization"]
B --> C["preInstantiateSingletons遍历定义"]
C --> D{"singleton且非abstract?"}
D -- "否" --> E["跳过预实例化"]
D -- "是" --> F{"lazyInit=true?"}
F -- "是" --> E
F -- "否" --> G["getBean"]
G --> H["实例化、依赖注入、初始化"]
H --> I["放入singletonObjects"]2.1 prototype 本来就是按需创建
prototype 不会被 preInstantiateSingletons() 提前批量创建。每次 getBean 通常都会创建新实例,因此给 prototype 加定义级 @Lazy 并不能再获得“比按需更按需”的效果。
但 singleton 在启动时注入 prototype 时,prototype 会在 singleton 创建过程中产生一次,之后该引用长期保存在 singleton 中。若想每次业务调用都取得新 prototype,应使用 ObjectProvider<PrototypeBean>、lookup method 或作用域代理,而不是只给 prototype 加 @Lazy。
2.2 BeanFactory 与 ApplicationContext 的区别
单独使用基础 BeanFactory 时,通常按 getBean 需要创建;常用 ApplicationContext 会在 refresh 完成前预实例化非懒 singleton。因此讨论“Spring 默认是否立即创建 singleton”时,必须说明具体容器和启动流程。
三、定义级 @Lazy 怎样生效
组件扫描、配置类解析或 @Bean 方法解析时,Spring 把注解元数据写入 BeanDefinition 的 lazy-init 属性。启动末尾预实例化检查到该标志便跳过,但定义仍然存在于容器中。
“跳过”只代表此刻不主动创建,不代表永远不会创建。后续任何需要真实 Bean 的操作都可能触发 getBean:
- 业务代码显式调用
context.getBean; - 另一个 eager singleton 解析普通依赖;
@DependsOn要求先创建它;- 某个 BeanFactoryPostProcessor、BeanPostProcessor 或业务初始化器过早调用
getBean; - 类型查询、FactoryBean 类型预测或第三方框架扫描间接触发实例化;
- 事件监听器、Runner、健康检查或预热逻辑主动使用它。
flowchart TD
A["解析Lazy注解"] --> B["BeanDefinition.lazyInit=true"]
B --> C["启动预实例化阶段跳过"]
C --> D{"后续是否需要真实Bean?"}
D -- "否" --> E["保持未创建"]
D -- "是" --> F["执行getBean"]
F --> G["创建并初始化singleton"]
G --> H["缓存后重复使用"]3.1 为什么目标类标了 @Lazy 仍会启动创建
下面的 ReportService 虽然是 lazy,但 OrderService 是 eager,且构造器需要真实 ReportService:
@Component
@Lazy
public class ReportService {
}
@Service
public class OrderService {
private final ReportService reportService;
public OrderService(ReportService reportService) {
this.reportService = reportService;
}
}创建 OrderService 时,普通依赖解析会调用 getBean("reportService"),因此目标仍在启动期创建。定义级 lazy 只阻止“主动预实例化”,不能拒绝其他 Bean 的真实依赖。
若确实要把依赖解析也推迟,应把 @Lazy 放到注入点:
public OrderService(@Lazy ReportService reportService) {
this.reportService = reportService;
}此时注入的是代理,不需要立即取得真实目标。
3.2 @Configuration 上的 @Lazy
@Configuration
@Lazy
public class ReportConfiguration {
@Bean
public ReportClient reportClient() {
return new ReportClient();
}
@Bean
@Lazy(false)
public ReportHealthVerifier reportHealthVerifier() {
return new ReportHealthVerifier();
}
}配置类上的 @Lazy 为其中 @Bean 定义提供默认懒加载语义,单个方法可用 @Lazy(false) 覆盖。不要只根据配置类对象是否创建来判断其产品 Bean 是否已创建:Spring 为处理 @Bean 方法可能较早处理配置基础设施,但每个产品 Bean 仍按自身 BeanDefinition 决定实例化时机。
四、注入点 @Lazy 为什么需要代理
构造 OrderService 时必须给构造器传一个 Java 对象,容器不可能把“未来再说”作为参数。因此 Spring 创建一个类型兼容的代理对象先传进去。
Spring 5 常见内部链路是:
AutowiredAnnotationBeanPostProcessor找到构造器、字段或方法注入点;- 它构造
DependencyDescriptor; - BeanFactory 调用
AutowireCandidateResolver.getLazyResolutionProxyIfNecessary(); ContextAnnotationAutowireCandidateResolver发现注入点存在@Lazy(true);- 使用
ProxyFactory和延迟TargetSource创建代理; - 创建当前 Bean 时只注入代理;
- 第一次代理方法调用才执行真实的
doResolveDependency(); - 找到目标 Bean 后执行其创建链,再把调用转发给目标。
flowchart TD
A["发现注入点Lazy"] --> B["构造DependencyDescriptor"]
B --> C["创建延迟TargetSource"]
C --> D["ProxyFactory生成类型兼容代理"]
D --> E["代理注入当前Bean"]
E --> F["当前Bean完成启动"]
F --> G["业务首次调用代理方法"]
G --> H["doResolveDependency解析目标"]
H --> I["必要时创建并初始化目标Bean"]
I --> J["把调用转发给真实目标"]4.1 接口代理和类代理
- 注入点声明接口时,代理可实现该接口;
- 注入点声明具体类时,通常需要创建该类的子类代理;
final类不能被子类代理,final方法也不能按普通子类覆盖方式拦截;- 生产代码优先面向接口注入,减少代理实现约束。
不要通过 dependency.getClass() == ReportService.class 判断依赖类型。代理的运行时 class 不是目标 class;应使用接口契约,必要时使用 Spring 的代理工具判断。
4.2 第一次“看起来没调用业务”也可能触发创建
日志框架、调试器、JSON 序列化、Bean 校验或代码中的 toString() 可能访问代理。不要假设只有显式调用核心业务方法才会解析目标。排查“为什么提前创建”时要看完整调用栈,而不是只搜索业务方法。
4.3 目标是否会反复解析
对于普通 singleton 依赖,目标最终仍由 singleton 缓存保证同一容器内复用,延迟 TargetSource 也可在满足缓存条件时保存解析结果。对于 prototype、作用域 Bean 或动态提供者,生命周期语义不同,不能把懒代理理解为永远缓存一个目标。
五、@Lazy 可以放在哪里
// 1. 组件定义级
@Component
@Lazy
class ReportService { }
// 2. @Bean定义级
@Bean
@Lazy
ReportClient reportClient() { return new ReportClient(); }
// 3. 构造器参数:注入点代理
OrderService(@Lazy ReportService reportService) { }
// 4. 字段:注入点代理,不推荐把强制依赖设计为字段注入
@Autowired
@Lazy
private ReportService reportService;
// 5. Setter参数:注入点代理
@Autowired
void setReportService(@Lazy ReportService reportService) { }@Lazy(false) 可显式覆盖上层默认值。位置不同决定处理阶段不同,不能机械地认为所有 @Lazy 都只是修改 BeanDefinition。
六、完整可运行 Demo
以下 Demo 使用 JDK 8 语法与 Spring Framework 5.3,可直接保存为 LazyDemo.java:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Lazy;
public class LazyDemo {
interface ReportService {
String generate();
}
@Lazy
static class ReportServiceImpl implements ReportService {
ReportServiceImpl() {
System.out.println("2. real ReportService created");
}
public String generate() {
return "REPORT-OK";
}
}
static class OrderService {
private final ReportService reportService;
OrderService(@Lazy ReportService reportService) {
this.reportService = reportService;
System.out.println("1. OrderService created with proxy");
}
String finishOrder() {
return reportService.generate();
}
}
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext();
context.register(ReportServiceImpl.class, OrderService.class);
context.refresh();
try {
OrderService orderService = context.getBean(OrderService.class);
System.out.println("proxyClass="
+ orderService.reportService.getClass().getName());
System.out.println("before first business call");
System.out.println(orderService.finishOrder());
} finally {
context.close();
}
}
}关键输出顺序应为:
1. OrderService created with proxy
proxyClass=...Proxy...
before first business call
2. real ReportService created
REPORT-OK这个 Demo 同时使用两层语义:ReportServiceImpl 上的定义级 @Lazy 阻止容器在启动末尾独立预实例化目标;OrderService 构造器参数上的注入点 @Lazy 阻止创建 OrderService 时解析真实目标。缺少前者,目标可能仍被 preInstantiateSingletons 独立创建;缺少后者,创建 eager 的 OrderService 又会立即要求真实目标。
七、Bean 级懒加载对比 Demo
@Bean
@Lazy
ReportService reportService() {
return new ReportServiceImpl();
}
@Bean
OrderService orderService(ReportService reportService) {
// 普通参数需要真实对象,因此创建OrderService时仍会创建ReportService
return new OrderService(reportService);
}若改成:
@Bean
OrderService orderService(@Lazy ReportService reportService) {
// 参数先得到代理,真实ReportService可以继续延迟
return new OrderService(reportService);
}判断是否真正延迟,不能只看目标定义上有没有注解,要沿着所有 eager 依赖者分析。
八、与 ObjectProvider 的区别
| 维度 | @Lazy T | ObjectProvider<T> |
|---|---|---|
| 注入结果 | 看起来仍是 T,实际为代理 | 显式提供者对象 |
| 取得时机 | 通常第一次方法调用 | 代码调用 getObject/getIfAvailable |
| 可选依赖 | 目标不存在时可能延迟到调用才报错 | getIfAvailable 可明确返回空 |
| 多实现 | 仍遵循候选选择 | 可迭代、排序或流式取得 |
| prototype | 代理语义容易被误读 | 每次 getObject 的意图更清楚 |
| 业务可读性 | 调用者看不出发生延迟 | 代码明确表达按需获取 |
核心依赖只是初始化昂贵、且业务希望像普通接口一样调用时,可考虑 @Lazy。插件、真正可选能力、每次获取 prototype、需要显式控制获取时机时,ObjectProvider 通常更清晰。
@Service
public class ExportService {
private final ObjectProvider<PdfEngine> pdfEngineProvider;
public ExportService(ObjectProvider<PdfEngine> pdfEngineProvider) {
this.pdfEngineProvider = pdfEngineProvider;
}
public byte[] exportPdf(Order order) {
PdfEngine engine = pdfEngineProvider.getIfAvailable();
if (engine == null) {
throw new IllegalStateException("PDF能力未启用");
}
return engine.render(order);
}
}九、与其他代理的区别
| 机制 | 主要目的 | 目标获取时机 |
|---|---|---|
| Lazy注入代理 | 延迟解析依赖 | 首次代理调用时 |
| AOP代理 | 事务、缓存、权限、日志等方法拦截 | 目标通常已由容器管理 |
| ScopedProxy | singleton安全引用request/session等短作用域对象 | 每次调用按当前作用域查目标 |
ObjectProvider | 显式按需、可选、多实例获取 | 调用提供者API时 |
同一个 Bean 可能同时经历 Lazy 代理、ScopedProxy 和 AOP 代理,形成多层代理。排查时记录:
- 注入点声明类型;
- 最外层运行时 class;
- 是否 JDK/CGLIB;
- 最终目标类型;
- 每层代理解决的职责。
不要为“少创建一个对象”盲目叠加代理,代理会增加类型限制、调试成本和首次调用的不确定性。
十、@Lazy 与循环依赖
假设 A 构造器需要 B,B 构造器需要 A:
class AService {
AService(BService b) { }
}
class BService {
BService(AService a) { }
}创建 A 必须先有 B,创建 B 又必须先有 A,任何一方都无法先完成实例化。把某一侧改成注入点 Lazy:
class AService {
AService(@Lazy BService bProxy) { }
}容器不再立即创建 B,而是给 A 一个 B 的代理,因此 A 可以先完成。以后调用代理时,A 已进入容器,B 才能创建并注入 A。
flowchart TD
A["开始创建A"] --> B["发现B注入点Lazy"]
B --> C["创建B类型代理"]
C --> D["A构造完成并初始化"]
D --> E["业务调用B代理"]
E --> F["开始创建真实B"]
F --> G["B注入已经存在的A"]
G --> H["B初始化完成并执行方法"]为什么不推荐把它当常规方案
- A 与 B 的职责仍然互相耦合;
- 初始化错误被推迟到运行期;
- 代理类型可能影响测试、序列化和反射;
- B 的第一次调用可能突然变慢;
- 如果构造、
@PostConstruct或初始化逻辑又过早调用代理,仍可能产生创建中 Bean 异常; - 团队看不到真实依赖顺序,后续重构更困难。
优先方案是提取协调服务、拆分读写职责、发布领域事件或重新定义单向依赖。只有第三方集成、兼容旧系统等短期无法重构时,才把 Lazy 代理作为受控过渡,并补测试与监控。
十一、Spring Boot 全局懒初始化
Spring Boot 2.2+ 支持:
spring.main.lazy-initialization=true它通过容器扩展机制把大量 BeanDefinition 调整为 lazy,而不是修改 JVM 的对象分配策略。优点是本地开发、测试或极少使用功能的应用启动可能更快;代价是:
- 缺 Bean、配置解析、网络连接和初始化异常推迟到请求期;
- 第一次请求延迟上升,甚至超时;
- readiness 通过不代表全部功能可用;
- 容量评估可能漏掉初始化后的线程、连接和内存;
- 故障从“发布失败”变成“用户流量失败”。
生产环境若启用,至少要有关键 Bean 排除策略、启动后预热、功能级健康检查、首调耗时指标和回滚方案。不能用全局 lazy 掩盖启动慢,应该先用 JFR、启动步骤指标和线程栈找出真正慢的构造器、初始化方法、远程 I/O 或锁等待。
十二、商业场景:低频报表引擎
订单服务有一个低频 PDF 报表引擎,初始化要加载字体、模板和本地库,耗时 3 秒、占用 300 MB。合理方案不是一句“加 @Lazy”,而是完成以下设计:
- 确认报表不是 readiness 必需能力;
- 注入点使用 Lazy 或
ObjectProvider,避免核心订单 Bean 启动时创建引擎; - 报表接口设置独立超时、并发隔离和降级提示;
- 部署后后台预热常用模板,避免真实用户承担完整首调;
- 记录初始化耗时、成功/失败、内存增量和首份报表耗时;
- 初始化失败后明确能否重试,避免每个请求同时重复初始化;
- 多实例发布时逐实例预热,不能只预热一台;
- 升级模板或字体资源时验证缓存失效与回滚。
不这样做会怎样
只加 Lazy 而不做并发和预热,流量高峰中第一个请求可能触发昂贵初始化,后续请求排队;初始化若访问对象存储失败,系统虽显示启动成功,但用户批量导出全部失败。
十三、首次调用并发会发生什么
普通 singleton 的最终创建仍受 Spring singleton 创建锁和缓存协调,通常不会因为多个线程同时首次调用就成功创建多份 singleton。但其他线程可能等待初始化,初始化方法若执行远程 I/O 或持有业务锁,会放大尾延迟。
因此需要观察:
- 首次创建开始、结束和失败次数;
- 等待线程数量与线程栈;
- 初始化是否持有数据库连接、分布式锁或线程池线程;
- 上游超时是否短于初始化时间;
- 失败后下一次请求是否再次触发初始化;
- 初始化产生的资源是否在失败路径正确释放。
不要在构造器或 @PostConstruct 中进行无超时远程调用。若必须加载外部资源,应设计超时、缓存、重试上限、熔断和可观测状态。
十四、常见误区与后果
| 误区 | 实际后果 | 正确做法 |
|---|---|---|
| 目标类加 Lazy 就一定不启动创建 | eager依赖仍会解析真实Bean | 检查完整依赖图或在注入点延迟 |
| 全局 Lazy 等于启动优化完成 | 故障与耗时转移到首个请求 | 定位根因并设计预热 |
| Lazy 能从根本解决循环依赖 | 逻辑耦合仍存在 | 优先重构依赖方向 |
| Lazy 与 prototype 一样 | singleton目标最终仍复用 | 明确作用域和获取语义 |
| 代理 class 就是目标 class | 类型判断、反射可能失败 | 面向接口并识别代理 |
| 启动成功就说明功能健康 | lazy目标可能尚未创建 | 功能级健康检查与烟测 |
| 首次调用只有一个用户受影响 | 并发线程可能一起等待 | 预热、超时、隔离、指标 |
| 给数据库连接池加 Lazy 就省资源 | 首次流量才发现凭据或网络错误 | 核心基础设施启动验证 |
十五、为什么标了 Lazy 仍提前创建
按以下顺序排查:
- 确认注解位置:目标定义还是注入点;
- 打印 BeanDefinition 的
isLazyInit(),确认元数据是否生效; - 查找所有 eager singleton 对该 Bean 的普通依赖;
- 搜索
getBean、ObjectProvider.getObject、Runner、Listener、health indicator 和预热逻辑; - 检查
@DependsOn; - 检查 FactoryBean、BeanPostProcessor、第三方自动配置是否做类型发现或实例访问;
- 在目标构造器打断点或临时记录创建栈;
- 区分“配置类被处理”与“产品 Bean 已实例化”;
- 检查测试是否显式获取全部 Bean;
- 检查调试器、日志
toString或序列化是否触发代理。
可在诊断环境临时输出:
ConfigurableListableBeanFactory factory = context.getBeanFactory();
BeanDefinition bd = factory.getBeanDefinition("reportService");
System.out.println("lazyInit=" + bd.isLazyInit());
System.out.println("singletonCreated="
+ factory.containsSingleton("reportService"));containsBean 只说明定义或父容器中能找到,不等于实例已经创建;判断 singleton 是否已经进入本地缓存要使用更精确的 API,并结合容器层级。
十六、生产故障排查 Runbook
16.1 系统启动正常,第一笔请求超时
- 从网关、应用日志和链路追踪定位首个慢 span;
- 采集请求期间线程栈,找 Bean 创建、类加载、连接池初始化、模板加载或远程 I/O;
- 查看该依赖是否是 Lazy 代理或全局 lazy;
- 对比目标构造器、
@PostConstruct、init method 的耗时; - 检查上游超时是否小于初始化时间;
- 临时止损可预热或摘除未就绪实例,长期拆掉远程初始化并增加超时。
16.2 只有一个实例持续失败
比较该实例的配置、挂载文件、DNS、证书、字体/本地库、磁盘权限和依赖版本。Lazy 会让问题在该实例第一次命中特定功能时才出现,导致同批实例表现不一致。
16.3 初始化失败后请求反复失败
记录最深 cause,不要只看 BeanCreationException。确认失败对象是否进入 singleton 缓存、创建中的缓存是否清理、外部资源是否泄漏,以及每次重试是否会造成雪崩。核心依赖通常应让实例退出并重新发布;可恢复插件要设计显式状态机和退避重试,不能靠用户请求无限重试。
16.4 内存突然上涨
确认是否首次创建大模型、报表引擎、缓存或客户端连接池。对比创建前后的 heap、native memory、线程数、直接内存和类加载量。Lazy 只改变时间点,不减少对象创建后的稳态资源占用。
16.5 循环依赖在运行时暴露
查看 Lazy 代理第一次解析目标时的创建链,检查初始化方法是否反向调用尚未完成的 Bean。先止损相关功能,再把依赖图画成单向关系并重构;不要继续在更多位置叠加 Lazy。
十七、测试策略
至少覆盖:
- 启动后目标构造计数为 0;
- 取得依赖者但不调用目标时计数仍为 0;
- 第一次业务调用后计数为 1;
- 多线程首次调用不会得到多份 singleton;
- 初始化失败时异常在预期边界被记录和转换;
- 预热失败会阻止实例进入流量或触发告警;
- 目标被代理后事务、校验、缓存等 AOP 仍按预期生效;
- Spring/JDK 升级后代理类型和注解包兼容。
测试不要只断言接口返回值,还要断言创建时机。可在测试 Bean 构造器中增加 AtomicInteger,或用事件/指标记录初始化次数。
十八、版本边界
| 技术线 | 说明 |
|---|---|
| JDK 7/8 | 文中核心概念适用;Demo使用JDK8语法 |
| Spring 4.3+ | 单构造器可省略 @Autowired,但 @Lazy 注入点语义仍需明确 |
| Spring 5.3 | 常见JDK8生产线,可用本文 Demo 验证内部代理链 |
| Spring Boot 2.2+ | 支持 spring.main.lazy-initialization |
| Spring 6 / Boot 3 | Java 17基线,内部实现类细节可能演进,原理仍是定义标志与依赖代理两条链 |
学习源码时要以实际依赖版本为准,不要把 Spring 6 某个内部类名硬套到 Spring 5 项目。面试时先讲稳定机制,再补版本实现差异。
十九、面试标准回答
@Lazy 的原理是什么
定义级 @Lazy 把 BeanDefinition 标记为 lazy-init,使 ApplicationContext 在 preInstantiateSingletons 时跳过它,首次 getBean 才创建。注入点 @Lazy 则由依赖解析器和 ProxyFactory 创建延迟代理,当前 Bean 先注入代理,第一次方法调用时代理才执行真实依赖解析并取得目标。
为什么加了 @Lazy 仍然启动创建
若只标在目标定义上,它只阻止容器主动预实例化;其他 eager singleton 的普通依赖、@DependsOn、显式 getBean、Runner、监听器或第三方基础设施仍会要求真实目标并触发创建。要先确认注解位置,再沿依赖和调用栈找触发者。
@Lazy 怎么解决构造器循环依赖
在其中一个构造器参数上加 @Lazy,该位置先得到目标类型代理,不需要立即创建真实依赖,因此当前 Bean 可以完成。以后代理首次调用时再创建另一方,此时前一方已经可用。但它只打断创建时序,没有消除双向职责耦合,应优先重构。
@Lazy 与 ObjectProvider 怎样选
Lazy 让调用方继续持有 T,但真实对象在代理首次调用时解析;ObjectProvider 把按需获取显式写进代码,并支持可选、迭代和每次获取。普通低频昂贵服务可用 Lazy,插件、可选能力、prototype 和需要精确控制获取时机时优先 ObjectProvider。
全局懒初始化有什么风险
它能缩短部分启动时间,但会把缺依赖、配置错误、连接失败和初始化耗时推迟到真实请求,造成首调慢或运行期故障。生产启用必须配合关键能力预热、功能健康检查、首调指标、并发隔离和回滚,不能代替启动慢根因分析。
二十、关联知识与验收
- Spring IoC、BeanDefinition与getBean全过程
- Bean生命周期
@Autowired与@Resource注入全过程- 循环依赖与三级缓存
- Spring AOP代理与拦截器链
- Spring核心扩展点
- Spring面试知识点
掌握验收:
- 能画出定义级 lazy 从 BeanDefinition 到
preInstantiateSingletons的跳过流程; - 能画出注入点 lazy 从 DependencyDescriptor 到代理首次解析目标的流程;
- 能解释为什么目标有 Lazy 仍可能被 eager 依赖创建;
- 能比较 singleton、prototype、ScopedProxy、Lazy 与 ObjectProvider;
- 能运行 Demo 并根据日志证明真实目标的创建时机;
- 能说明 Lazy 打断构造循环的边界和设计代价;
- 能为首调延迟、初始化失败和并发等待给出生产 Runbook;
- 能明确 JDK 8 / Spring 5 与 Spring 6 / Boot 3 的版本边界。
