@Lazy 详解
定义与作用
@Lazy 是 Spring 提供的延迟初始化(Lazy Initialization)注解,用于控制 Bean 的创建时机。默认情况下,Spring 容器在启动时会立即实例化所有 singleton 作用域的 Bean(称为 Eager Initialization,饥饿初始化)。当应用中存在大量 Bean,或某些 Bean 的创建成本极高(如需要连接远程服务、加载大量数据)时,启动过程会变得缓慢。@Lazy 让这些 Bean 在首次被请求时才创建,从而加速应用启动,并避免初始化那些可能永远不会被使用的组件。
在飞翔科技的学生管理系统中,运维工程师李眉发现应用启动需要 30 秒,其中 20 秒花在初始化各种报表生成器、邮件模板引擎和第三方 SDK 客户端上。CTO 大翔拍板:所有非核心路径的组件全部改为延迟初始化。小崔通过 @Lazy 实现了这一目标,启动时间缩短到 8 秒。
适用位置与常用属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value | boolean | true | 是否启用延迟初始化。@Lazy 等价于 @Lazy(true),@Lazy(false) 表示不延迟(饥饿初始化) |
适用位置:
- 类级别:与
@Component、@Service、@Bean等一起使用,标记该 Bean 延迟初始化 - 方法级别:与
@Bean一起使用 - 注入点级别:与
@Autowired、@Inject一起使用,标记该依赖延迟解析 - 配置类级别:与
@Configuration一起使用,标记该配置类中的所有@Bean方法延迟初始化
核心原理
Spring 容器在注册 BeanDefinition 时,会记录其 lazyInit 属性(默认为 false)。在容器启动的**预实例化(Pre-instantiation)**阶段:
- 对于
lazyInit = false的 singleton Bean,容器立即调用getBean()创建实例 - 对于
lazyInit = true的 singleton Bean,容器跳过预实例化,仅在后续某个 Bean 依赖它、或显式调用getBean()时才创建
当 @Lazy 标注在注入点时,Spring 不会注入真实的 Bean 实例,而是注入一个代理对象(Proxy)。代理对象在方法被调用时,才会触发目标 Bean 的真正创建和初始化。
完整示例
场景简述
飞翔科技的学生管理系统中,ReportEngine(报表引擎)需要加载 500MB 的字体库和模板文件,EmailTemplateLoader(邮件模板加载器)需要解析 200 个 Freemarker 模板。这两个组件在应用启动时就被初始化,但上线后 90% 的请求根本不生成报表或发邮件。小崔将它们标记为 @Lazy,并在外层服务中通过 @Lazy 注入点延迟解析依赖。
操作前:启动缓慢的应用
@Service
public class ReportEngine {
public ReportEngine() {
// 模拟耗时初始化
System.out.println("[ReportEngine] 正在加载字体库和模板...");
try {
Thread.sleep(5000); // 模拟 5 秒加载时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("[ReportEngine] 初始化完成");
}
public byte[] generatePdf(String templateName, Map<String, Object> data) {
// 生成 PDF 逻辑
return new byte[1024];
}
}
@Service
public class StudentService {
private final ReportEngine reportEngine; // 启动时立即注入,触发 ReportEngine 初始化
@Autowired
public StudentService(ReportEngine reportEngine) {
this.reportEngine = reportEngine;
}
public Student getStudent(Long id) {
// 90% 的请求只查数据库,根本不用报表引擎
return new Student(id, "小崔", 20, "计算机科学");
}
}
启动日志:
[ReportEngine] 正在加载字体库和模板...
[ReportEngine] 初始化完成
// 应用启动耗时:6.2 秒
痛点分析:
StudentService在启动时就需要ReportEngine,导致报表引擎被强制初始化- 绝大多数 API 请求只查询学生信息,报表引擎的 5 秒初始化完全浪费
- 如果
ReportEngine初始化失败(如字体库文件缺失),整个应用无法启动,即使该功能很少使用
使用该注解的完整代码
package com.feixiang.student.report;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
@Lazy
public class ReportEngine {
public ReportEngine() {
System.out.println("[ReportEngine] 正在加载字体库和模板...");
try {
Thread.sleep(5000); // 模拟 5 秒加载时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("[ReportEngine] 初始化完成");
}
public byte[] generatePdf(String templateName, Map<String, Object> data) {
System.out.println("[ReportEngine] 生成 PDF:" + templateName);
return new byte[1024];
}
}
package com.feixiang.student.service;
import com.feixiang.student.entity.Student;
import com.feixiang.student.report.ReportEngine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@Service
public class StudentService {
private final ReportEngine reportEngine;
// 注入点标注 @Lazy:Spring 注入代理对象,而非真实实例
@Autowired
public StudentService(@Lazy ReportEngine reportEngine) {
this.reportEngine = reportEngine;
}
public Student getStudent(Long id) {
// 查询学生信息,不触发 ReportEngine 初始化
return new Student(id, "小崔", 20, "计算机科学");
}
public byte[] exportStudentReport(Long id) {
// 首次调用 reportEngine 的方法时,代理触发真实 Bean 的创建
return reportEngine.generatePdf("student_report", Map.of("id", id));
}
}
package com.feixiang.student;
import com.feixiang.student.config.AppConfig;
import com.feixiang.student.service.StudentService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class StudentApplication {
public static void main(String[] args) {
long start = System.currentTimeMillis();
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
long bootTime = System.currentTimeMillis() - start;
System.out.println("容器启动耗时:" + bootTime + "ms");
StudentService studentService = ctx.getBean(StudentService.class);
// 第一次调用:不触发 ReportEngine
System.out.println("\n--- 查询学生信息 ---");
studentService.getStudent(1L);
// 第二次调用:触发 ReportEngine 初始化
System.out.println("\n--- 生成报表 ---");
studentService.exportStudentReport(1L);
// 第三次调用:ReportEngine 已初始化,直接复用
System.out.println("\n--- 再次生成报表 ---");
studentService.exportStudentReport(2L);
ctx.close();
}
}
操作后运行结果及分析
容器启动耗时:1200ms
--- 查询学生信息 ---
--- 生成报表 ---
[ReportEngine] 正在加载字体库和模板...
[ReportEngine] 初始化完成
[ReportEngine] 生成 PDF:student_report
--- 再次生成报表 ---
[ReportEngine] 生成 PDF:student_report
变化分析:
- 容器启动耗时从 6.2 秒降至 1.2 秒,
ReportEngine未被立即创建 getStudent()调用不触及ReportEngine,代理对象保持惰性,零开销exportStudentReport()首次调用时,代理对象触发ReportEngine的真正初始化(5 秒加载在此刻发生)- 第二次调用
exportStudentReport()时,ReportEngine已是完全初始化的单例,直接复用,无额外开销
@Lazy 在配置类上的全局控制
可以在 @Configuration 类上标注 @Lazy,让该类中所有 @Bean 方法默认延迟初始化:
@Configuration
@Lazy
public class HeavyInfrastructureConfig {
@Bean
public ReportEngine reportEngine() {
return new ReportEngine();
}
@Bean
public EmailTemplateLoader emailTemplateLoader() {
return new EmailTemplateLoader();
}
@Bean
@Lazy(false) // 例外:该 Bean 仍然饥饿初始化
public CoreDatabaseClient coreDatabaseClient() {
return new CoreDatabaseClient();
}
}
@Lazy 解决循环依赖
@Lazy 标注在注入点上时,Spring 注入的是代理对象而非真实实例,这可以打破构造器注入的循环依赖。
@Service
public class AService {
private final BService bService;
@Autowired
public AService(@Lazy BService bService) {
this.bService = bService; // 注入的是 BService 的代理
}
}
@Service
public class BService {
private final AService aService;
@Autowired
public BService(AService aService) {
this.aService = aService;
}
}
原理:AService 构造时不需要真实的 BService 实例,只需要一个代理。BService 构造时可以正常注入已创建完成的 AService。当 AService 后续调用 bService 的方法时,代理再触发真实 BService 的创建。
易错场景与面试考点
易错场景一:@Lazy Bean 的初始化异常延迟到运行期
@Service
@Lazy
public class ReportEngine {
public ReportEngine() {
// 假设字体库文件缺失
if (!new File("/fonts/student.ttf").exists()) {
throw new IllegalStateException("字体库文件缺失");
}
}
}
后果:应用启动时不会报错,看起来一切正常。直到第一个用户请求报表时,ReportEngine 初始化失败,抛出 BeanCreationException,此时应用已经对外提供服务,故障影响面更大。
正确做法:对于核心路径的组件,不应使用 @Lazy,让启动时失败(Fail-fast)比运行期失败更容易排查。@Lazy 只适用于非核心路径、初始化耗时高、失败不影响主流程的组件。
易错场景二:@Lazy 注入点与 final 字段冲突
@Service
public class StudentService {
private final ReportEngine reportEngine;
@Autowired
public StudentService(@Lazy ReportEngine reportEngine) {
this.reportEngine = reportEngine;
}
}
后果:编译通过,运行正常。但某些开发者误以为 @Lazy 注入的代理对象会在构造时触发初始化,担心 final 字段在构造后无法重新赋值。实际上代理对象在构造时就已经确定(它是一个合法的 Bean 引用),后续调用方法时只是代理内部转发到目标实例,final 字段始终指向同一个代理对象,不存在重新赋值问题。
正确理解:@Lazy 注入点注入的是代理引用,该引用在构造时确定且永不改变。final 与 @Lazy 完全兼容。
易错场景三:prototype 作用域 Bean 使用 @Lazy
@Component
@Scope("prototype")
@Lazy
public class TemporaryWorker { ... }
后果:@Lazy 对 prototype Bean 的效果有限。prototype Bean 本身就不在启动时创建,@Lazy 的延迟语义与 prototype 的"每次请求新建"语义叠加后,行为取决于注入方式:如果通过 @Lazy 注入点注入代理,每次调用代理方法时都会创建新的 prototype 实例(因为代理会重新调用 getBean());如果直接注入,则只在首次使用时创建一次。
正确做法:prototype + @Lazy 的组合通常不是预期行为,应避免混用。如果确实需要延迟创建 prototype 实例,使用 ObjectProvider.getObject() 显式控制。
面试考点
Q:@Lazy 标注在 Bean 上和标注在注入点上有什么区别?
标注在 Bean 上(类或
@Bean方法):控制该 Bean 自身的创建时机,容器启动时不实例化,首次被请求时才创建。标注在注入点上(构造器参数、字段):控制依赖的解析时机,Spring 注入一个代理对象,真实 Bean 的创建被推迟到代理方法首次被调用时。
Q:@Lazy 能否解决构造器循环依赖?
可以。当两个 Bean 通过构造器互相依赖时,在其中一个注入点上标注
@Lazy,Spring 会注入代理对象而非真实实例,从而打破"构造完成前必须持有对方"的死锁。这是解决构造器循环依赖的推荐方案之一(另一种是重构代码消除循环)。
Q:@Lazy 代理是 JDK 代理还是 CGLIB 代理?
取决于目标类是否实现接口。如果目标类实现了接口,Spring 默认使用 JDK 动态代理;如果没有接口,则使用 CGLIB 代理。可以通过
@Lazy配合@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)强制使用 CGLIB。
Q:Spring Boot 如何全局开启延迟初始化?
Spring Boot 2.2+ 提供了
spring.main.lazy-initialization=true配置,可以让应用中的所有 Bean 默认延迟初始化。这在开发环境非常有用,可以显著缩短启动时间。但生产环境不建议开启,因为首次请求延迟会影响用户体验,且运行期初始化失败的风险更高。
Q:@Lazy Bean 的销毁回调何时执行?
与普通的 singleton Bean 相同,在容器关闭时执行。
@Lazy只影响创建时机,不影响销毁时机和生命周期回调。一旦@LazyBean 被创建,它就成为正常的 singleton 实例,参与容器的完整生命周期管理。