Skip to content

Spring @Lazy 懒加载与代理全过程

@Lazy 不是简单的“用到时才创建”。它至少有两种完全不同的工作方式:

  1. 标在 Bean 定义上,修改 BeanDefinition.lazyInit,让容器启动时不预实例化这个 singleton;
  2. 标在注入点上,先注入一个代理,代理直到方法调用时才解析真实依赖。

如果不区分这两条链路,就会出现最常见的误判:目标类已经写了 @Lazy,但仍在启动时创建;或者系统启动很快,却在第一笔真实请求中初始化失败。

学习目标

学完本页,你应当能够:

  • 说清 singleton 为什么默认在启动期创建,prototype 为什么本来就是按需创建;
  • 区分 Bean 定义级懒加载和注入点懒代理;
  • 解释 @Lazy 在类、@Bean 方法、配置类、构造器参数、字段上的不同效果;
  • BeanDefinitionfinishBeanFactoryInitialization、依赖解析和代理调用链解释原理;
  • 判断为什么标了 @Lazy 仍被提前创建;
  • 比较 @LazyObjectProviderSupplier、作用域代理和 AOP 代理;
  • 理解 @Lazy 如何打断一部分构造器循环依赖,以及为什么它不是首选设计;
  • 设计预热、健康检查、指标和生产排查方案;
  • 运行 JDK 8 + Spring 5 兼容 Demo,并知道 Spring 6 / Boot 3 的版本边界。

一、先建立正确模型

写法改变的对象启动时注入什么真正创建目标的时机
@Lazy @ComponentBeanDefinition 的 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) 提前创建。提前创建的目的不是浪费资源,而是让配置错误、构造异常、缺失依赖尽量在发布启动阶段暴露,并让首个请求避免承担大量初始化成本。

mermaid
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、健康检查或预热逻辑主动使用它。
mermaid
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

java
@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 放到注入点:

java
public OrderService(@Lazy ReportService reportService) {
    this.reportService = reportService;
}

此时注入的是代理,不需要立即取得真实目标。

3.2 @Configuration 上的 @Lazy

java
@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 常见内部链路是:

  1. AutowiredAnnotationBeanPostProcessor 找到构造器、字段或方法注入点;
  2. 它构造 DependencyDescriptor
  3. BeanFactory 调用 AutowireCandidateResolver.getLazyResolutionProxyIfNecessary()
  4. ContextAnnotationAutowireCandidateResolver 发现注入点存在 @Lazy(true)
  5. 使用 ProxyFactory 和延迟 TargetSource 创建代理;
  6. 创建当前 Bean 时只注入代理;
  7. 第一次代理方法调用才执行真实的 doResolveDependency()
  8. 找到目标 Bean 后执行其创建链,再把调用转发给目标。
mermaid
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 可以放在哪里

java
// 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

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();
        }
    }
}

关键输出顺序应为:

text
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

java
@Bean
@Lazy
ReportService reportService() {
    return new ReportServiceImpl();
}

@Bean
OrderService orderService(ReportService reportService) {
    // 普通参数需要真实对象,因此创建OrderService时仍会创建ReportService
    return new OrderService(reportService);
}

若改成:

java
@Bean
OrderService orderService(@Lazy ReportService reportService) {
    // 参数先得到代理,真实ReportService可以继续延迟
    return new OrderService(reportService);
}

判断是否真正延迟,不能只看目标定义上有没有注解,要沿着所有 eager 依赖者分析。

八、与 ObjectProvider 的区别

维度@Lazy TObjectProvider<T>
注入结果看起来仍是 T,实际为代理显式提供者对象
取得时机通常第一次方法调用代码调用 getObject/getIfAvailable
可选依赖目标不存在时可能延迟到调用才报错getIfAvailable 可明确返回空
多实现仍遵循候选选择可迭代、排序或流式取得
prototype代理语义容易被误读每次 getObject 的意图更清楚
业务可读性调用者看不出发生延迟代码明确表达按需获取

