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<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
还是在启动时被加载了(postProcessBeanFactory
和finishBeanFactoryInitialization
执行先后可以看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 启动后的第一次调用超时的问题)。
- 错误不会在应用启动时抛出,不利于早发现、早解决。