聊聊SpringBoot应用启动优化
聊聊Spring Boot 应用启动优化
随着业务发展,线上业务jar 包越来越大,动不动就几百兆,启动时间也越来越慢,严重影响效率。目前大部分
java
项目都是使用SpringBoot
,这篇文章就来简单聊聊关于SpringBoot
应用启动优化。
对于一个“历史悠久”的项目,业务代码自然是指数增长,而且在日常业务开发中,我们我都是在做加法,很少会去做删代码之类的操作。另外一般项目都会有很多中间件的初始化,比如:数据连接、redis
连接、mq
生产者和消费者注册、dubbo
生产者和消费者注册、定时任务等;对于各种中间件的初始化,大部分都会在开源工具上封装一层,所以这里就不细说了。这篇主要从业务代码如何后置处理,以及bean
加载两方面来简单聊聊启动优化。
启动时间分析
优化前要做的自然是分析启动过程,要分析启动过程,首先想到的应该是日志,将日志级别调为debug
,然后分析日志。当然我们也可以借助第三方工具,比如:Async Profiler
、JProfiler
等。idea
现在已经集成了Async Profiler
,选择Run *** with Async Profiler
启动项目,启动完成之后点击停止便可生成火焰图。
- 火焰图分析
y 轴表示调用栈,每一层都是一个函数。调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数。 x 轴表示抽样数,如果一个函数在 x 轴占据的宽度越宽,就表示它被抽到的次数多,即执行的时间长。注意,x 轴不代表时间,而是所有的调用栈合并后,按字母顺序排列的。
启动优化
业务代码优化
大部分的耗时应该都在业务太大或者蕴含大量的初始化逻辑,比方数据库连接、Redis
连接、各种连接池等等,对于业务方的倡议则是尽量减少不必要的依赖,能异步则异步。
启动时业务代码调整
很多业务场景需要在启动时预加载预处理数据等,我们经常都是使用@PostConstruct
注解来实现。被这个注解修饰的方法会在该类中所有注入操作完成之后执行,并且是在main
线程执行,如果执行时间过长,会导致启动阻塞。 类似加载数据到缓存这种,也可以在接口第一次调用时,将数据加载到缓存。也可以在项目启动完成后执行相应的方法,SpringBoot
提供了两种启动完成执行的接口,分别是ApplicationRunner
和CommandLineRunner
,这两个接口都有一个run()
方法,实现该方法,并使用@Component
注解使其成为bean
。如果存在多个实现这两个接口的类,为了使他们按一定顺序执行,可以使用@Order
注解或实现Ordered
接口。
加载优化
在Spring
中提供了Bean
后置处理器BeanPostProcessor
,BeanPostProcessor
提供了两个方法:
postProcessBeforeInitialization
:每一个bean对象的初始化方法调用之前回调postProcessAfterInitialization
:每个bean对象的初始化方法调用之后被回调
@Slf4j
@Component
public class BeanInitMetrics implements BeanPostProcessor, CommandLineRunner {
private Map<String, Long> stats = new HashMap<>();
private List<Metric> metrics = new ArrayList<>();
@Override
public void run(String... args) throws Exception {
/**
* 启动完成之后打印时间
*/
List<Metric> metrics = getMetrics();
log.info(JSON.toJSONString(metrics));
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
stats.put(beanName, System.currentTimeMillis());
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Long start = stats.get(beanName);
if (start != null) {
metrics.add(new Metric(beanName, Math.toIntExact(System.currentTimeMillis() - start)));
}
return bean;
}
public List<Metric> getMetrics() {
metrics.sort((o1, o2) -> {
try {
return o2.getValue() - o1.getValue();
}catch (Exception e){
return 0;
}
});
log.info("metrics {}", JSON.toJSONString(metrics));
return UnmodifiableList.unmodifiableList(metrics);
}
@Data
public static class Metric{
public Metric(String name, Integer value) {
this.name = name;
this.value = value;
this.createDate = new Date();
}
private String name;
private Integer value;
private Date createDate;
}
}
value
即为bean
初始化所花时间,单位为毫秒,根据启动时间排序后便能知道那些bean
初始化耗时大,然后处理相应的bean即可。
spring-contex-index
随着业务发展,项目越来越大,Spring
扫描的类也越来越多,启动速度自然也会越来越慢,Spring
从5
开始提供了spring-context-indexer
,可以通过在编译时创建候选对象的静态列表来提高大型应用程序的启动性能。 官方介绍: 在项目中使用了@Indexed
之后,编译打包的时候会在项目中自动生成META-INT/spring.components
文件。当Spring
应用上下文执行ComponentScan
扫描时,META-INT/spring.components
将会被CandidateComponentsIndexLoader
读取并加载,转换为CandidateComponentsIndex
对象,这样的话@ComponentScan
不在扫描指定的package
,而是读取CandidateComponentsIndex
对象,从而达到提升性能的目的。 引入依赖,在启动类上使用@Indexed
注解修饰即可
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<optional>true</optional>
</dependency>
需要注意是使用该模式之后,需要依赖的所有模块都使用此模式,不然会出现找不到bean
的情况。假设应用中存在一个包含META-INT/spring.components
资源的a.jar
,b.jar
仅存在模式注解,那么使用@ComponentScan
扫描这两个JAR
中的package
时,b.jar
中的模式注解不会被识别,因此会出现找不到b.jar
中的bean
的情况。 对于引入的jar
中存在bean
,且jar
中没有使用Indexed
模式,可以在项目资源路径创建META-INT/spring.components
文件,将jar
中的bean
手动添加到文件中,编译时不会覆盖手动添加。
延迟加载
SpringBoot2.2
开始提供了应用测试级别的延迟加载,将spring.main.lazy-initialization
设置为true
意味着应用程序中的所有bean将使用延迟初始化。这样做可以大大加快应用启动速度,不过首次访问速度会变慢,所以这种方式在测试预发环境使用比较合适。 除了使用spring.main.lazy-initialization
配置设置延迟加载也可以在启动时方法中使用SpringApplication
和SpringApplicationBuilder
来设置项目为延迟加载
SpringApplication application = new SpringApplication(ExpertsWebApplication.class);
application.setLazyInitialization(Boolean.TRUE);
application.run(args);
new SpringApplicationBuilder(ExpertsWebApplication.class)
.lazyInitialization(Boolean.TRUE).build(args)
.run();
以上配置方式影响上下文中的所有bean
。 如果想为特定bean
配置延迟初始化,可以通过@Lazy
注解来完成。 官网介绍
其他方面优化
- 很多大项目都是经过漫长的迭代才变得越来越庞大,所以及时清理无用代码是非常有必要的;如果有必要,可以拆分服务。
- 在有些项目中,可能会存在分布式定时任务、
mq
消费等,像定时任务通常会使用elasticjob
,而elasticjob
是每个定时任务单独初始化,初始化过程会连接zookeeper
。一般我们都会选择在项目启动过程去初始化,如果定时任务过多,启动过程自然也就变慢。类似这种情况,可以考虑像业务代码一样放到启动后初始化或者异步初始化。
以上便是从业务侧和bean
加载方面对SpringBoot
启动优化的一些建议和想法,如果有其他的想法欢迎评论区探讨😁😁😁
推荐阅读
招贤纳士
政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有 500 多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com
微信公众号
文章同步发布,政采云技术团队公众号,欢迎关注