SmartInitializingSingleton源码分析

SmartInitializingSingleton

一、基本信息

✒️ 作者 - Lex 📝 博客 - 我的CSDN 📚 文章目录 - 所有文章 🔗 源码地址 - SmartInitializingSingleton源码

二、接口描述

SmartInitializingSingleton接口,用于bean初始化,当所有单例bean都已完全初始化后,用此接口进行回调。

三、接口源码

SmartInitializingSingleton 是 Spring 框架自 4.1 版本开始引入的一个核心接口。其中afterSingletonsInstantiated()方法会在单例预实例化阶段结束时被调用。它保证所有常规的单例beans在此时已经被创建和初始化。

/**
 * 在BeanFactory启动时,单例预实例化阶段结束后触发的回调接口。
 * 单例beans可以实现此接口,以在常规单例实例化算法后执行某些初始化,
 * 避免因意外的早期初始化(例如,从ListableBeanFactory#getBeansOfType调用)引起的副作用。
 * 在这方面,它是InitializingBean的替代品,后者在bean的本地构建阶段结束时被触发。
 *
 * 这个回调变种与org.springframework.context.event.ContextRefreshedEvent有些类似,
 * 但不需要实现org.springframework.context.ApplicationListener,
 * 也不需要在上下文层次结构中过滤上下文引用等。它还意味着仅依赖于beans包,
 * 并由单独的ListableBeanFactory实现尊重,不仅仅在org.springframework.context.ApplicationContext环境中。
 *
 * 注意: 如果我们打算开始/管理异步任务,最好实现org.springframework.context.Lifecycle,
 * 它提供了一个更丰富的运行时管理模型,并允许分阶段启动/关闭。
 *
 * @author Juergen Hoeller
 * @since 4.1
 * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory#preInstantiateSingletons()
 */
public interface SmartInitializingSingleton {

    /**
     * 在单例预实例化阶段的末尾调用,
     * 保证所有常规单例beans已经创建。在此方法中的
     * ListableBeanFactory#getBeansOfType调用不会在引导期间引起意外的副作用。
     * 注意: 此回调不会为在BeanFactory启动后按需延迟初始化的单例beans触发,
     * 也不会触发任何其他bean范围。仅为具有预期引导语义的beans小心使用它。
     */
    void afterSingletonsInstantiated();

}

四、主要功能

  1. bean已完全初始化后回调
    • 提供了一个回调机制,允许单例bean在Spring容器中所有其他常规单例bean都已完全初始化之后,执行某些特定的初始化操作。具体来说,当所有的单例bean都被实例化和初始化后,SmartInitializingSingleton接口中的afterSingletonsInstantiated()方法会被调用。

五、最佳实践

首先来看看启动类入口,上下文环境使用AnnotationConfigApplicationContext(此类是使用Java注解来配置Spring容器的方式),构造参数我们给定了一个MyConfiguration组件类。

public class SmartInitializingSingletonApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
    }
}

这里使用@Bean注解,定义了一个Bean,是为了确保 MySmartInitializingSingleton 被 Spring 容器执行

@Configuration
@ComponentScan("com.xcs.spring.service")
public class MyConfiguration {

    @Bean
    public static MySmartInitializingSingleton mySmartInitializingSingleton(){
        return new MySmartInitializingSingleton();
    }
}

MySmartInitializingSingleton类中,在所有其他的单例bean被完全初始化后,然后在afterSingletonsInstantiated()方法会启动MyService类中定义的定时任务。

public class MySmartInitializingSingleton implements SmartInitializingSingleton {

    @Autowired
    private MyService myService;

    @Override
    public void afterSingletonsInstantiated() {
        myService.startScheduledTask();
    }
}

MyService定义了一个定时任务,该任务会每隔2秒打印出当前的日期时间和"hello world"消息。其中MySmartInitializingSingleton会在所有的单例bean完全初始化后,调用startScheduledTask()方法,从而启动定时任务。

@Service
public class MyService {

