草庐IT

一个@Lazy注解也能写上万字?

冰河 2023-04-13 原文

一、学习指引

Spring中的@Lazy注解真的可以实现Bean的延迟创建吗?

平时工作过程中,不知道大家有没有遇到过这样一种场景:应用程序可能会在启动的时候创建大量的对象,加载大量的配置文件来进行初始化工作。但是在程序运行的过程中,这些对象或者配置文件使用的频率并不是很频繁,甚至是只有个别很少使用的功能在使用这些配置文件。

此时,为了优化应用的启动性能,我们就可以对这些对象的创建和配置文件的加载进行延迟处理。也就是说,在应用启动的时候不去创建这些对象和加载配置文件,而是到触发某些功能操作时,再去创建这些对象和加载配置文件,这就是一种延迟处理的操作。

在设计模式的单例模式中,会分为懒汉模式和饿汉模式,其中,懒汉模式就是一种延迟创建对象的模式。

二、注解说明

关于@Lazy注解的一点点说明~~

对于单例Bean来说,如果不想在IOC容器启动的时候就创建Bean对象,而是在第一次使用时创建Bean对象,就可以使用@Lazy注解进行处理。

2.1 注解源码

@Lazy注解可以标注到类、方法、构造方法、参数和属性字段上,能够实现在启动IOC容器时,不创建单例Bean,而是在第一次使用时创建单例Bean对象。源码详见:org.springframework.context.annotation.Lazy。

/**
* @author Chris Beams
* @author Juergen Hoeller
* @since 3.0
*/
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Lazy {
boolean value() default true;
}

从源码可以看出,@Lazy注解是从Spring3.0版本开始提供的注解,其中,只提供了一个boolean类型的value属性,具体含义如下所示。

  • value:boolean类型的属性,表示是否延迟创建单例Bean,默认值为true。

true:表示延迟创建单例Bean,此时在IOC启动时不会创建Bean对象,而是在第一次使用时创建单例Bean对象。

false:表示不延迟创建单例Bean对象,IOC容器启动时,就会创建单例Bean对象。

注意:使用@Lazy直接延迟创建单例Bean,不是延迟加载思想,因为不是每次使用时都创建,只是改变了第一次创建单例Bean的时机。

2.2 使用场景

在实际开发过程中,如果使用Spring创建的Bean是单例对象时,有时并不是每个单例Bean对象都需要在IOC容器启动时就创建,有些单例Bean可以在使用的时候再创建。此时,就可以使用@Lazy注解实现这样的场景。

注意:@Lazy注解只对单例Bean对象起作用,如果使用@Scope注解指定为多例Bean对象,则@Lazy注解将不起作用。

三、使用案例

@Lazy的实现案例,我们一起实现吧~~

本节,就使用@Lazy注解实现延迟创建Bean的案例。本节主要从创建单例Bean、添加@Lazy注解和获取单例Bean三个方面实现案例程序。

3.1 创建单例Bean

本小节,完成创建单例Bean的案例部分,具体步骤如下所示。

(1)新增LazyBean类

LazyBean类的源码详见:spring-annotation-chapter-09工程下的io.binghe.spring.annotation.chapter09.bean.LazyBean。

public class LazyBean {
public LazyBean(){
System.out.println("执行LazyBean类的构造方法...");
}
}

可以看到,LazyBean类就是一个普通的实体类对象,在LazyBean类的构造方法中,打印了执行LazyBean类的构造方法...的日志。

(2)新增LazyConfig类

LazyConfig类的源码详见:spring-annotation-chapter-09工程下的io.binghe.spring.annotation.chapter09.config.LazyConfig。

@Configuration
public class LazyConfig {
@Bean
public LazyBean lazyBean(){
return new LazyBean();
}
}

可以看到,LazyConfig类是Spring中的配置类,在LazyConfig类中使用@Bean注解创建了LazyBean类的单例Bean对象,同时在lazyBean()方法上并没有标注@Lazy注解。

(3)新增LazyTest类

LazyTest类的源码详见:spring-annotation-chapter-09工程下的io.binghe.spring.annotation.chapter09.LazyTest。

public class LazyTest {
public static void main(String[] args) {
System.out.println("创建IOC容器开始...");
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LazyConfig.class);
System.out.println("创建IOC容器结束");
}
}

可以看到,LazyTest类主要是测试案例程序,在main()方法中,创建了IOC容器,并在创建IOC容器前后打印了相关的日志信息。

(4)运行LazyTest类

运行LazyTest类中的main()方法,输出的结果信息如下所示。

创建IOC容器开始...
执行LazyBean类的构造方法...
创建IOC容器结束...

从输出的结果信息可以看出,打印了LazyBean类的构造方法中输出的日志信息。

说明:Spring会在IOC容器启动时,创建单例Bean。

