Spring-Boot全局懒加载机制解析

Spring一直被诟病启动时间慢,占用内存高,可Spring/SpringBoot官方是介绍为轻量级的框架。因为当Spring项目越来越大的时候,添加了很多依赖后,在启动时加载和初始化Bean就会变得越来越慢,其实很多时候我们在启动时并不需要加载全部的Bean,在调用时再加载就行,那这就需要懒加载的功能了,Spring提供了Layz注解,可以配置Bean是否需要懒加载,如下:

package com.example.lazyinitdemo;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

@Lazy
@Configuration
public class DemoComponent {

    public DemoComponent() {
        System.out.println("DemoComponent is init");
    }
}

项目启动后可以看到,DemoComponent并没有被初始化。

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.3)

2022-02-19 21:38:11.055  INFO 81075 --- [           main] c.e.l.LazyInitDemoApplication            : Starting LazyInitDemoApplication using Java 1.8.0_221 on LM-SHC-15009790 with PID 81075 
2022-02-19 21:38:11.057  INFO 81075 --- [           main] c.e.l.LazyInitDemoApplication            : No active profile set, falling back to default profiles: default
2022-02-19 21:38:11.388  INFO 81075 --- [           main] c.e.l.LazyInitDemoApplication            : Started LazyInitDemoApplication in 0.581 seconds (JVM running for 0.972)

Process finished with exit code 0

当我们把@Lazy注解去掉后,就可以看到DemoComponent is init被打印了出来,说明DemoComponent在启动时就被初始化了。

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.3)

2022-02-19 21:46:16.257  INFO 81213 --- [           main] c.e.l.LazyInitDemoApplication            : Starting LazyInitDemoApplication using Java 1.8.0_221 on LM-SHC-15009790 with PID 81213 (/Users/jqichen/Documents/Developer/projects/lazy-init-demo/target/classes started by jqichen in /Users/jqichen/Documents/Developer/projects/lazy-init-demo)
2022-02-19 21:46:16.258  INFO 81213 --- [           main] c.e.l.LazyInitDemoApplication            : No active profile set, falling back to default profiles: default
DemoComponent is init
2022-02-19 21:46:16.583  INFO 81213 --- [           main] c.e.l.LazyInitDemoApplication            : Started LazyInitDemoApplication in 0.544 seconds (JVM running for 0.919)

Process finished with exit code 0

全局懒加载

但是使用Lazy注解就要修改每一个Class,而且项目中会有很多依赖,这些依赖就无法使用注解来懒加载了。想要在Spring中实现全局懒加载也不是不可以,精力旺盛不嫌麻烦的话重写覆盖BeanFactoryPostProcessor就可以,但是在Spring2.2之后,我们通过配置就可以实现懒加载,如下:

spring.main.lazy-initialization=true

这时在上面的Demo中即使没有加@Lazy,日志中也并不会出现DemoComponent is init,如果依然想要在启动时加载Bean,只要添加@Lazy(false)注解就可以了。

源码解析

Spring Boot应用Main函数入口 Primary SourceSpringBoot 启动流程这两篇文章中有对SpringBoot如何启动,如何初始化Bean有详细的介绍,这里不在赘述。SpringBoot启动过程中,调用refresh时org.springframework.context.support.AbstractApplicationContext.refresh()有这么一段

public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

            // Prepare this context for refreshing.
            prepareRefresh();

            // Tell the subclass to refresh the internal bean factory.
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

            // Prepare the bean factory for use in this context.
            prepareBeanFactory(beanFactory);

            try {
                // Allows post-processing of the bean factory in context subclasses.
                postProcessBeanFactory(beanFactory);

                StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
                // Invoke factory processors registered as beans in the context.
                invokeBeanFactoryPostProcessors(beanFactory);

                // Register bean processors that intercept bean creation.
                registerBeanPostProcessors(beanFactory);
                beanPostProcess.end();

                // Initialize message source for this context.
                initMessageSource();

                // Initialize event multicaster for this context.
                initApplicationEventMulticaster();

                // Initialize other special beans in specific context subclasses.
                onRefresh();

                // Check for listener beans and register them.
                registerListeners();

                // Instantiate all remaining (non-lazy-init) singletons.
                finishBeanFactoryInitialization(beanFactory);

                // Last step: publish corresponding event.
                finishRefresh();
            }
            ......省略......

