本文系统性分析并优化了一个Spring Boot项目启动耗时高达 280 秒的问题。通过识别瓶颈、优化分库分表加载逻辑、异步初始化耗时任务等手段,最终将启动耗时缩短至 159 秒,提升近 50%。文章涵盖启动流程分析、性能热点识别、异步初始化设计等关键技术细节,适用于大型Spring Boot项目的性能优化参考。

文章太长?1分钟看图抓住核心观点👇

图片

一、前言

随着业务的发展,笔者项目对应的Spring Boot工程的依赖越来越多。随着依赖数量的增长,Spring 容器需要加载更多组件、解析复杂依赖并执行自动装配,导致项目启动时间显著增长。在日常开发或测试过程中,一旦因为配置变更或者其他热部署不生效的变更时,项目重启就需要等待很长的时间影响代码的交付。加快Spring项目的启动可以更好的投入项目中,提升开发效率。

整体环境介绍:

  • Spring版本:4.3.22
  • Spring Boot版本:1.5.19
  • CPU:i5-9500
  • 内存:24GB
  • 优化前启动耗时:280秒

二、Spring Boot项目启动流程介绍

Spring Boot项目主要启动流程都在org.spring-

framework.boot.SpringApplication#run(java.lang.String...)方法中:

public ConfigurableApplicationContext run(String... args) {StopWatch stopWatch = new StopWatch();stopWatch.start();// Spring上下文ConfigurableApplicationContext context = null;FailureAnalyzers analyzers = null;configureHeadlessProperty();// 初始化SpringApplicationRunListener监听器SpringApplicationRunListeners listeners = getRunListeners(args);listeners.starting();try {ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);// 环境准备ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);// 打印bannerBanner printedBanner = printBanner(environment);// 创建上下文context = createApplicationContext();analyzers = new FailureAnalyzers(context);// 容器初始化prepareContext(context, environment, listeners, applicationArguments,printedBanner);// 刷新容器内容refreshContext(context);afterRefresh(context, applicationArguments);// 结束监听广播listeners.finished(context, null);stopWatch.stop();if (this.logStartupInfo) {new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);}return context;} catch (Throwable ex) {handleRunFailure(context, listeners, analyzers, ex);throw new IllegalStateException(ex);}
}

可以看到在启动流程中,监听器应用在了应用的多个生命周期中。并且Spring Boot中也预留了针对listener的扩展点。我们可以借此实现一个自己的扩展点去监听Spring Boot的每个阶段的启动耗时,实现如下:

@Slf4j
public class MySpringApplicationRunListener implements SpringApplicationRunListener{private Long startTime;public MySpringApplicationRunListener(SpringApplication application, String[] args){}@Overridepublic void starting(){startTime = System.currentTimeMillis();log.info("MySpringListener启动开始 {}", LocalTime.now());}@Overridepublic void environmentPrepared(ConfigurableEnvironment environment){log.info("MySpringListener环境准备 准备耗时:{}毫秒", (System.currentTimeMillis() - startTime));startTime = System.currentTimeMillis();}@Overridepublic void contextPrepared(ConfigurableApplicationContext context){log.info("MySpringListener上下文准备 耗时:{}毫秒", (System.currentTimeMillis() - startTime));startTime = System.currentTimeMillis();}@Overridepublic void contextLoaded(ConfigurableApplicationContext context){log.info("MySpringListener上下文载入 耗时:{}毫秒", (System.currentTimeMillis() - startTime));startTime = System.currentTimeMillis();}@Overridepublic void finished(ConfigurableApplicationContext context, Throwable exception){log.info("MySpringListener结束 耗时:{}毫秒", (System.currentTimeMillis() - startTime));startTime = System.currentTimeMillis();}
}

接着还需要在classpath/META-INF目录下新建spring.factories文件,并添加如下文件内容:

org.springframework.boot.SpringApplicationRunListener=com.vivo.internet.gameactivity.api.web.MySpringApplicationRunListener

至此,借助Listener机制,我们能够追踪Spring Boot启动各阶段的耗时分布,为后续性能优化提供数据支撑。

图片

contextLoaded事件是在run方法中的prepareContext()结束时调用的,因此contextLoaded事件和finished事件之间仅存在两个语句:refreshContext(context)和afterRefresh