核心依赖只是初始化昂贵、且业务希望像普通接口一样调用时,可考虑 @Lazy。插件、真正可选能力、每次获取 prototype、需要显式控制获取时机时,ObjectProvider 通常更清晰。

java
@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代理事务、缓存、权限、日志等方法拦截目标通常已由容器管理
ScopedProxysingleton安全引用request/session等短作用域对象每次调用按当前作用域查目标
ObjectProvider显式按需、可选、多实例获取调用提供者API时

同一个 Bean 可能同时经历 Lazy 代理、ScopedProxy 和 AOP 代理,形成多层代理。排查时记录:

  • 注入点声明类型;
  • 最外层运行时 class;
  • 是否 JDK/CGLIB;
  • 最终目标类型;
  • 每层代理解决的职责。

不要为“少创建一个对象”盲目叠加代理,代理会增加类型限制、调试成本和首次调用的不确定性。

十、@Lazy 与循环依赖

假设 A 构造器需要 B,B 构造器需要 A:

java
class AService {
    AService(BService b) { }
}

class BService {
    BService(AService a) { }
}

创建 A 必须先有 B,创建 B 又必须先有 A,任何一方都无法先完成实例化。把某一侧改成注入点 Lazy:

java
class AService {
    AService(@Lazy BService bProxy) { }
}

容器不再立即创建 B,而是给 A 一个 B 的代理,因此 A 可以先完成。以后调用代理时,A 已进入容器,B 才能创建并注入 A。

mermaid
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+ 支持:

properties
spring.main.lazy-initialization=true

它通过容器扩展机制把大量 BeanDefinition 调整为 lazy,而不是修改 JVM 的对象分配策略。优点是本地开发、测试或极少使用功能的应用启动可能更快;代价是:

  • 缺 Bean、配置解析、网络连接和初始化异常推迟到请求期;
  • 第一次请求延迟上升,甚至超时;
  • readiness 通过不代表全部功能可用;
  • 容量评估可能漏掉初始化后的线程、连接和内存;
  • 故障从“发布失败”变成“用户流量失败”。

生产环境若启用,至少要有关键 Bean 排除策略、启动后预热、功能级健康检查、首调耗时指标和回滚方案。不能用全局 lazy 掩盖启动慢,应该先用 JFR、启动步骤指标和线程栈找出真正慢的构造器、初始化方法、远程 I/O 或锁等待。

十二、商业场景:低频报表引擎

订单服务有一个低频 PDF 报表引擎,初始化要加载字体、模板和本地库,耗时 3 秒、占用 300 MB。合理方案不是一句“加 @Lazy”,而是完成以下设计:

  1. 确认报表不是 readiness 必需能力;
  2. 注入点使用 Lazy 或 ObjectProvider,避免核心订单 Bean 启动时创建引擎;
  3. 报表接口设置独立超时、并发隔离和降级提示;
  4. 部署后后台预热常用模板,避免真实用户承担完整首调;
  5. 记录初始化耗时、成功/失败、内存增量和首份报表耗时;
  6. 初始化失败后明确能否重试,避免每个请求同时重复初始化;
  7. 多实例发布时逐实例预热,不能只预热一台;
  8. 升级模板或字体资源时验证缓存失效与回滚。

不这样做会怎样

只加 Lazy 而不做并发和预热,流量高峰中第一个请求可能触发昂贵初始化,后续请求排队;初始化若访问对象存储失败,系统虽显示启动成功,但用户批量导出全部失败。

十三、首次调用并发会发生什么

普通 singleton 的最终创建仍受 Spring singleton 创建锁和缓存协调,通常不会因为多个线程同时首次调用就成功创建多份 singleton。但其他线程可能等待初始化,初始化方法若执行远程 I/O 或持有业务锁,会放大尾延迟。

因此需要观察:

  • 首次创建开始、结束和失败次数;
  • 等待线程数量与线程栈;
  • 初始化是否持有数据库连接、分布式锁或线程池线程;
  • 上游超时是否短于初始化时间;
  • 失败后下一次请求是否再次触发初始化;
  • 初始化产生的资源是否在失败路径正确释放。