3.2 添加@Lazy注解

本小节在3.1小节的基础上,完成案例添加@Lazy注解的部分,具体实现步骤如下所示。

(1)修改LazyConfig类

在LazyConfig类的lazyBean()方法上添加@Lazy注解,如下所示。

@Bean
@Lazy
public LazyBean lazyBean(){
return new LazyBean();
}

(2)运行LazyTest类

运行LazyTest类中的main()方法,输出的结果信息如下所示。

创建IOC容器开始...
创建IOC容器结束...

可以看到,输出的结果信息中并没有打印LazyBean类的构造方法中输出的日志信息。

说明:在创建单实例Bean的方法上添加@Lazy注解时,当IOC容器启动时,并不会创建单例Bean。

3.3 获取单例Bean

本小节在3.2小节的基础上,完成案例获取单例Bean的部分,具体实现步骤如下所示。

(1)修改LazyTest类

在LazyTest类的main()方法中,创建完IOC容器,从IOC容器中多次获取LazyBean类的Bean对象,如下所示。

public class LazyTest {
public static void main(String[] args) {
System.out.println("创建IOC容器开始...");
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LazyConfig.class);
System.out.println("创建IOC容器结束...");
System.out.println("从IOC容器中获取Bean开始...");
LazyBean lazyBean1 = context.getBean(LazyBean.class);
LazyBean lazyBean2 = context.getBean(LazyBean.class);
System.out.println("(lazyBean1 是否等于 lazyBean2) ===>>> " + (lazyBean1 == lazyBean2));
System.out.println("从IOC容器中获取Bean结束...");
}
}

可以看到,在LazyTest类的构造方法中,创建完IOC容器中,从IOC容器中连续获取两次LazyBean类的Bean对象,并打印两次获取的Bean对象是否相等。

(2)运行LazyTest类

运行LazyTest类中的main()方法,输出的结果信息如下所示。

创建IOC容器开始...
创建IOC容器结束...
从IOC容器中获取Bean开始...
执行LazyBean类的构造方法...
(lazyBean1 是否等于 lazyBean2) ===>>> true
从IOC容器中获取Bean结束...

从输出的结果信息可以看出,从第一次从IOC容器中获取Bean对象时,打印了LazyBean类的构造方法中输出的日志信息,并且两次从IOC容器中获取到的Bean对象相同。

说明:当在创建单例Bean的方法上标注@Lazy注解时,启动IOC容器并不会创建对应的单例Bean对象,而是在第一次获取Bean对象时才会创建,同时,由于Spring创建的是单例Bean对象,所以,无论从IOC容器中获取多少次对象,每次获取到的Bean对象都是相同的。

四、源码时序图

结合时序图理解源码会事半功倍,你觉得呢?

本节,就以源码时序图的方式,直观的感受下@Lazy注解在Spring源码层面的执行流程。本节,主要从注册Bean、调用Bean工厂后置处理器和创建单例Bean三个方面分析源码时序图。

4.1 注册Bean的源码时序图

@Lazy注解涉及到的注册Bean的源码时序图如图9-1所示。

由图9-1可以看出,@Lazy注解在注册Bean的流程中涉及到LazyTest类、AnnotationConfigApplicationContext类、AnnotatedBeanDefinitionReader类、AnnotationConfigUtils类、BeanDefinitionReaderUtils类和DefaultListableBeanFactory类。具体的源码执行细节参见源码解析部分。

4.2 调用Bean后置处理器的源码时序图

@Lazy注解涉及到的调用Bean工厂后置处理器的源码时序图如图9-2~9-4所示。

由图9-2~9-4可以看出,@Lazy注解涉及到的调用Bean工厂后置处理器的流程涉及到LazyTest类、AnnotationConfigApplicationContext类、AbstractApplicationContext类、PostProcessorRegistrationDelegate类、ConfigurationClassPostProcessor类、ConfigurationClassParser类、ComponentScanAnnotationParser类、ClassPathBeanDefinitionScanner类、AnnotationConfigUtils类、BeanDefinitionReaderUtils类和DefaultListableBeanFactory类。具体的源码执行细节参见源码解析部分。

4.3  创建单例Bean的源码时序图

@Lazy注解涉及到的创建Bean的源码时序图如图9-5所示。

由图9-5可以看出,@Lazy注解涉及到的创建Bean的流程涉及到LazyTest类、AnnotationConfigApplicationContext类、AbstractApplicationContext类、DefaultListableBeanFactory类和AbstractBeanFactory类。具体的源码执行细节参见源码解析部分。

五、源码解析

源码时序图整清楚了,那就整源码解析呗!

本节,主要分析@Lazy注解在Spring源码层面的执行流程,结合源码执行的时序图,会理解的更加深刻。本节,同样会从注册Bean、调用Bean工厂后置处理器和创建单例Bean三个方面分析源码的执行流程。

