0 背景
公司 SpringBoot 项目在日常开发过程中发现服务启动过程异常缓慢,常常需要6-7分钟才能暴露端口,严重降低开发效率。通过 SpringBoot 的 SpringApplicationRunListener
、BeanPostProcessor
原理和源码调试等手段排查发现,在 Bean 扫描和 Bean 注入这个两个阶段有很大的性能瓶颈。
通过 JavaConfig 注册 Bean, 减少 SpringBoot 的扫描路径,同时基于 Springboot 自动配置原理对第三方依赖优化改造,将服务本地启动时间从7min 降至40s 左右的过程。 本文会涉及以下知识点:
- 基于 SpringApplicationRunListener 原理观察 SpringBoot 启动 run 方法;
- 基于 BeanPostProcessor 原理监控 Bean 注入耗时;
- SpringBoot Cache 自动化配置原理;
- SpringBoot 自动化配置原理及 starter 改造;
1 耗时问题排查
SpringBoot 服务启动耗时排查,目前有2个思路:
- 排查 SpringBoot 服务的启动过程;
- 排查 Bean 的初始化耗时;
Spring Boot 基础就不介绍了,推荐看这个免费教程:
GitHub - javastacks/spring-boot-best-practice: Spring Boot 最佳实践,包括自动配置、核心原理、源码分析、国际化支持、调试、日志集成、热部署等。
1.1 观察 SpringBoot 启动 run 方法
该项目使用基于 SpringBoot 改造的内部微服务组件 XxBoot 作为服务端实现,其启动流程与 SpringBoot 类似,分为 ApplicationContext
构造和 ApplicationContext
启动两部分,即通过构造函数实例化 ApplicationContext
对象,并调用其 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);
}
ApplicationContext
对象构造过程,主要做了自定义 Banner 设置、应用类型推断、配置源设置等工作,不做特殊扩展的话,大部分项目都是差不多的,不太可能引起耗时问题。通过在 run
方法中打断点,启动后很快就运行到断点位置,也能验证这一点。
接下就是重点排查 run
方法的启动过程中有哪些性能瓶颈?SpringBoot 的启动过程非常复杂,庆幸的是 SpringBoot 本身提供的一些机制,将 SpringBoot 的启动过程划分了多个阶段,这个阶段划分的过程就体现在 SpringApplicationRunListener
接口中,该接口将 ApplicationContext
对象的 run
方法划分成不同的阶段:
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);
}
目前,SpringBoot 中自带的 SpringApplicationRunListener
接口只有一个实现类:EventPublishingRunListener
,该实现类作用:通过观察者模式的事件机制,在 run
方法的不同阶段触发 Event
事件,ApplicationListener
的实现类们通过监听不同的 Event
事件对象触发不同的业务处理逻辑。
通过自定义实现
ApplicationListener
实现类,可以在 SpringBoot 启动的不同阶段,实现一定的处理,可见SpringApplicationRunListener
接口给SpringBoot
带来了扩展性。
这里我们不必深究实现类 EventPublishingRunListener
的功能,但是可以通过 SpringApplicationRunListener
原理,添加一个自定义的实现类,在不同阶段结束时打印下当前时间,通过计算不同阶段的运行时间,就能大体定位哪些阶段耗时比较高,然后重点排查这些阶段的代码。
先看下 SpringApplicationRunListener
的实现原理,其划分不同阶段的逻辑体现在 ApplicationContext
的 run
方法中:
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(enviro