(context,applicationArguements)消耗了285秒的时间,调试一下就能发现主要耗时在refreshContext()中。

三、AbstractApplicationContext#refresh

refreshContext()最终调用到org.spring-framework.context.support.AbstractApplicationContext#refresh方法中,这个方法主要是beanFactory的预准备、对beanFactory完成创建并进行后置处理、向容器添加bean并且给bean添加属性、实例化所有bean。通过调试发现,finishBeanFactoryInitialization(beanFactory) 方法耗时最久。该方法负责实例化容器中所有的单例 Bean,是启动性能的关键影响点。

四、找出实例化耗时的Bean

Spring Boot也是利用的Spring的加载流程。在Spring中可以实现InstantiationAwareBeanPost-

Processor接口去在Bean的实例化和初始化的过程中加入扩展点。因此我们可以实现该接口并添加自己的扩展点找到处理耗时的Bean。

@Service
public class TimeCostCalBeanPostProcessor implements InstantiationAwareBeanPostProcessor {private Map<String, Long> costMap = Maps.newConcurrentMap();@Overridepublic Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {if (!costMap.containsKey(beanName)) {costMap.put(beanName, System.currentTimeMillis());}return null;}@Overridepublic boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {return true;}@Overridepublic PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {return pvs;}@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {return bean;}@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if (costMap.containsKey(beanName)) {Long start = costMap.get(beanName);long cost = System.currentTimeMillis() - start;// 只打印耗时长的beanif (cost > 5000) {System.out.println("bean: " + beanName + "\ttime: " + cost + "ms");}}return bean;}
}

具体原理就是在Bean开始实例化之前记录时间,在Bean初始化完成后记录结束时间,打印实例化到初始化的时间差获得Bean的加载总体耗时。结果如图:

图片

可以看到有许多耗时在10秒以上的类,接下来可以针对性的做优化。值得注意的是,统计方式为单点耗时计算,未考虑依赖链上下文对整体加载顺序的影响,实际优化还需结合依赖关系分析。

五、singletonDataSource

@Bean(name = "singletonDataSource")
public DataSource singletonDataSource(DefaultDataSourceWrapper dataSourceWrapper) throws SQLException {//先初始化连接dataSourceWrapper.getMaster().init();//构建分库分表数据源String dataSource0 = "ds0";Map<String, DataSource> dataSourceMap = new HashMap<>();dataSourceMap.put(dataSource0, dataSourceWrapper.getMaster());//分库分表数据源DataSource shardingDataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap,shardingRuleConfiguration, prop);return shardingDataSource;    }

singletonDataSource是一个分库分表的数据源,连接池采用的是Druid,分库分表组件采用的是公司内部优化后的中间件。通过简单调试代码发现,整个Bean耗时的过程发生在createDataSource方法,该方法中会调用createMetaData方法去获取数据表的元数据,最终运行到loadDefaultTables方法。该方法如下图,会遍历数据库中所有的表。因此数据库中表越多,整体就越耗时。

图片

笔者的测试环境数据库中有很多的分表,这些分表为了和线上保持一致,分表的数量都和线上是一样的。

图片

因此在测试环境启动时,为了加载这些分表会更加的耗时。可通过将分表数量配置化,使测试环境在不影响功能验证的前提下减少分表数量,从而加快启动速度。

六、初始化异步

activityServiceImpl启动中,主要会进行活动信息的查询初始化,这是一个耗时的操作。类似同样的操作在工程的其他类中也存在。

@Service
public class ActivityServiceImpl implements ActivityService, InitializingBean{// 省略无关代码@Overridepublic void afterPropertiesSet() throws Exception {initActivity();}// 省略无关代码
}

可以通过将afterPropertiesSet()异步化的方式加速项目的启动。

观察Spring源码可以注意到afterPropertiesSet方法是在AbstractAutowireCapableBeanFactory#

invokeInitMethods中调用的。在这个方法中,不光处理了afterPropertiesSet方法,也处理了init-method。

因此我们可以写一个自己的BeanFactory继承AbstractAutowireCapableBeanFactory,将invokeInitMethods方法进行异步化重写。考虑到AbstractAutowireCapableBeanFactory是个抽象类,有额外的抽象方法需要实现,因此继承该抽象类的子类DefaultListableBeanFactory。具体实现代码如下:

public class AsyncInitListableBeanFactory extends DefaultListableBeanFactory{public AsyncInitBeanFactory(ConfigurableListableBeanFactory beanFactory){super(beanFactory);}@Overrideprotected void invokeInitMethods(String beanName, Object bean, RootBeanDefinition mbd)throws Throwable {if (beanName.equals("activityServiceImpl")) {AsyncTaskExecutor.submitTask(() -> {try {super.invokeInitMethods(beanName, bean, mbd);} catch (Throwable throwable) {throwable.printStackTrace();}});} else {super.invokeInitMethods(beanName, bean, mbd);}}
}

又因为Spring在refreshContext()方法之前的prepareContext()发放中针对initialize方法提供了接口扩展(applyInitializers())。因此我们可以通过实现该接口并将我们的新的BeanFactory通过反射的方式更新到Spring的初始化流程之前。

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {/*** Initialize the given application context.* @param applicationContext the application to configure*/void initialize(C applicationContext);}

改造后的代码如下,新增AsyncAccelerate-

Initializer类实现ApplicationContextInitializer接口:

public class AsyncBeanFactoryInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {@SneakyThrows@Overridepublic void initialize(ConfigurableApplicationContext applicationContext){if (applicationContext instanceof GenericApplicationContext) {AsyncInitListableBeanFactory beanFactory = new AsyncInitListableBeanFactory(applicationContext.getBeanFactory());Field field = GenericApplicationContext.class.getDeclaredField("beanFactory");field.setAccessible(true);field.set(applicationContext, beanFactory);}}
}
public class AsyncBeanInitExecutor{private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();private static final AtomicReference<ThreadPoolExecutor> THREAD_POOL_REF = new AtomicReference<>();private static final List<Future<?>> FUTURES = new ArrayList<>();/*** 创建线程池实例*/private static ThreadPoolExecutor createThreadPoolExecutor(){int poolSize = CPU_COUNT + 1;return new ThreadPoolExecutor(poolSize, poolSize, 50L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy());}/*** 确保线程池已初始化(线程安全)*/private static void ensureThreadPoolExists(){if (THREAD_POOL_REF.get() != null) {return;}ThreadPoolExecutor executor = createThreadPoolExecutor();if (!THREAD_POOL_REF.compareAndSet(null, executor)) {executor.shutdown(); // 另一线程已初始化成功}}/*** 提交异步初始化任务** @param task 初始化任务* @return 提交后的 Future 对象*/public static Future<?> submitInitTask(Runnable task) {ensureThreadPoolExists();Future<?> future = THREAD_POOL_REF.get().submit(task);FUTURES.add(future);return future;}/*** 等待所有初始化任务完成并释放资源*/public static void waitForInitTasks(){try {for (Future<?> future : FUTURES) {future.get();}} catch (Exception ex) {throw new RuntimeException("Async init task failed", ex);} finally {FUTURES.clear();shutdownThreadPool();}}/*** 关闭线程池并重置引用*/private static void shutdownThreadPool(){ThreadPoolExecutor executor = THREAD_POOL_REF.getAndSet(null);if (executor != null) {executor.shutdown();}}
}

实现类后,还需要在META-INF/spring.factories下新增说明org.springframework.context.

ApplicationContextInitializer=com.xxx.AsyncAccelerateInitializer,这样这个类才能真正生效。

这样异步化以后还有一个点需要注意,如果该初始化方法执行耗时很长,那么会存在Spring容器已经启动完成,但是异步初始化任务没执行完的情况,可能会导致空指针等异常。为了避免这种问题的发生,还要借助于Spring容器启动中finishRefresh()方法,监听对应事件,确保异步任务执行完成之后,再启动容器。

public class AsyncInitCompletionListener implements ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware, PriorityOrdered{private ApplicationContext currentContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext)throws BeansException {this.currentContext = applicationContext;}@Overridepublic void onApplicationEvent(ContextRefreshedEvent event){if (event.getApplicationContext() == currentContext) {AsyncBeanInitExecutor.waitForInitTasks();}}@Overridepublic int getOrder(){return Ordered.HIGHEST_PRECEDENCE;}
}

七、总结

启动优化后的项目实际测试结果如下:

图片

通过异步化初始化和分库分表加载优化,项目启动时间从 280 秒缩短至 159 秒,提升约 50%。这对于提升日常开发效率、加快测试与联调流程具有重要意义。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/911940.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/911940.shtml
英文地址,请注明出处:http://en.pswp.cn/news/911940.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Jenkins执行Jenkinsfile报错

遇到部署的步骤时传输文件到其他服务器&#xff0c;文件传上去了&#xff0c;但是命令都没有执行成功。 写法&#xff1a; 报错了&#xff1a;ERROR:Exception when publishing,exception message [Exec exit status not zero.Status [1]] 原因是因为&#xff1a;cd 引用了环…

Modbus TCP转Profibus DP网接APM810/MCE安科瑞多功能电表通讯案例

Modbus TCP转Profibus DP网接APM810/MCE安科瑞多功能电表通讯案例 在工业自动化和电力监控领域&#xff0c;Modbus TCP与Profibus DP是两种常见的通讯协议&#xff0c;它们各自有着广泛的应用场景和优势。而当需要将这两者进行连接&#xff0c;以实现不同设备间的数据传输和信…

MySQL常见问题概述

一、MySQL常见问题概述 MySQL是最常用的关系型数据库&#xff0c;但使用中常会遇到 性能慢、数据丢失、主从不同步、锁冲突 等问题。这些问题可能导致系统响应变慢、用户操作失败&#xff0c;甚至数据损坏。 核心解决思路&#xff1a;先定位问题类型&#xff08;是查询慢&…

zlmediakit windows 编译webrtc

1、环境准备 系统环境&#xff1a;Windows 10 专业版 序号名称版本用途1Microsoft Visual Studio20222openssl3.0.53cmake3.24.04libsrtp2.4.0webrtc播放需要 2、安装libsrtp https://github.com/cisco/libsrtp/releases/tag/v2.4.2 2.1、新建构建目录 在libsrtp-2.4.2根目录…

Redis Pipelining 是性能加速的秘密武器?

在高性能的现代应用中&#xff0c;Redis 因其闪电般的速度而备受青睐。而 Pipelining&#xff08;管道技术&#xff09; 则是 Redis 性能优化的核心功能之一。许多开发者都听说过它能提升性能&#xff0c;但它究竟是如何做到的&#xff1f;是否会带来负面影响&#xff1f;今天我…

系统性能优化-6 TCP 三次握手

系统性能优化-6 TCP 三次握手 TCP 三次握手 客户端优化 客户端发送 SYN 给服务器 此时客户端连接状态&#xff1a;SYN_SENT如果服务器繁忙或中间网络不畅&#xff0c;客户端会重发 SYN&#xff0c;重试的次数由 tcp_syn_retries 参数控制&#xff0c;默认是 6 次&#xff0c;第…

WPF 实现自定义弹窗输入功能

1.前端实现 <Grid><Grid.RowDefinitions><RowDefinition Height"60" /><RowDefinition Height"*" /></Grid.RowDefinitions><BorderGrid.Row"0"BorderBrush"WhiteSmoke"BorderThickness"0.1&qu…

WPF中Converter基础用法

IValueConverter 1.创建一个类集成接口IValueConverter,并实现 2在xaml中引入 举例 性别用int来表示&#xff0c;1为男&#xff0c;2为女 核心代码 创建GenderConverter继承IValueConverter public class GenderConverter : IValueConverter {//model->view转换public…

Postgresql的json充当字典应用

一般我们会将一些系统参数放到参数表中&#xff0c;有些参数的值是json结构&#xff0c;那么如何在查询时引用这些参数&#xff1f;&#xff1f; 比如我在业务表的的xxx_type,或xxx_status记录的是key,又想在查询的时候显示其描述。 先定义字典 如下图如何应用 Postgresql对j…

Dify全面升级:打造极致智能应用开发体验,携手奇墨科技共拓AI新生态

智能应用开发平台Dify以六大核心功能升级与深度性能优化&#xff0c;重新定义AI开发效率与体验。本次更新不仅响应了开发者社区的迫切需求&#xff0c;更通过与云计算领域先锋奇墨科技的战略合作&#xff0c;为企业提供了从开发到部署的全链路智能化解决方案。 .技术领先&#…

关于uniapp开发阻止事件冒泡问题

背景。uniapp开发微信小程序。在使用两个组件拼接嵌套使用后&#xff0c;发现问题&#xff0c;会误操作跳转到更多页面。下图中两个事件若不使用stop修饰符&#xff0c;会相互影响。若点击uni-list-item会串行触发uni-card的handledoctorlist方法。 产生上面问题原因是组件之间…

箭头函数和普通函数的区别?

箭头函数&#xff08;Arrow Functions&#xff09;和普通函数&#xff08;传统函数&#xff09;在 JavaScript 中有显著的区别&#xff0c;主要体现在语法、this 的绑定、构造函数行为、参数处理等方面。以下是详细对比&#xff1a; 1. 语法差异 普通函数&#xff1a; functio…

Linux系统日志与守护进程开发实战指南

Linux系统日志与守护进程开发实战指南 系统日志与守护进程 ├── 系统日志syslog │ ├── 日志路径: /var/log/syslog │ └── 核心API │ ├── openlog │ ├── syslog │ └── closelog └── 守护进程daemon└── 创建步骤├── um…

Vue.js 过滤器详解

Vue.js 过滤器详解 下面我将详细讲解Vue.js中过滤器的语法和使用注意事项&#xff0c;并提供一个完整的演示页面。 过滤器基本概念 在Vue.js中&#xff0c;过滤器&#xff08;Filters&#xff09; 是用于文本格式化的功能&#xff0c;可以在双花括号插值和v-bind表达式中使用…

【iOS】iOS崩溃总结

【iOS】iOS崩溃总结 一、前言 之前写了一篇博文《【Flutter】程序报错导致的灰屏总结》&#xff0c;浏览量、收藏率和点赞量还挺高&#xff0c;还被收录了&#xff0c;就想着总结一下iOS崩溃&#xff0c;这个也是在iOS面试中经常被问到的。 在 iOS 开发过程中&#xff0c;导致…

机器学习:特征向量与数据维数概念

特征向量与数据维数概念 一、特征向量与维数的定义 特征向量与特征类别 在机器学习和数据处理中&#xff0c;每个样本通常由多个特征&#xff08;Feature&#xff09; 描述。例如&#xff0c;一张图片的特征可能包括颜色、形状、纹理等&#xff1b;一个客户的特征可能包括年龄…

开发基于Jeston Orin Nx 开发版 16G的实现

一、基本配置 1.配置参数 密码&#xff1a;yahboom Ubuntu 20.04版本、python3.8、CUDA11.4、cuDNN8.6、TensorRT8.5、Jetpack5.1.1、Opencv4.5.4版本 终端输入命令&#xff1a;sudo jtop 其中Jetpack是英伟达提供的专门供它自己的嵌入式计算机平台使用的人工智能包。 终…

【技术分享】XR技术体系浅析:VR、AR与MR的区别、联系与应用实践

XR技术体系浅析&#xff1a;VR、AR与MR的区别、联系与应用实践 作者&#xff1a;EQ 雪梨蛋花汤 本文是技术分享文档&#xff0c;浅析VR&#xff08;虚拟现实&#xff09;、AR&#xff08;增强现实&#xff09;、MR&#xff08;混合现实&#xff09;的定义、特性、技术演进路线&…

R语言入门课| 05 一文掌握R语言常见数据类型

视频教程 大家可以先做一做R语言基础小测验&#xff0c;看看自己是否需要跟我们5.5h入门R语言的课程。 先上教程视频&#xff0c;B站同步播出&#xff1a; https://www.bilibili.com/video/BV1miNVeWEkw 完整视频回放和答疑服务可见&#xff1a;5.5h入门R语言 本节课程视频…

vRDMA 发布,助力云上 VPC 内高性能通信

资料来源&#xff1a;火山引擎-开发者社区 近日&#xff0c;火山引擎基于部分云服务器实例规格邀测发布 vRDMA 特性&#xff0c;提供云上 VPC 内大规模 RDMA 加速能力&#xff0c;可兼容传统 HPC 应用、AI 应用以及传统 TCP/IP 应用&#xff0c;降低大众化场景的适配门槛&#…