在最后调用了finishBeanFactoryInitialization(beanFactory) 可以看到注释// Instantiate all remaining (non-lazy-init) singletons. 初始化non-lazy-init的单例Bean。具体代码如下:

    /**
     * Finish the initialization of this context's bean factory,
     * initializing all remaining singleton beans.
     */
    protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
        // Initialize conversion service for this context.
        if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
                beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
            beanFactory.setConversionService(
                    beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
        }

        // Register a default embedded value resolver if no BeanFactoryPostProcessor
        // (such as a PropertySourcesPlaceholderConfigurer bean) registered any before:
        // at this point, primarily for resolution in annotation attribute values.
        if (!beanFactory.hasEmbeddedValueResolver()) {
            beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
        }

        // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
        String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
        for (String weaverAwareName : weaverAwareNames) {
            getBean(weaverAwareName);
        }

        // Stop using the temporary ClassLoader for type matching.
        beanFactory.setTempClassLoader(null);

        // Allow for caching all bean definition metadata, not expecting further changes.
        beanFactory.freezeConfiguration();

        // Instantiate all remaining (non-lazy-init) singletons.
        beanFactory.preInstantiateSingletons();
    }

这里又可以看到调用了beanFactory.preInstantiateSingletons();,通过注释可知,具体实现加载Bean的逻辑在preInstantiateSingletons方法中,继续跟下去:

    @Override
    public void preInstantiateSingletons() throws BeansException {
        if (logger.isTraceEnabled()) {
            logger.trace("Pre-instantiating singletons in " + this);
        }

        // Iterate over a copy to allow for init methods which in turn register new bean definitions.
        // While this may not be part of the regular factory bootstrap, it does otherwise work fine.
        List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

        // Trigger initialization of all non-lazy singleton beans...
        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 FactoryBean) {
                        FactoryBean<?> factory = (FactoryBean<?>) bean;
                        boolean isEagerInit;
                        if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
                            isEagerInit = AccessController.doPrivileged(
                                    (PrivilegedAction<Boolean>) ((SmartFactoryBean<?>) factory)::isEagerInit,
                                    getAccessControlContext());
                        }
                        else {
                            isEagerInit = (factory instanceof SmartFactoryBean &&
                                    ((SmartFactoryBean<?>) factory).isEagerInit());
                        }
                        if (isEagerInit) {
                            getBean(beanName);
                        }
                    }
                }
                else {
                    getBean(beanName);
                }
            }
        }
        ......省略......

重点在for循环中的if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()),这里可以看到初始化所有非抽象(abstract = false)、非懒加载(lazy-init=false)的单例Bean(scope=singleton),代码里有isLazyInit()的校验,所以设置lazy-init=true的bean都不会随着IOC容器启动而被实例加载。

全局懒加载Filter

解决以上其中一些问题可以在配置了全局懒加载的情况下,为一些需要在程序启动时就要加载的bean设置lazy init为false,而对于依赖库中的bean,我们也不可能覆盖所有的bean再加上@Lazy(false)的注解,这就需要一种代码改动最小的方式来实现这一需求,具体配置如下:

项目是全局懒加载,所以application.properties配置如下

#application.properties
spring.main.lazy-initialization=true 

DemoComponent会在初始化时打印DemoComponent is init,现在配置了全局懒加载,启动时应该是看不到打印的值的。

LazyInitializationExcludeFilter

可以指定规则实现 LazyInitializationExcludeFilter 来排除lazy init。

原理

@Bean
LazyInitializationExcludeFilter integrationLazyInitExcludeFilter() {
return LazyInitializationExcludeFilter.forBeanTypes(DemoConfig.class);
}

LazyInitializationExcludeFilter起作用是发生在LazyInitializationBeanFactoryPostProcessor