    /**
     * 这里使用了Java的Timer来模拟定时任务。在实际应用中,可能会使用更复杂的调度机制。
     */
    public void startScheduledTask() {
        new java.util.Timer().schedule(
                new java.util.TimerTask() {
                    @Override
                    public void run() {
                        System.out.println(getDate() + " hello world ");
                    }
                },
                0,
                2000
        );
    }

    /**
     * 获取当前时间
     *
     * @return
     */
    public String getDate() {
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return now.format(formatter);
    }
}

运行结果发现,MySmartInitializingSingleton成功地在所有其他的单例bean初始化后启动了MyService中的定时任务。我们的实现是正确的,每隔2秒都会产生下述输出。

2023-09-27 10:41:36 hello world 
2023-09-27 10:41:38 hello world 
2023-09-27 10:41:40 hello world 
2023-09-27 10:41:42 hello world 
2023-09-27 10:41:44 hello world 

六、时序图

sequenceDiagram
    Title: SmartInitializingSingleton时序图
    participant SmartInitializingSingletonApplication
    participant AnnotationConfigApplicationContext
    participant AbstractApplicationContext
    participant DefaultListableBeanFactory
    participant MySmartInitializingSingleton
    
    SmartInitializingSingletonApplication->>AnnotationConfigApplicationContext:AnnotationConfigApplicationContext(componentClasses)<br>创建上下文
    AnnotationConfigApplicationContext->>AbstractApplicationContext:refresh()<br>刷新上下文
    AbstractApplicationContext->>AbstractApplicationContext:finishBeanFactoryInitialization(beanFactory)<br>初始化Bean工厂
    AbstractApplicationContext->>DefaultListableBeanFactory:preInstantiateSingletons()<br>实例化单例
    DefaultListableBeanFactory->>MySmartInitializingSingleton:afterSingletonsInstantiated()<br>所有单例初始化

七、源码分析

public class SmartInitializingSingletonApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
    }
}

org.springframework.context.annotation.AnnotationConfigApplicationContext#AnnotationConfigApplicationContext构造函数中,执行了三个步骤,我们重点关注refresh()方法

public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
    this();
    register(componentClasses);
    refresh();
}

org.springframework.context.support.AbstractApplicationContext#refresh方法中我们重点关注一下finishBeanFactoryInitialization(beanFactory)这方法会对实例化所有剩余非懒加载的单列Bean对象,其他方法不是本次源码阅读的重点暂时忽略。

@Override
public void refresh() throws BeansException, IllegalStateException {
    // ... [代码部分省略以简化]
    // Instantiate all remaining (non-lazy-init) singletons.
    finishBeanFactoryInitialization(beanFactory);
    // ... [代码部分省略以简化]
}

org.springframework.context.support.AbstractApplicationContext#finishBeanFactoryInitialization方法中,会继续调用DefaultListableBeanFactory类中的preInstantiateSingletons方法来完成所有剩余非懒加载的单列Bean对象。

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
    // ... [代码部分省略以简化]
    // Instantiate all remaining (non-lazy-init) singletons.
    beanFactory.preInstantiateSingletons();
}

org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons方法中,实现SmartInitializingSingleton接口的beans在所有其他的单例beans完全实例化后才会触发其afterSingletonsInstantiated方法,从而确保了初始化的正确时序。

@Override
public void preInstantiateSingletons() throws BeansException {
    // ... [代码部分省略以简化]

    // 触发所有SmartInitializingSingleton bean的初始化后回调。。。
    for (String beanName : beanNames) {
        Object singletonInstance = getSingleton(beanName);
        if (singletonInstance instanceof SmartInitializingSingleton) {
            // ... [代码部分省略以简化]
            smartSingleton.afterSingletonsInstantiated();
            // ... [代码部分省略以简化]
        }
    }
}