5.1 注册Bean的源码流程

@Lazy注解在Spring源码层面注册Bean的执行流程,结合源码执行的时序图,会理解的更加深刻,本节的源码执行流程可以结合图9-1进行理解。

@Lazy注解涉及到的注册Bean的源码流程与第7章5.1小节@DependsOn注解涉及到的注册Bean的源码流程大体相同,只是在解析AnnotatedBeanDefinitionReader类的doRegisterBean()方法时,略有不同。本小节,就从AnnotatedBeanDefinitionReader类的doRegisterBean()方法开始解析。

(1)解析AnnotatedBeanDefinitionReader类的doRegisterBean()方法

源码详见:org.springframework.context.annotation.AnnotatedBeanDefinitionReader#doRegisterBean(ClassbeanClass, String name, Class<? extends Annotation>[] qualifiers, Suppliersupplier, BeanDefinitionCustomizer[] customizers)。重点关注如下代码片段。

private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name, @Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier, @Nullable BeanDefinitionCustomizer[] customizers) {
/***********省略其他代码************/
AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
if (qualifiers != null) {
for (Class<? extends Annotation> qualifier : qualifiers) {
if (Primary.class == qualifier) {
abd.setPrimary(true);
}
else if (Lazy.class == qualifier) {
abd.setLazyInit(true);
}
else {
abd.addQualifier(new AutowireCandidateQualifier(qualifier));
}
}
}
/**********省略其他代码************/
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}

可以看到,在AnnotatedBeanDefinitionReader类的doRegisterBean()方法中,调用了AnnotationConfigUtils类的processCommonDefinitionAnnotations()方法。

(2)解析AnnotationConfigUtils类的processCommonDefinitionAnnotations()方法

源码详见:org.springframework.context.annotation.AnnotationConfigUtils#processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd)。

public static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd) {
processCommonDefinitionAnnotations(abd, abd.getMetadata());
}

可以看到,在AnnotationConfigUtils类的processCommonDefinitionAnnotations()方法中,直接调用了另一个重载的processCommonDefinitionAnnotations()方法。

(3)解析AnnotationConfigUtils类的processCommonDefinitionAnnotations()方法

源码详见:org.springframework.context.annotation.AnnotationConfigUtils#processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata)。

static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) {
AnnotationAttributes lazy = attributesFor(metadata, Lazy.class);
if (lazy != null) {
abd.setLazyInit(lazy.getBoolean("value"));
}
else if (abd.getMetadata() != metadata) {
lazy = attributesFor(abd.getMetadata(), Lazy.class);
if (lazy != null) {
abd.setLazyInit(lazy.getBoolean("value"));
}
}
/**********省略其他代码***********/
}

可以看到,在AnnotationConfigUtils类的processCommonDefinitionAnnotations()方法中,会解析@Lazy注解中的value属性,并将属性值存入abd对象的lazyInit字段中。

(4)回到AnnotatedBeanDefinitionReader类的doRegisterBean()方法。

可以看到,在方法中遍历qualifiers数组,如果Lazy.class的值与遍历出的qualifier对象相等,就会将abd对象的lazyInit字段设置为true。如果abd对象的lazyInit字段为true,则后续在启动IOC容器的过程中,就不会创建单例Bean对象。

后续的执行流程就与第7章5.1小节的执行流程相同,不再赘述。

至此,@Lazy注解涉及到的注册Bean的源码流程分析完毕。

5.2 调用Bean后置处理器的源码流程

@Lazy注解在Spring源码层面调用Bean工厂后置处理器的执行流程,结合源码执行的时序图,会理解的更加深刻,本节的源码执行流程可以结合图9-2~9-4进行理解。

@Lazy注解涉及到的调用Bean后置处理器的源码流程,与第7章5.2小节@DependsOn注解涉及到的调用Bean后置处理器的源码流程大体相同,只是在解析ComponentScanAnnotationParser类的parse()方法和AnnotationConfigUtils类的processCommonDefinitionAnnotations()方法时,略有不同。

(1)解析ComponentScanAnnotationParser类的parse()方法

源码详见:org.springframework.context.annotation.ComponentScanAnnotationParser#parse(AnnotationAttributes componentScan, String declaringClass)。重点关注如下代码片段。

public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, String declaringClass) {
/**********省略其他代码**********/
boolean lazyInit = componentScan.getBoolean("lazyInit");
if (lazyInit) {
scanner.getBeanDefinitionDefaults().setLazyInit(true);
}
/**********省略其他代码**********/
return scanner.doScan(StringUtils.toStringArray(basePackages));
}

