bigywater.tech
Spring · 18

@Lazy 与懒加载:启动时间与首次请求代价的取舍

@Lazy 能把启动时间省多少、代价挪到了哪里?配置错误还能在启动时发现吗?注入点上的 @Lazy 和 Bean 上的 @Lazy 有什么区别?带 @EventListener 的 Bean 能不能懒加载?这篇在 Spring Framework 7.0.9 上逐项验证。

1263 → 86 ms
50 个构造要 20 ms 的单例,饿汉与懒加载的 refresh() 耗时
127 ms
懒加载之后第一次取其中 5 个的耗时,代价被挪到第一次使用
三处不推迟
@Bean @Lazy、@Configuration @Lazy 和普通单例上的 @EventListener Bean,在 refresh 时都被构造

省的是启动时间

实测 · 本机执行输出
  饿汉 10 个 Heavy(构造器各睡 20 ms):refresh() 312 ms;之后第一次取其中 5 个 0 ms;再取这 5 个 0 ms;已创建的单例数 23
  懒加载 10 个 Heavy(构造器各睡 20 ms):refresh() 65 ms;之后第一次取其中 5 个 124 ms;再取这 5 个 0 ms;已创建的单例数 18
  饿汉 50 个 Heavy(构造器各睡 20 ms):refresh() 1263 ms;之后第一次取其中 5 个 0 ms;再取这 5 个 0 ms;已创建的单例数 63
  懒加载 50 个 Heavy(构造器各睡 20 ms):refresh() 86 ms;之后第一次取其中 5 个 127 ms;再取这 5 个 0 ms;已创建的单例数 18

懒加载把构造代价从 refresh 挪到第一次使用,总工作量不变。

错误被推迟

实测 · 本机执行输出
  eager:refresh() 就失败了:UnsatisfiedDependencyException
  lazy:refresh() 成功(没有发现 Missing 缺失)
    第一次 getBean("fails") 才失败:UnsatisfiedDependencyException: No qualifying bean of type 'Missing' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

依赖缺失这类错误在懒加载下要到第一次使用才抛出。

注入点 @Lazy

实测 · 本机执行输出
  --- 场景 a
  1. 构造 Heavy(目标 Bean 是普通单例)
  2. 构造 User(heavy 字段是 @Lazy 注入)
  3. --- refresh 完成,调用 use()
  4. User 第一次调用 heavy.hi() 之前
  5. User 调用完 heavy.hi()
  6. heavy 字段的类型:Main$Heavy$$…(CGLIB 代理)
  --- 场景 b
  1. 构造 User(heavy 字段是 @Lazy 注入)
  2. --- refresh 完成,调用 use()
  3. User 第一次调用 heavy.hi() 之前
  4. 构造 Heavy(目标 Bean 标了 @Lazy)
  5. User 调用完 heavy.hi()
  6. heavy 字段的类型:Main$Heavy$$…(CGLIB 代理)
  --- 场景 c
  1. 构造 UserPlain(heavy 字段是普通 @Autowired)
  2. 构造 Heavy(目标 Bean 标了 @Lazy)
  3. --- refresh 完成,调用 use()
  4. UserPlain 第一次调用 heavy.hi() 之前
  5. UserPlain 调用完 heavy.hi()

只推迟解析,目标和注入点两处都 @Lazy 才推迟创建。

监听器

实测 · 本机执行输出
  --- 监听器 Bean 的懒加载情况:eager
  1. 构造 Lis(带 @EventListener 方法)
  2. Lis 收到 ContextRefreshedEvent
  3. --- refresh 完成,现在发布自定义事件
  4. Lis 收到自定义事件 E1
  --- 监听器 Bean 的懒加载情况:lazy
  1. 构造 Lis(带 @EventListener 方法)
  2. Lis 收到 ContextRefreshedEvent
  3. --- refresh 完成,现在发布自定义事件
  4. Lis 收到自定义事件 E1
  --- 监听器 Bean 的懒加载情况:all
  1. 构造 Lis(带 @EventListener 方法)
  2. Lis 收到 ContextRefreshedEvent
  3. --- refresh 完成,现在发布自定义事件
  4. Lis 收到自定义事件 E1

带 @EventListener 的 Bean 不会被推迟。

全局懒加载

实测 · 本机执行输出
  SpringApplication.setLazyInitialization(false):启动后单例 22 个;HeavyBean 已构造?true
  SpringApplication.setLazyInitialization(true):启动后单例 20 个;HeavyBean 已构造?false

SpringApplication.setLazyInitialization(true) 一次性让所有 Bean 懒加载。

分步演示

@Lazy 与懒加载
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用不带 Boot 的 Spring Framework 7.0.9,第五节用 SpringApplication(WebApplicationType.NONE);Heavy 的构造器用 Thread.sleep(20) 模拟耗时,毫秒数每次运行有几 ms 抖动(例如懒加载 50 个的 refresh 在几次运行里是 66 到 86 ms);带 @EventListener 的 Bean 不被推迟是根据现象下的结论,没有读源码
    • 没有验证:Boot 的 spring.main.lazy-initialization 配置项(只验证了 SpringApplication.setLazyInitialization 这个 API);全局懒加载下用 @Lazy(false) 排除个别 Bean;@Lazy 与 @Async 代理的组合;Web 应用里懒加载对第一个请求延迟的影响;懒加载对 Actuator 健康检查的影响
    京ICP备2026067502号-1京公网安备11010502063487号