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);
}
springboot项目其实就是排查run
方法的启动过程中有哪些性能瓶颈?
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
事件对象触发不同的业务处理逻辑。
先看下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(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;
}
run
方法中getRunListeners(args)
通过SpringFactoriesLoader
加载classpath
下META-INF/spring.factotries
中配置的所有SpringApplicationRunListener
的实现类,通过反射实例化后,存到局部变量listeners
中,其类型为SpringApplicationRunListeners
;
然后在run
方法不同阶段通过调用listeners
的不同阶段方法来触发SpringApplicationRunListener
所有实现类的阶段方法调用。
因此,只要编写一个SpringApplicationRunListener
的自定义实现类,在实现接口不同阶段方法时,打印当前时间;
并在META-INF/spring.factotries
中配置该类后,该类也会实例化,存到listeners
中;
在不同阶段结束时打印结束时间,以此来评估不同阶段的执行耗时。
在项目中添加实现类MySpringApplicationRunListener
:
@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());
}
}
在resources
文件下的META-INF/spring.factotries
文件中配置上该类:
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=
com.xxx.ad.diagnostic.tools.api.MySpringApplicationRunListener
重启服务,观察MySpringApplicationRunListener
的日志输出,发现主要耗时都在contextLoaded
和started
两个阶段之间,在这两个阶段之间调用了2个方法:refreshContext
和afterRefresh
方法,而refreshContext
底层调用的是AbstractApplicationContext#refresh
,Spring 初始化 context 的核心方法之一就是这个refresh
。
至此基本可以断定,高耗时的原因就是在初始化 Spring 的 context,然而这个方法依然十分复杂,好在 refresh 方法也将初始化 Spring 的 context 的过程做了整理,并详细注释了各个步骤的作用:
通过简单调试,很快就定位了高耗时的原因:
- 在
invokeBeanFactoryPostProcessors(beanFactory)
方法中,调用了所有注册的BeanFactory
的后置处理器; - 其中,
ConfigurationClassPostProcessor
这个后置处理器贡献了大部分的耗时; - 查阅相关资料,该后置处理器相当重要,主要负责
@Configuration
、@ComponentScan
、@Import
、@Bean
等注解的解析; - 继续调试发现,主要耗时都花在主配置类的
@ComponentScan
解析上,而且主要耗时还是在解析属性basePackages
;
即项目主配置类上@SpringBootApplication
注解的scanBasePackages
属性:
查看相关代码,大体了解到该过程是在递归扫描、解析basePackages
所有路径下的 class,对于可作为 Bean 的对象,生成其BeanDefinition
;
如果遇到@Configuration
注解的配置类,还得递归解析其@ComponentScan
。至此,服务启动缓慢的原因就找到了。
弄明白耗时的原因后,我有2个疑问:
- 是否所有的 class 都需要扫描,是否可以只扫描那些提供 Bean 的 class?
- 扫描出来的 Bean 是否都需要?我只接入一个功能,但是注入了所有的 Bean,这似乎不太合理?
监控 Bean 注入耗时
第二个优化的思路是监控所有 Bean 对象初始化的耗时,即每个 Bean 对象实例化、初始化、注册所花费的时间,有没有特别耗时 Bean 对象?
同样的,我们可以利用 SpringBoot 提供了BeanPostProcessor
接口来监控 Bean 的注入耗时,BeanPostProcessor
是 Spring 提供的 Bean 初始化前后的 IOC 钩子,用于在 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;
}
}
对于BeanPostProcessor
接口的实现类,其前后置处理过程体现在AbstractAutowireCapableBeanFactory#doCreateBean
,这也是 Spring 中非常重要的一个方法,用于真正实例化 Bean 对象,通过BeanFactory#getBean
方法一路 Debug 就能找到。
在该方法中调用了initializeBean
方法:
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;
}
通过BeanPostProcessor
原理,在前置处理时记录下当前时间,在后置处理时,用当前时间减去前置处理时间,就能知道每个 Bean 的初始化耗时,下面是我的实现:
@Component
public class TimeCostBeanPostProcessor implements BeanPostProcessor {
private Map 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;
}
}
BeanPostProcessor
的逻辑是在Beanfactory
准备好后处理的,就不需要通过SpringFactoriesLoader
加载了,直接@Component
注入即可。
重启服务,通过以上方法排查 Bean 初始化过程,还真的有所发现:
这个 Bean 初始化耗时43s,具体看下这个 Bean 的初始化方法,发现会从数据库查询大量配置元数据,并更新到 Redis 缓存中,所以初始化非常慢:
另外,还发现了一些非项目自身服务的service、controller对象,这些 Bean 来自于第三方依赖:UPM服务,项目中并不需要:
其实,原因上文已经提到:我只接入一个功能,但我注入了该服务路径下所有的 Bean,也就是说,服务里注入其他服务的、对自身无用的 Bean。服务器托管网
那我们该如何来优化呢?
那么如何解决扫描路径过多?
首先我们删掉主配置类上扫描路径,使用 JavaConfig 的方式显式手动注入。
如上图,我们使用 Config 的改造方式是:不再扫描 UPM 的服务路径,而是主动注入。
删除”com.xxx.ad.upm”,并在服务路径下添加以下配置类:
@Configuration
public class ThirdPartyBeanConfig {
@Bean
public UpmResourceClient upmResourceClient() {
return new UpmResourceClient();
}
}
Tips:如果该 Bean 还依赖其他 Bean,则需要把所依赖的 Bean 都注入哦;
如何解决 Bean 初始化高耗时?
Bean 初始化耗时高,就需要 case by case 地处理了,比如项目中遇到的初始化配置元数据的问题,可以考虑通过将该任务提交到线程池的方式异步处理或者懒加载的方式来解决。
完成以上优化后,本地启动时间从之前的 8min 左右降低至 40s,效果还是非常显著的,今天的分享就跟大家分享到这!!
最后说一句(求关注!)
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。
关注公众号:woniuxgg,在公众号中回复:笔记就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net