八、注意事项

  1. 避免复杂逻辑

    • SmartInitializingSingleton的设计是为了执行初始化后的逻辑。避免在afterSingletonsInstantiated()方法中加入过于复杂的逻辑。
  2. 注意依赖关系

    • afterSingletonsInstantiated()被调用时,所有的常规单例bean都已经被初始化。但请确保在这个方法中调用的任何bean已经完全初始化并且所有的依赖都被满足。
  3. 避免早期初始化

    • 请确保不会意外地触发其他bean的早期初始化,尤其是在afterSingletonsInstantiated()方法中。早期初始化可能会导致不可预见的副作用。
  4. 限制范围

    • SmartInitializingSingleton仅对常规单例bean起作用。对于在BeanFactory启动后按需延迟初始化或其他作用域的beans(如原型作用域),此回调不会被触发。
  5. 异步任务

    • 如果我们的目的是启动或管理异步任务,最好使用Lifecycle接口或考虑其他Spring的启动监听器,如ApplicationListener<ContextRefreshedEvent>Lifecycle为运行时管理提供了一个更完善的模型。
  6. 确保幂等性

    • 如果有可能多次刷新应用程序上下文(虽然在我看来这种情况基本上很少),请确保afterSingletonsInstantiated()方法的实现是幂等的,即多次执行与一次执行产生的效果相同。
  7. InitializingBean@PostConstruct的区别

    • SmartInitializingSingletonInitializingBean@PostConstruct注解有区别。后两者是bean级别的初始化回调,而SmartInitializingSingleton是容器级别的,确保在所有bean初始化之后才执行。
  8. 不要滥用

    • 只有在确实需要确保所有其他bean都初始化后才执行某些操作时,才应使用SmartInitializingSingleton。如果不需要这种保证,考虑使用更标准的初始化回调。

八、总结

最佳实践总结

  1. 启动入口

    • 在示例的启动类SmartInitializingSingletonApplication中,我们使用了AnnotationConfigApplicationContext来加载和初始化Spring容器。我们为上下文提供了一个Java配置类MyConfiguration,该类定义了应该由Spring扫描和管理的bean。
  2. 配置

    • MyConfiguration类中,我们使用@Bean注解显式地定义了MySmartInitializingSingleton这个bean。这确保了MySmartInitializingSingleton被Spring容器管理并在适当的时机执行。
  3. 实现SmartInitializingSingleton接口

    • MySmartInitializingSingleton实现了SmartInitializingSingleton接口。当所有其他的单例bean都被完全初始化后,afterSingletonsInstantiated()方法被调用。在这个方法中,我们启动了MyService类中定义的定时任务。
  4. 定时任务

    • MyService中定义了一个使用Java的Timer模拟的定时任务。这个任务会每隔2秒打印当前时间和"hello world"这个消息。在实际应用中,可能会使用更复杂的调度机制,如Spring的TaskScheduler或Quartz等。
  5. 结果

    • 启动示例应用后,可以观察到每隔2秒在控制台上都会输出格式化的当前时间后跟着"hello world"这样的消息,证明定时任务已经成功启动并在运行。

源码分析总结

  1. 应用启动

    • 一切从SmartInitializingSingletonApplication的主函数开始,其中初始化了AnnotationConfigApplicationContext,这是Spring用于Java注解配置的上下文。
  2. AnnotationConfigApplicationContext构造函数

    • AnnotationConfigApplicationContext的构造函数中,执行了三个主要步骤,其中最关键的是refresh()方法。
  3. 执行refresh方法

    • refresh()方法是Spring上下文刷新的核心。在这里,重点是finishBeanFactoryInitialization(beanFactory),它负责实例化所有剩余的非懒加载单例Bean。
  4. 完成BeanFactory初始化

    • finishBeanFactoryInitialization方法中,为了完成上述任务,它进一步调用了beanFactory.preInstantiateSingletons()
  5. 预实例化单例

    • 这步是最关键的。在DefaultListableBeanFactorypreInstantiateSingletons方法中,所有单例beans都被实例化。紧接着,为那些实现了SmartInitializingSingleton接口的beans触发了afterSingletonsInstantiated回调,确保这些回调在所有其他单例beans完全实例化后才被执行。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容