不要在构造器或 @PostConstruct 中进行无超时远程调用。若必须加载外部资源,应设计超时、缓存、重试上限、熔断和可观测状态。

十四、常见误区与后果

误区实际后果正确做法
目标类加 Lazy 就一定不启动创建eager依赖仍会解析真实Bean检查完整依赖图或在注入点延迟
全局 Lazy 等于启动优化完成故障与耗时转移到首个请求定位根因并设计预热
Lazy 能从根本解决循环依赖逻辑耦合仍存在优先重构依赖方向
Lazy 与 prototype 一样singleton目标最终仍复用明确作用域和获取语义
代理 class 就是目标 class类型判断、反射可能失败面向接口并识别代理
启动成功就说明功能健康lazy目标可能尚未创建功能级健康检查与烟测
首次调用只有一个用户受影响并发线程可能一起等待预热、超时、隔离、指标
给数据库连接池加 Lazy 就省资源首次流量才发现凭据或网络错误核心基础设施启动验证

十五、为什么标了 Lazy 仍提前创建

按以下顺序排查:

  1. 确认注解位置:目标定义还是注入点;
  2. 打印 BeanDefinition 的 isLazyInit(),确认元数据是否生效;
  3. 查找所有 eager singleton 对该 Bean 的普通依赖;
  4. 搜索 getBeanObjectProvider.getObject、Runner、Listener、health indicator 和预热逻辑;
  5. 检查 @DependsOn
  6. 检查 FactoryBean、BeanPostProcessor、第三方自动配置是否做类型发现或实例访问;
  7. 在目标构造器打断点或临时记录创建栈;
  8. 区分“配置类被处理”与“产品 Bean 已实例化”;
  9. 检查测试是否显式获取全部 Bean;
  10. 检查调试器、日志 toString 或序列化是否触发代理。

可在诊断环境临时输出:

java
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 系统启动正常,第一笔请求超时

  1. 从网关、应用日志和链路追踪定位首个慢 span;
  2. 采集请求期间线程栈,找 Bean 创建、类加载、连接池初始化、模板加载或远程 I/O;
  3. 查看该依赖是否是 Lazy 代理或全局 lazy;
  4. 对比目标构造器、@PostConstruct、init method 的耗时;
  5. 检查上游超时是否小于初始化时间;
  6. 临时止损可预热或摘除未就绪实例,长期拆掉远程初始化并增加超时。

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 3Java 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 可以完成。以后代理首次调用时再创建另一方,此时前一方已经可用。但它只打断创建时序,没有消除双向职责耦合,应优先重构。

@LazyObjectProvider 怎样选

Lazy 让调用方继续持有 T,但真实对象在代理首次调用时解析;ObjectProvider 把按需获取显式写进代码,并支持可选、迭代和每次获取。普通低频昂贵服务可用 Lazy,插件、可选能力、prototype 和需要精确控制获取时机时优先 ObjectProvider。

全局懒初始化有什么风险

它能缩短部分启动时间,但会把缺依赖、配置错误、连接失败和初始化耗时推迟到真实请求,造成首调慢或运行期故障。生产启用必须配合关键能力预热、功能健康检查、首调指标、并发隔离和回滚,不能代替启动慢根因分析。

二十、关联知识与验收

掌握验收:

  • 能画出定义级 lazy 从 BeanDefinition 到 preInstantiateSingletons 的跳过流程;
  • 能画出注入点 lazy 从 DependencyDescriptor 到代理首次解析目标的流程;
  • 能解释为什么目标有 Lazy 仍可能被 eager 依赖创建;
  • 能比较 singleton、prototype、ScopedProxy、Lazy 与 ObjectProvider;
  • 能运行 Demo 并根据日志证明真实目标的创建时机;
  • 能说明 Lazy 打断构造循环的边界和设计代价;
  • 能为首调延迟、初始化失败和并发等待给出生产 Runbook;
  • 能明确 JDK 8 / Spring 5 与 Spring 6 / Boot 3 的版本边界。