可以看到,在ComponentScanAnnotationParser类的parse()方法中,会获取componentScan中的lazyInit属性,如果属性的值为true,会将scanner对象中beanDefinitionDefaults对象的lazyInit属性设置为true。

(2)解析AnnotationConfigUtils类的processCommonDefinitionAnnotations()方法

此时,与本章5.1节注册Bean的源码流程中解析AnnotationConfigUtils类的processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata)方法的流程相同。不再赘述。

后续源码的解析流程与第7章5.2小节解析源码的流程相同,这里不再赘述。

至此,@Lazy注解涉及到的调用Bean后置处理器的源码流程分析完毕。

5.3 创建单例Bean的源码流程

@Lazy注解在Spring源码层面创建单例Bean的执行流程,结合源码执行的时序图,会理解的更加深刻,本节的源码执行流程可以结合图9-5进行理解。

本节@Lazy注解创建单例Bean的源码流程,与第7章中5.3小节中@DependsOn注解创建单例Bean的源码流程大体相同,只是在DefaultListableBeanFactory类的preInstantiateSingletons()方法中略有差异。

DefaultListableBeanFactory类的preInstantiateSingletons()方法的源码详见:org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons()。重点关注如下代码片段。

@Override
public void preInstantiateSingletons() throws BeansException {
/************省略其他代码**************/
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
if (isFactoryBean(beanName)) {
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
if (bean instanceof SmartFactoryBean<?> smartFactoryBean && smartFactoryBean.isEagerInit()) {
getBean(beanName);
}
}
else {
getBean(beanName);
}
}
}
/************省略其他代码**************/
}

可以看到,在preInstantiateSingletons()方法中,会循环遍历解析出的Bean名称,在循环中,会根据遍历出的Bean名称获取RootBeanDefinition对象。接下来会进行如下判断。

if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
/*************省略其他代码*************/
}

可以看到,在preInstantiateSingletons()方法中,会判断每次遍历获取出的RootBeanDefinition对象中如果标记的不是抽象类,并且是单实例对象,并且没有设置延迟创建Bean。同时满足这些条件后,参会调用getbean()方法创建对应的Bean对象,并注入到IOC容器中。

所以,使用@Lazy注解指定延迟创建对象后,启动IOC容器时并不会创建对应的单例Bean,而是在第一次使用对应的Bean对象时,才会创建对应的单例Bean对象。

后续的源码执行流程与第7章5.3小节的源码执行流程相同,这里不再赘述。

至此,@Lazy注解在Spring源码层面创建单例Bean的执行流程分析完毕。

六、总结

@Lazy注解介绍完了,我们一起总结下吧!

本章,首先介绍了@Lazy注解的源码和使用场景,随后介绍了@Lazy的使用案例。接下来,详细介绍了@Lazy在Spring中执行的源码时序图和源码流程。

七、思考

既然学完了,就开始思考几个问题吧?

关于@Lazy注解,通常会有如下几个经典面试题:

  • @Lazy注解的作用是什么?
  • @Lazy注解有哪些使用场景?
  • @Lazy注解延迟创建Bean是如何实现的?
  • @Lazy注解在Spring内部的执行流程?
  • 你在平时工作中,会在哪些场景下使用@Lazy注解?
  • 你从@Lazy注解的设计中得到了哪些启发?

有关一个@Lazy注解也能写上万字?的更多相关文章

  1. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  2. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  3. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  4. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  5. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  6. ruby-on-rails - Rails - 从另一个模型中创建一个模型的实例 - 2

    我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案

  7. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  8. ruby - 一个 YAML 对象可以引用另一个吗? - 2

    我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的ruby​​yaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir

  9. ruby - Rails 关联 - 同一个类的多个 has_one 关系 - 2

    我的问题的一个例子是体育游戏。一场体育比赛有两支球队,一支主队和一支客队。我的事件记录模型如下:classTeam"Team"has_one:away_team,:class_name=>"Team"end我希望能够通过游戏访问一个团队,例如:Game.find(1).home_team但我收到一个单元化常量错误:Game::team。谁能告诉我我做错了什么?谢谢, 最佳答案 如果Gamehas_one:team那么Rails假设您的teams表有一个game_id列。不过,您想要的是games表有一个team_id列,在这种情况下

  10. ruby - 将一个超薄文件包含在另一个超薄文件中 - 2

    我在一个静态网站上工作(因此没有真正的服务器支持),我想在另一个网站中包含一个小的细长片段,可能会向它传递一个变量。这可能吗?在rails中很容易,虽然是render方法,但我不知道如何在slim上做(显然load方法不适用于slim)。 最佳答案 Slim包含Include插件,允许在编译时直接在模板文件中包含其他文件:require'slim/include'includepartial_name文档可在此处获得:https://github.com/slim-template/slim/blob/master/doc/incl

随机推荐