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 Source,SpringBoot 启动流程这两篇文章中有对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 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) ((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 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 getFilters(ConfigurableListableBeanFactory beanFactory) {
        // Take care not to force the eager init of factory beans when getting filters
        ArrayList filters = new ArrayList<>(
                beanFactory.getBeansOfType(LazyInitializationExcludeFilter.class, false, false).values());
        filters.add(LazyInitializationExcludeFilter.forBeanTypes(SmartInitializingSingleton.class));
        return filters;
    }

    private void postProcess(ConfigurableListableBeanFactory beanFactory,
            Collection 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 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> 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> annotatedMethods = MethodIntrospector.selectMethods(targetType,
                    (MethodIntrospector.MetadataLookup>) (method) -> {
                        Set 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 启动后的第一次调用超时的问题)。
  • 错误不会在应用启动时抛出,不利于早发现、早解决。

你可能感兴趣的:(Spring-Boot全局懒加载机制解析)