乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@Lazy 详解

定义与作用

@Lazy 是 Spring 提供的延迟初始化(Lazy Initialization)注解,用于控制 Bean 的创建时机。默认情况下,Spring 容器在启动时会立即实例化所有 singleton 作用域的 Bean(称为 Eager Initialization,饥饿初始化)。当应用中存在大量 Bean,或某些 Bean 的创建成本极高(如需要连接远程服务、加载大量数据)时,启动过程会变得缓慢。@Lazy 让这些 Bean 在首次被请求时才创建,从而加速应用启动,并避免初始化那些可能永远不会被使用的组件。

在飞翔科技的学生管理系统中,运维工程师李眉发现应用启动需要 30 秒,其中 20 秒花在初始化各种报表生成器、邮件模板引擎和第三方 SDK 客户端上。CTO 大翔拍板:所有非核心路径的组件全部改为延迟初始化。小崔通过 @Lazy 实现了这一目标,启动时间缩短到 8 秒。

适用位置与常用属性

属性类型默认值说明
valuebooleantrue是否启用延迟初始化。@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 只影响创建时机,不影响销毁时机和生命周期回调。一旦 @Lazy Bean 被创建,它就成为正常的 singleton 实例,参与容器的完整生命周期管理。

上一页
@Scope 详解