【SpringBoot】优化慢启动应用的用户体验

ops/2024/11/13 9:20:24/

通过深入分析SpringBoot中WebServer的启动流程,插入自定义的Loading页面展示逻辑,优化软件使用时的用户体验。

背景

Java本身的特点,再加上开发人员能力差,软件开发工程化程度低等等问题,经过一段时间的迭代之后,经常会出现的一个问题就是应用启动越来越慢。

过往我也是收集了不少相关资料,例如 云音乐服务端应用启动时间下降40%实践分享,7min到40s: SpringBoot 启动优化实践!等等。

本文跳出与上述文章类似的技术角度,借鉴Jenkins的启动流程 —— Jenkins也是Java开发的,而且其启动速度虽然经过不少优化,但依然有着非常明显的延迟,但Jenkins通过加入Loading页面的方式,极大提升了用户在等待系统就绪期间的用户体验 —— 启动之后马上呈现给用户一个Loading页面,待后台服务能够正常提供服务之后冲向到常规Index页面。

实现

Jenkins默认适用的是winstone 容器,所以我在一开始所设想的"拿来主义"被掐死在了摇篮里,最后经过一番摸索,基于SpringBoot + Undertow实现了类似的效果。

思路:

  1. SpringBoot项目中,单单一个WebServer的启动是很快的,只是SpringBoot的启动流程里,其会在WebServer启动之前初始化所有的Bean,而这是整个启动流程里最耗时的。
  2. 所以,我们将在SpringBoot容器中的Bean实例化之前启动我们自定义的"minimal Undertow Server"来向用户提供对于 loading页面的响应。
  3. 待Spring容器准备进行WebServer的启动时,停止我们的"minimal Undertow Server",以避免端口占用。
  4. 前端loading页面将定期轮询后端服务启动情况,待其正常响应时跳转到相应的主体服务页面。

直接上代码:

java">// ====================== 1. 应用启动入口类// 应用启动入口public static void main(String[] args) {final SpringApplication springApplication = new SpringApplication(SpringBootTestApplication.class);// 读取用户配置, 决定启动方式final Boolean humanableStart = Convert.toBool(CommonUtil.getProperty("START_HUMANABLE", "false"));if (humanableStart) {springApplication.setApplicationContextClass(AnnotationConfigServletWebServerApplicationContextEx.class); // 自定义}// 启动Spring容器springApplication.run(args);}// ======================== 2. SpringBoot不同版本实现方式不同
// 这里以 SpringBoot 2.3.3为例, 更高版本需要实现 ApplicationContextFactory 接口
class AnnotationConfigServletWebServerApplicationContextWithHumanableStart extends AnnotationConfigServletWebServerApplicationContext {Undertow minimalUndertowserver;@Overrideprotected void prepareRefresh() {// 尽量提前"minimal Undertow Server"的创建, 优化用户体验.String property = this.getEnvironment().getProperty("server.port");minimalUndertowserver = minimalUndertowserver(Convert.toInt(property));super.prepareRefresh();}@Overrideprotected void onRefresh() {// ServletWebServerApplicationContext正是通过覆写本方法来实现 WebServer 创建的// 同时会向容器中注入webServerStartStop Bean,借助Spring的生命周期回调接口SmartLifecycle来负责将webServer的开启和关闭;super.onRefresh();}@Overrideprotected void finishRefresh() {// 关键流程:  AbstractApplicationContext.refresh()// super.finishRefresh()中将触发 WebServerStartStopLifecycle.start() 以启动webserver, 所以我们得在它之前将我们的轻量级webserver关闭掉.minimalUndertowserver.stop();super.finishRefresh();}static Undertow minimalUndertowserver(int port) {// 这个loading.html是从jenkins里扒过来了, 也算是实现了部分"拿来主义"final String loadingHtml = ResourceUtil.readStr("static/loading.html", CharsetUtil.CHARSET_UTF_8);// Start the minimal Undertow serverUndertow undertow = Undertow.builder().addHttpListener(port, "0.0.0.0").setHandler(new HttpHandler() {@Overridepublic void handleRequest(HttpServerExchange exchange) throws Exception {exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html");exchange.getResponseSender().send(loadingHtml);}}).build();undertow.start();return undertow;}
}

原理分析

Spring容器之所以启动慢,主要原因肯定就是各类Bean的实例化耗时叠加

java">// AbstractApplicationContext.java
// Spring核心启动流程
@Override
public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {// Prepare this context for refreshing.prepareRefresh();  // 我们在这里启动 minimal Undertow Server// Tell the subclass to refresh the internal bean factory.ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// Prepare the bean factory for use in this context.prepareBeanFactory(beanFactory);try {// Allows post-processing of the bean factory in context subclasses.postProcessBeanFactory(beanFactory);// Invoke factory processors registered as beans in the context.invokeBeanFactoryPostProcessors(beanFactory);// Register bean processors that intercept bean creation.registerBeanPostProcessors(beanFactory);// Initialize message source for this context.initMessageSource();// Initialize event multicaster for this context.initApplicationEventMulticaster();// Initialize other special beans in specific context subclasses.onRefresh();// Check for listener beans and register them.registerListeners();// Instantiate all remaining (non-lazy-init) singletons.finishBeanFactoryInitialization(beanFactory);// Last step: publish corresponding event.finishRefresh();  // 在这里关闭 minimal Undertow Server.} catch (BeansException ex) {if (logger.isWarnEnabled()) {logger.warn("Exception encountered during context initialization - " +"cancelling refresh attempt: " + ex);}// Destroy already created singletons to avoid dangling resources.destroyBeans();// Reset 'active' flag.cancelRefresh(ex);// Propagate exception to caller.throw ex;}finally {// Reset common introspection caches in Spring's core, since we// might not ever need metadata for singleton beans anymore...resetCommonCaches();}}
}

相关