public final class LazyInitializationBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        Collection<LazyInitializationExcludeFilter> filters = getFilters(beanFactory);
        for (String beanName : beanFactory.getBeanDefinitionNames()) {
            BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
            if (beanDefinition instanceof AbstractBeanDefinition) {
                postProcess(beanFactory, filters, beanName, (AbstractBeanDefinition) beanDefinition);
            }
        }
    }

    private Collection<LazyInitializationExcludeFilter> getFilters(ConfigurableListableBeanFactory beanFactory) {
        // Take care not to force the eager init of factory beans when getting filters
        ArrayList<LazyInitializationExcludeFilter> filters = new ArrayList<>(
                beanFactory.getBeansOfType(LazyInitializationExcludeFilter.class, false, false).values());
        filters.add(LazyInitializationExcludeFilter.forBeanTypes(SmartInitializingSingleton.class));
        return filters;
    }

    private void postProcess(ConfigurableListableBeanFactory beanFactory,
            Collection<LazyInitializationExcludeFilter> filters, String beanName,
            AbstractBeanDefinition beanDefinition) {
        Boolean lazyInit = beanDefinition.getLazyInit();
        if (lazyInit != null) {
            return;
        }
        Class<?> beanType = getBeanType(beanFactory, beanName);
        if (!isExcluded(filters, beanName, beanDefinition, beanType)) {
            beanDefinition.setLazyInit(true);
        }
    }

    private Class<?> getBeanType(ConfigurableListableBeanFactory beanFactory, String beanName) {
        try {
            return beanFactory.getType(beanName, false);
        }
        catch (NoSuchBeanDefinitionException ex) {
            return null;
        }
    }

    private boolean isExcluded(Collection<LazyInitializationExcludeFilter> filters, String beanName,
            AbstractBeanDefinition beanDefinition, Class<?> beanType) {
        if (beanType != null) {
            for (LazyInitializationExcludeFilter filter : filters) {
                if (filter.isExcluded(beanName, beanDefinition, beanType)) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

}

应用

如果要把上文的DemoComponent排除在lazy init里, 可以实现这样一个LazyInitializationExcludeFilter Bean


    @Bean
        static LazyInitializationExcludeFilter eagerStompWebSocketHandlerMapping() {
            return (name, definition, type) -> name.equals("DemoComponent");
        }

这时再启动程序,就可以看到一下输出:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.3)

2022-02-27 00:30:38.532  INFO 38303 --- [           main] c.e.l.LazyInitDemoApplication            : Starting LazyInitDemoApplication using Java 1.8.0_221 on LM-SHC-15009790 with PID 38303 
2022-02-27 00:30:38.534  INFO 38303 --- [           main] c.e.l.LazyInitDemoApplication            : No active profile set, falling back to default profiles: default
DemoComponent is init
2022-02-27 00:30:38.846  INFO 38303 --- [           main] c.e.l.LazyInitDemoApplication            : Started LazyInitDemoApplication in 0.544 seconds (JVM running for 1.014)

Process finished with exit code 0

这时依然输出了DemoComponent is init说明即使在全局设置了懒加载的情况下,DemoComponent还是在启动时被加载了(postProcessBeanFactoryfinishBeanFactoryInitialization执行先后可以看refresh中的代码和上面提到的两篇文章,再在项目中debug设置断点就可知)。这样,我们就可以根据项目需要配置相关的bean不为懒加载,即使是依赖库中的bean,不能手动的为他们添加@Lazy(false),也能通过这样的方式在启动时加载。

Default 实现

可以看到在LazyInitializationBeanFactoryPostProcessor 里 会得到所有的LazyInitializationExcludeFilter BEAN 从而进行过滤。 在Spring boot 里实现了两个LazyInitializationExcludeFilter

  • ScheduledBeanLazyInitializationExcludeFilter
class ScheduledBeanLazyInitializationExcludeFilter implements LazyInitializationExcludeFilter {

    private final Set<Class<?>> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64));

    ScheduledBeanLazyInitializationExcludeFilter() {
        // Ignore AOP infrastructure such as scoped proxies.
        this.nonAnnotatedClasses.add(AopInfrastructureBean.class);
        this.nonAnnotatedClasses.add(TaskScheduler.class);
        this.nonAnnotatedClasses.add(ScheduledExecutorService.class);
    }

    @Override
    public boolean isExcluded(String beanName, BeanDefinition beanDefinition, Class<?> beanType) {
        return hasScheduledTask(beanType);
    }

    private boolean hasScheduledTask(Class<?> type) {
        Class<?> targetType = ClassUtils.getUserClass(type);
        if (!this.nonAnnotatedClasses.contains(targetType)
                && AnnotationUtils.isCandidateClass(targetType, Arrays.asList(Scheduled.class, Schedules.class))) {
            Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetType,
                    (MethodIntrospector.MetadataLookup<Set<Scheduled>>) (method) -> {
                        Set<Scheduled> scheduledAnnotations = AnnotatedElementUtils
                                .getMergedRepeatableAnnotations(method, Scheduled.class, Schedules.class);
                        return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : null);
                    });
            if (annotatedMethods.isEmpty()) {
                this.nonAnnotatedClasses.add(targetType);
            }
            return !annotatedMethods.isEmpty();
        }
        return false;
    }

}
  • ScheduledBeanLazyInitializationExcludeFilter 用在`TaskSchedulingAutoConfiguration.
  • 一个是WebSocketMessagingAutoConfiguration 的内部bean

        @Bean
        static LazyInitializationExcludeFilter eagerStompWebSocketHandlerMapping() {
            return (name, definition, type) -> name.equals("stompWebSocketHandlerMapping");
        }

对于那些独立启动,没有办法通过别人的调用而启动的就不能lazy init。 比如scheduler。 此时就需要提供LazyInitializationExcludeFilter

全局懒加载的问题

通过设置全局懒加载,我们可以减少启动时的创建任务从而大幅度的缩减应用的启动时间。但全局懒加载的缺点可以归纳为以下两点:

  • 在启动时没有加载,而是在第一次请求处理加载, 会导致第一次请求时间变长。之后的请求不受影响(说到这里自然而然的会联系到 spring cloud 启动后的第一次调用超时的问题)。
  • 错误不会在应用启动时抛出,不利于早发现、早解决。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容