美文网首页spring frameworkalreadyspringboot
Spring-Boot全局懒加载机制解析

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

作者: 程序员札记 | 来源:发表于2022-02-28 08:50 被阅读0次

    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 启动后的第一次调用超时的问题)。
    • 错误不会在应用启动时抛出,不利于早发现、早解决。

    相关文章

      网友评论

        本文标题:Spring-Boot全局懒加载机制解析

        本文链接:https://www.haomeiwen.com/subject/utpylrtx.html