把SpringBoot项目启动从420秒优化到了40秒!
背景
-
基于 SpringApplicationRunListener 原理观察 SpringBoot 启动 run 方法; -
基于 BeanPostProcessor 原理监控 Bean 注入耗时; -
SpringBoot Cache 自动化配置原理; -
SpringBoot 自动化配置原理及 starter 改造;
耗时问题排查
-
排查 SpringBoot 服务的启动过程; -
排查 Bean 的初始化耗时;
1.1 观察 SpringBoot 启动 run 方法
public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) { return new SpringApplication(primarySources).run(args); }
public interface SpringApplicationRunListener { // run 方法第一次被执行时调用,早期初始化工作 void starting(); // environment 创建后,ApplicationContext 创建前 void environmentPrepared(ConfigurableEnvironment environment); // ApplicationContext 实例创建,部分属性设置了 void contextPrepared(ConfigurableApplicationContext context); // ApplicationContext 加载后,refresh 前 void contextLoaded(ConfigurableApplicationContext context); // refresh 后 void started(ConfigurableApplicationContext context); // 所有初始化完成后,run 结束前 void running(ConfigurableApplicationContext context); // 初始化失败后 void failed(ConfigurableApplicationContext context, Throwable exception); }
通过自定义实现 ApplicationListener 实现类,可以在 SpringBoot 启动的不同阶段,实现一定的处理,可见SpringApplicationRunListener 接口给 SpringBoot 带来了扩展性。
public ConfigurableApplicationContext run(String... args) { ... // 加载所有 SpringApplicationRunListener 的实现类 SpringApplicationRunListeners listeners = getRunListeners(args); // 调用了 starting listeners.starting(); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // 调用了 environmentPrepared ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); Banner printedBanner = printBanner(environment); context = createApplicationContext(); exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); // 内部调用了 contextPrepared、contextLoaded prepareContext(context, environment, listeners, applicationArguments, printedBanner); refreshContext(context); afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } // 调用了 started listeners.started(context); callRunners(context, applicationArguments); } catch (Throwable ex) { // 内部调用了 failed handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { // 调用了 running listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; }
@Slf4j public class MySpringApplicationRunListener implements SpringApplicationRunListener { // 这个构造函数不能少,否则反射生成实例会报错 public MySpringApplicationRunListener(SpringApplication sa, String[] args) { } @Override public void starting() { log.info("starting {}", LocalDateTime.now()); } @Override public void environmentPrepared(ConfigurableEnvironment environment) { log.info("environmentPrepared {}", LocalDateTime.now()); } @Override public void contextPrepared(ConfigurableApplicationContext context) { log.info("contextPrepared {}", LocalDateTime.now()); } @Override public void contextLoaded(ConfigurableApplicationContext context) { log.info("contextLoaded {}", LocalDateTime.now()); } @Override public void started(ConfigurableApplicationContext context) { log.info("started {}", LocalDateTime.now()); } @Override public void running(ConfigurableApplicationContext context) { log.info("running {}", LocalDateTime.now()); } @Override public void failed(ConfigurableApplicationContext context, Throwable exception) { log.info("failed {}", LocalDateTime.now()); } }
# Run Listenersorg.springframework.boot.SpringApplicationRunListener=\com.xxx.ad.diagnostic.tools.api.MySpringApplicationRunListener
run 方法中是通过 getSpringFactoriesInstances 方法来获取 META-INF/spring.factotries 下配置的 SpringApplicationRunListener 的实现类,其底层是依赖 SpringFactoriesLoader 来获取配置的类的全限定类名,然后反射生成实例;
这种方式在 SpringBoot 用的非常多,如 EnableAutoConfiguration、ApplicationListener、ApplicationContextInitializer 等。


-
在 invokeBeanFactoryPostProcessors(beanFactory) 方法中,调用了所有注册的 BeanFactory 的后置处理器; -
其中,ConfigurationClassPostProcessor 这个后置处理器贡献了大部分的耗时; -
查阅相关资料,该后置处理器相当重要,主要负责@Configuration、@ComponentScan、@Import、@Bean 等注解的解析; -
继续调试发现,主要耗时都花在主配置类的 @ComponentScan 解析上,而且主要耗时还是在解析属性 basePackages;


-
作为数据平台,我们的服务引用了很多第三方依赖服务,这些依赖往往提供了对应业务的完整功能,所以提供的 jar 包非常大; -
扫描这些包路径下的 class 非常耗时,很多 class 都不提供 Bean,但还是花时间扫描了; -
每添加一个服务的依赖,都会线性增加扫描的时间;
-
是否所有的 class 都需要扫描,是否可以只扫描那些提供 Bean 的 class? -
扫描出来的 Bean 是否都需要?我只接入一个功能,但是注入了所有的 Bean,这似乎不太合理?
1.2 监控 Bean 注入耗时
public interface BeanPostProcessor { // 初始化前 default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } // 初始化后 default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } }
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) { ... Object wrappedBean = bean; if (mbd == null || !mbd.isSynthetic()) { // 应用所有 BeanPostProcessor 的前置方法 wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); } try { invokeInitMethods(beanName, wrappedBean, mbd); } catch (Throwable ex) { throw new BeanCreationException( (mbd != null ? mbd.getResourceDescription() : null), beanName, "Invocation of init method failed", ex); } if (mbd == null || !mbd.isSynthetic()) { // 应用所有 BeanPostProcessor 的后置方法 wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); } return wrappedBean; }
@Component public class TimeCostBeanPostProcessor implements BeanPostProcessor { private Map<String, Long> costMap = Maps.newConcurrentMap(); @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { costMap.put(beanName, System.currentTimeMillis()); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (costMap.containsKey(beanName)) { Long start = costMap.get(beanName); long cost = System.currentTimeMillis() - start; if (cost > 0) { costMap.put(beanName, cost); System.out.println("bean: " + beanName + "\ttime: " + cost); } } return bean; } }



2.1 如何解决扫描路径过多?


@Configuration public class ThirdPartyBeanConfig { @Bean public UpmResourceClient upmResourceClient() { return new UpmResourceClient(); } }
Tips:如果该 Bean 还依赖其他 Bean,则需要把所依赖的 Bean 都注入;针对 Bean 依赖情况复杂的场景梳理起来就比较麻烦了,所幸项目用到的服务 Bean 依赖关系都比较简单,一些依赖关系复杂的服务,观察到其路径扫描耗时也不是很高,就不处理了。
2.2 如何解决 Bean 初始化高耗时?
新的问题



3.1 SpringBoot 自动化装配,让人防不胜防

@SpringBootApplication 复合注解中集成了三个非常重要的注解:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan,其中 @EnableAutoConfiguration 就是负责开启自动化配置功能;
SpringBoot 中有多@EnableXXX 的注解,都是用来开启某一方面的功能,其实现原理也是类似的:通过 @Import 筛选、导入满足条件的自动化配置类。



上文多次提到@Import,这是 SpringBoot 中重要注解,主要有以下作用:
1、导入 @Configuration 注解的类;
2、导入实现了 ImportSelector 或 ImportBeanDefinitionRegistrar 的类;
3、导入普通的 POJO。
3.2 使用 starter 机制,开箱即用
# EnableAutoConfigurations org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.xxx.ad.rediscache.XxxAdCacheConfiguration
扫码领红包SpringBoot 的 EnableAutoConfiguration 自动配置原理还是比较复杂的,在加载自动配置类前还要先加载自动配置的元数据,对所有自动配置类做有效性筛选,具体可查阅 EnableAutoConfiguration 相关代码;
微信赞赏
支付宝扫码领红包
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。