  1. Jenkins源码 - loading页面
  2. Jenkins源码 - loading后端代码
  3. GitHub - jenkinsci-winstone

http://www.ppmy.cn/ops/98468.html

相关文章

Vue的生命周期了解

目录 1.何为Vue的生命周期 2.Vue2生命周期阶段 (1)beforeCreate(创建前) (2)created(创建后) (3)beforeMount(挂载前) (4)mounted(挂载后) (5)beforeU…

推荐一个完全自由的目录设计网站

引言 如果我们能通过网站出一本书,这将是一件很酷的事。 事实上,我们通过网站发布知识,最常见的是写博客。 这二者有什么区别呢? 书本的知识内容有很强的逻辑性、系统性。而博客是随心所欲的,一时灵感来了就写一篇…

「UCD」用户体验设计专家

VOC 设计系统的作用及建立方式?用户画像的作用和创建方法?如何评估产品的用户体验?一、设计系统的作用及建立方式 设计系统(Design System)是一套标准化的视觉和交互指南,旨在确保产品设计的一致性和高效性。它不仅可以提高设计与开发团队之间的协作效率,还可以为用户带…

【前端基础篇】CSS基础速通万字介绍(下篇)

文章目录 前言背景属性背景颜色背景图片背景平铺背景位置背景尺寸 圆角矩形生成圆形生成圆角矩形 Chrome调试工具打开方式标签页含义elements标签页使用 元素显示模式块级元素行内元素/内联元素行内元素和块级元素的区别 盒模型边框内边距外边距 块级元素水平居中去除浏览器默认…

Baumer工业相机堡盟工业相机如何通过BGAPISDK初始化时过滤其它非Baumer相机(C++)

Baumer工业相机堡盟工业相机如何通过BGAPISDK初始化时过滤其它非Baumer相机(C) Baumer工业相机Baumer工业相机通过SDK初始化时过滤其它非Baumer相机的技术背景通过SDK过滤其它非Baumer相机的代码分析1、引用合适的类文件2、初始化时过滤其它非Baumer相机…

PHPShort轻量级网址缩短程序源码开心版,内含汉化包

需要网址缩短并且想获得更多有关链接点击率和流量的数据分析,那么 PHPShort 可能是一个非常好的选择。PHPShort 是一款高级的 URL 缩短器平台,可以帮助你轻松地缩短链接,并根据受众群体的位置或平台来定位受众。 该程序基于 Laravel 框架编写…

Apache CloudStack Official Document 翻译节选(七)

关于 Apache CloudStack 的 最佳实践 (一) Best Practices 部署Apache CloudStack是极具挑战性的,在整个部署过程中需要你做出形形色色的技术性选择。Apache CloudStack的配置条目是相当灵活的,这是因为在组合和配置具体条目时有…

高效率伪原创检测,6款工具为你轻松搞定

在内容创作领域,原创性是衡量作品价值的重要标准之一。然而,创作高质量的原创内容不仅需要灵感和创意,还需要大量的时间和精力。为了提高效率,许多创作者和编辑开始寻求伪原创检测工具的帮助,以确保他们的作品在保持独…