在Web浏览器中显示Spring应用程序启动的进度

重新启动企业应用程序时,客户打开Web浏览器时会看到什么?

  1. 他们什么也没看到,服务器还没有响应,因此Web浏览器显示ERR_CONNECTION_REFUSED
  2. 应用程序前面的Web代理(如果有)注意到它已关闭并显示“友好”错误消息
  3. 该网站需要永久加载-它接受了套接字连接和HTTP请求,但是等待响应,直到应用程序实际启动
  4. 您的应用程序进行了横向扩展,以便其他节点可以快速处理请求,而不会发出通知(并且会话始终得以复制)
  5. …或应用程序启动速度如此之快,以至于没有人注意到任何中断(嘿,普通的Spring Boot Hello world应用程序从点击java -jar ... [Enter]开始服务请求不到3秒)。 顺便说一句,请检出SPR-8767:启动过程中的并行bean初始化

处于情况4和5.绝对更好。但是,在本文中,我们将介绍对情况1和3的更强大的处理。

典型的Spring Boot应用程序会在所有Bean都加载完毕时(状态1),在最后启动Web容器(例如Tomcat)。这是一个非常合理的默认值,因为它会阻止客户端在完全配置之前无法访问我们的端点。 但是,这意味着我们无法区分启动了几秒钟的应用程序和关闭了的应用程序。 因此,想法是要有一个应用程序在加载时显示一些有意义的启动页面,类似于显示“ 服务不可用 ”的Web代理。 但是,由于此类启动页面是我们应用程序的一部分,因此它可能会更深入地了解启动进度。 我们希望在初始化生命周期中更早地启动Tomcat,但是要提供特殊目的的启动页面,直到Spring完全引导为止。 这个特殊页面应该拦截所有可能的请求-因此听起来像一个servlet过滤器。

渴望并尽早启动Tomcat。

在Spring启动servlet容器通过初始化EmbeddedServletContainerFactory创建的实例EmbeddedServletContainer 。 我们有机会使用EmbeddedServletContainerCustomizer拦截此过程。 容器是在应用程序生命周期的早期创建的,但是在整个上下文完成后才开始 。 所以我想我将只在自己的定制器中调用start()就是这样。 不幸的是ConfigurableEmbeddedServletContainer没有公开这样的API,所以我不得不像这样装饰EmbeddedServletContainerFactory

class ProgressBeanPostProcessor implements BeanPostProcessor {//...@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if (bean instanceof EmbeddedServletContainerFactory) {return wrap((EmbeddedServletContainerFactory) bean);} else {return bean;}}private EmbeddedServletContainerFactory wrap(EmbeddedServletContainerFactory factory) {return new EmbeddedServletContainerFactory() {@Overridepublic EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {final EmbeddedServletContainer container = factory.getEmbeddedServletContainer(initializers);log.debug("Eagerly starting {}", container);container.start();return container;}};}
}

您可能会认为BeanPostProcessor是一个过大的功能,但是稍后它将变得非常有用。 我们在这里所做的是,如果遇到从应用程序上下文中被请求的EmbeddedServletContainerFactory ,我们将返回一个装饰器,该装饰器急切地启动Tomcat。 这给我们带来了相当不稳定的设置,即Tomcat接受到尚未初始化的上下文的连接。 因此,让我们放置一个servlet过滤器来拦截所有请求,直到上下文完成为止。

启动期间拦截请求

我只是通过在Spring上下文中添加FilterRegistrationBean来开始的,希望它会拦截传入的请求,直到上下文启动为止。 这是徒劳的:我不得不等待很长时间,直到注册过滤器并准备就绪,因此从用户的角度来看,应用程序已挂起。 后来我什至尝试使用Servlet API( javax.servlet.ServletContext.addFilter() )在Tomcat中直接注册过滤器,但是显然必须预先引导整个DispatcherServlet 。 记住,我想要的只是来自即将初始化的应用程序的快速反馈。 因此,我最终得到了Tomcat的专有API: org.apache.catalina.ValveValve与Servlet过滤器类似,但它是Tomcat体系结构的一部分。 Tomcat自己捆绑了多个阀门,以处理各种容器功能,例如SSL,会话群集和X-Forwarded-For处理。 Logback Access也使用此API,因此我不会感到内。 阀门看起来像这样:

package com.nurkiewicz.progress;import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.apache.tomcat.util.http.fileupload.IOUtils;import javax.servlet.ServletException;
import java.io.IOException;
import java.io.InputStream;public class ProgressValve extends ValveBase {@Overridepublic void invoke(Request request, Response response) throws IOException, ServletException {try (InputStream loadingHtml = getClass().getResourceAsStream("loading.html")) {IOUtils.copy(loadingHtml, response.getOutputStream());}}
}

阀门通常委托给链中的下一个阀门,但是这次我们只为每个单个请求返回static loading.html页面。 注册这样的阀门非常简单,Spring Boot为此提供了一个API!

if (factory instanceof TomcatEmbeddedServletContainerFactory) {((TomcatEmbeddedServletContainerFactory) factory).addContextValves(new ProgressValve());
}

定制阀门原来是一个好主意,它从Tomcat立即开始并且非常易于使用。 但是,您可能已经注意到,即使在应用程序启动后,我们也不会放弃提供loading.html 。 那很糟。 Spring上下文可以通过多种方式发出初始化信号,例如,使用ApplicationListener<ContextRefreshedEvent>

@Component
class Listener implements ApplicationListener<ContextRefreshedEvent> {private static final CompletableFuture<ContextRefreshedEvent> promise = new CompletableFuture<>();public static CompletableFuture<ContextRefreshedEvent> initialization() {return promise;}public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {return bean;}@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {promise.complete(event);}}

我知道您的想法是“ static ”吗? 但是在Valve内部,我根本不想接触Spring上下文,因为如果我在错误的时间点从随机线程请​​求某个bean,它可能会引入阻塞甚至死锁。 完成promiseValve注销其自身:

public class ProgressValve extends ValveBase {public ProgressValve() {Listener.initialization().thenRun(this::removeMyself);}private void removeMyself() {getContainer().getPipeline().removeValve(this);}//...}

这是令人惊讶的干净解决方案:当不再需要Valve我们无需从处理管道中删除它,而不必为每个请求支付费用。 我不会演示它如何工作以及为什么起作用,让我们直接转向目标解决方案。

监控进度

监视Spring应用程序上下文启动的进度非常简单。 另外,与基于API和规范驱动的框架(如EJB或JSF)相反,我也惊讶于Spring框架的“可破解性”。 在Spring中,我可以简单地实现BeanPostProcessor ,以通知每个正在创建和初始化的bean( 完整的源代码 ):

package com.nurkiewicz.progress;import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import rx.Observable;
import rx.subjects.ReplaySubject;
import rx.subjects.Subject;class ProgressBeanPostProcessor implements BeanPostProcessor, ApplicationListener<ContextRefreshedEvent> {private static final Subject<String, String> beans = ReplaySubject.create();public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {return bean;}@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {beans.onNext(beanName);return bean;}@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {beans.onCompleted();}static Observable<String> observe() {return beans;}
}

每次初始化新bean时,我将其名称发布到RxJava的可观察对象中。 整个应用程序初始化后,我完成了Observable 。 任何人都可以使用此Observable ,例如我们的自定义ProgressValve ( 完整的源代码 ):

public class ProgressValve extends ValveBase {public ProgressValve() {super(true);ProgressBeanPostProcessor.observe().subscribe(beanName -> log.trace("Bean found: {}", beanName),t -> log.error("Failed", t),this::removeMyself);}@Overridepublic void invoke(Request request, Response response) throws IOException, ServletException {switch (request.getRequestURI()) {case "/init.stream":final AsyncContext asyncContext = request.startAsync();streamProgress(asyncContext);break;case "/health":case "/info":response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);break;default:sendHtml(response, "loading.html");}}//...}

ProgressValve现在变得更复杂了,我们还没有完成。 它可以处理多个不同的请求,例如,我有意在/health/info Actuator端点上返回503,以便该应用程序看起来像在启动期间处于关闭状态。 除了所有其他请求init.stream表明熟悉loading.html/init.stream是特殊的。 这是服务器发送的事件端点,它将在每次初始化新bean时推送消息(很抱歉,上面没有代码):

private void streamProgress(AsyncContext asyncContext) throws IOException {final ServletResponse resp = asyncContext.getResponse();resp.setContentType("text/event-stream");resp.setCharacterEncoding("UTF-8");resp.flushBuffer();final Subscription subscription = ProgressBeanPostProcessor.observe().map(beanName -> "data: " + beanName).subscribeOn(Schedulers.io()).subscribe(event -> stream(event, asyncContext.getResponse()),e -> log.error("Error in observe()", e),() -> complete(asyncContext));unsubscribeOnDisconnect(asyncContext, subscription);
}private void complete(AsyncContext asyncContext) {stream("event: complete\ndata:", asyncContext.getResponse());asyncContext.complete();
}private void unsubscribeOnDisconnect(AsyncContext asyncContext, final Subscription subscription) {asyncContext.addListener(new AsyncListener() {@Overridepublic void onComplete(AsyncEvent event) throws IOException {subscription.unsubscribe();}@Overridepublic void onTimeout(AsyncEvent event) throws IOException {subscription.unsubscribe();}@Overridepublic void onError(AsyncEvent event) throws IOException {subscription.unsubscribe();}@Overridepublic void onStartAsync(AsyncEvent event) throws IOException {}});
}private void stream(String event, ServletResponse response) {try {final PrintWriter writer = response.getWriter();writer.println(event);writer.println();writer.flush();} catch (IOException e) {log.warn("Failed to stream", e);}
}

这意味着我们可以使用简单的HTTP接口(!)来跟踪Spring应用程序上下文启动的进度:

$ curl -v localhost:8090/init.stream
> GET /init.stream HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8090
> Accept: */*< HTTP/1.1 200 OK
< Content-Type: text/event-stream;charset=UTF-8
< Transfer-Encoding: chunkeddata: org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration$EmbeddedTomcatdata: org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration$TomcatWebSocketConfigurationdata: websocketContainerCustomizerdata: org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfigurationdata: toStringFriendlyJsonNodeToStringConverterdata: org.hibernate.validator.internal.constraintvalidators.bv.NotNullValidatordata: serverPropertiesdata: org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration...data: beanNameViewResolverdata: basicErrorControllerdata: org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration$JpaWebConfiguration$JpaWebMvcConfiguration

该端点将实时初始化(请参阅: 使用RxJava和SseEmitter的服务器发送的事件 )每个初始化的单个bean名称。 有了如此出色的工具,我们将构建更强大的( 反应性的 ,在这里,我说过) loading.html页面。

花式进度前端

首先,我们需要确定哪些Spring bean代表了系统中的哪些子系统 ,高级组件(甚至可能是有限的上下文 )。 我使用data-bean自定义属性在HTML内部对此进行了编码:

<h2 data-bean="websocketContainerCustomizer" class="waiting">Web socket support
</h2><h2 data-bean="messageConverters" class="waiting">Spring MVC
</h2><h2 data-bean="metricFilter" class="waiting">Metrics
</h2><h2 data-bean="endpointMBeanExporter" class="waiting">Actuator
</h2><h2 data-bean="mongoTemplate" class="waiting">MongoDB
</h2><h2 data-bean="dataSource" class="waiting">Database
</h2><h2 data-bean="entityManagerFactory" class="waiting">Hibernate
</h2>

CSS class="waiting"表示给定的模块尚未初始化,即给定的bean尚未出现在SSE流中。 最初,所有组件都处于"waiting"状态。 然后,我订阅init.stream并更改CSS类以反映模块状态更改:

var source = new EventSource('init.stream');
source.addEventListener('message', function (e) {var h2 = document.querySelector('h2[data-bean="' + e.data + '"]');if(h2) {h2.className = 'done';}
});

简单吧? 显然,没有jQuery的人就可以使用纯JavaScript编写前端。 加载所有bean后, Observable在服务器端event: complete ,SSE发出event: complete ,让我们处理一下:

source.addEventListener('complete', function (e) {window.location.reload();
});

因为前端是在应用程序上下文启动时通知的,所以我们可以简单地重新加载当前页面。 到那时,我们的ProgressValve已经注销,因此重新加载将打开真实的应用程序,而不是loading.html占位符。 我们的工作完成了。 另外,我还计算了启动的bean数量,并知道总共有多少bean(我用JavaScript对其进行了硬编码,请原谅),我可以用百分比来计算启动进度。 图片值一千个字,下面的屏幕截图向您展示了我们所取得的成果:

后续模块启动良好,我们不再关注浏览器错误。 以百分比衡量的进度使整个启动进度感觉非常顺利。 最后但并非最不重要的一点是,当应用程序启动时,我们将自动重定向。 希望您喜欢这个概念证明,整个工作示例应用程序都可以在GitHub上找到。

翻译自: https://www.javacodegeeks.com/2015/09/displaying-progress-of-spring-application-startup-in-web-browser.html

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

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

相关文章

教你玩转CSS padding(填充)

padding(填充) 当元素的 padding(填充)内边距被清除时,所释放的区域将会受到元素背景颜色的填充。 单独使用 padding 属性可以改变上下左右的填充。 可能的值 填充- 单边内边距属性 在CSS中,它可以指定不同的侧面不同的填充: padding-top:25px; padding-bottom:25px…

代码审计之SQL注入:BlueCMSv1.6 sp1

这是一篇纪录关于BlueCMSv1.6 sp1两个SQL注入的审计过程&#xff0c;原文来自代码审计之SQL注入&#xff1a;BlueCMSv1.6 sp1 &#xff0c;主要纪录一下个人在参考博文复现这两个漏洞经过。 href"https://www.ichunqiu.com/course/406" target"_blank">…

莫队算法 BOJ 2038 [2009国家集训队]小Z的袜子(hose)

题目传送门 1 /*2 莫队算法&#xff1a;求出[l, r]上取出两只相同袜子的个数。3 莫队算法是离线处理一类区间不修改查询类问题的算法。如果你知道了[L,R]的答案&#xff0c;可以在O(1)的时间下得到4 [L,R-1]和[L,R1]和[L-1,R]和[L1,R]&#xff0c;4个while…

linux prelink.cache,Linux基础操作

1、Linux上的文件管理类命令都有哪些&#xff0c;其常用的使用方法及其相关示例演示。1、1 cp复制命令单个文件复制&#xff1a;cp [OPTION]... SOURCE DEST如果DEST不存在&#xff0c;则事先创建文件&#xff0c;并复制源文件数据如果DEST是非目录文件&#xff0c;则覆盖目录文…

教你玩转CSS 分组选择器和嵌套选择器

目录 分组选择器 嵌套选择器 分组选择器 在样式表中有很多具有相同样式的元素。 h1 {color:green;}h2 {color:green;}p {color:green;} 为了尽量减少代码,你可以使用分组选择器。 每个选择器用逗号分隔。 在下面的例子中,我们对以上代码使用分组选择器: h1,h2,p {color…

记一次无意间发现某学校图书检索系统的变量覆盖漏洞

这是汇文OPAC很早就存在的一个严重漏洞 补充漏洞信息参考来源https://www.seebug.org/vuldb/ssvid-90722 1. 利用存在该漏洞参数的链接&#xff0c;访问存在漏洞文件&#xff0c;并覆盖 session 值 http://*******/opac/openlink_ebk.php?_SESSION[ADMIN_USER]opac_admin 2.…

多线程笔记5

第六章&#xff1a;Overlapped I/O&#xff0c;在你身后变戏法 1.overlapped I/O 是 Win32 的一项技术&#xff0c;你可以要求操作系统为你传送数据&#xff0c;并且在传送完毕时通知你。这项技术使你的程序在I/O 进行过程中仍然能够继续处理事务。事实上&#xff0c;操作系统内…

腾讯手游助手android版本,腾讯游戏助手下载-腾讯游戏助手 安卓版v3.3.4.22-PC6安卓网...

需要调用以下重要权限- 允许应用程序写入到外部存储器, 允许只读到电话状态访问&#xff0c;包括该装置的电话号码&#xff0c;当前蜂窝网络信息&#xff0c;任何正在进行的呼叫的状态&#xff0c;并且任何一个列表 PhoneAccount的注册在设备上, 允许应用程序从外部存储读取, 需…

java 并发 同步信号_Java并发教程–信号量

java 并发 同步信号这是我们将要进行的Java并发系列的第一部分。 具体来说&#xff0c;我们将深入研究Java 1.5及更高版本中内置的并发工具。 我们假设您对同步和易失性关键字有基本的了解。 第一篇文章将介绍信号量-特别是对信号量进行计数 。 信号量是用于限制对资源访问的经…

教你玩转CSS 尺寸 (Dimension)

目录 所有CSS 尺寸 (Dimension)属性 CSS 尺寸 (Dimension) 属性允许你控制元素的高度和宽度。同样&#xff0c;它允许你增加行间距。 所有CSS 尺寸 (Dimension)属性

目录扫描工具dirsearch用法

目录扫描工具-dirsearch 1.下载dirsearch#git clone https://github.com/maurosoria/dirsearch2. 进入dirsearch目录&#xff0c;进行扫描 每一列的含义分别是&#xff1a;扫描时间&#xff0c;状态码&#xff0c;大小&#xff0c;扫描的目录&#xff0c;重定向的地址 参数列…

Truncated incorrect DOUBLE value

我有点傻…… sql update语法&#xff0c;修改的字段之间要用,连接&#xff0c;而不是and 否则就会报错Truncated incorrect DOUBLE value…… 如&#xff1a; update teacher set IMAGE_URL #{1} and UPDATE_TIMEnow() where TEACHER_MOBILE #{0} 错误 update teacher set …

教你玩转CSS Display(显示) 与 Visibility(可见性)

目录 隐藏元素 - display:none或visibility:hidden CSS Display - 块和内联元素 如何改变一个元素显示 隐藏元素 - display:none或visibility:hidden 隐藏一个元素可以通过把display属性设置为”none”,或把visibility属性设置为”hidden”。但是请注意,这两种方法会产…

html dd自动换行,为什么我的dd里面的内容没有自动换行呢

2-6图文混排*{margin: 0;padding: 0;}.clear{clear: both;}.content{width: 1000px;margin: 0 auto;}.word{font-family: "微软雅黑";font-weight: bolder;font-size: 20px;padding-top: 10px;padding-bottom: 10px;text-align: center;}.content .pic-word dl{width…

Java 9抢先体验:与JShell的动手实践– Java REPL

从今天开始如何开始使用Java 9最酷的功能之一&#xff1f; 上周末&#xff0c;我终于开始尝试使用Java 9的早期访问版本了。第一站是JShell&#xff0c;它也被称为Project Kulla。 首先让我鼓起勇气尝试早期访问Java版本的原因。 那就对了。 Java 9的正式发布日期当前设置为2…

CSRF的绕过与利用

Origin & Referer Bypass Null值绕过 当遇到一个cors可用null值绕过时&#xff0c;用iframe配合data协议&#xff0c;就可以发送一个origin为null的请求。这个绕过方式同样也可以用在CSRF这里。 1<iframe sandbox"allow-scripts allow-top-navigation allow-forms&…

ASP.NET中禁止继承IIS中web.config根目录的配置

异常信息&#xff1a; Configuration Error Description: An error occurred during the processing of a configuration file required to service this request. Please review the specific error details below and modify your configuration file appropriately. Parser…

教你玩转CSS Position(定位)

目录 CSS Position(定位) static 定位 fixed 定位 relative 定位 absolute 定位 sticky 定位 重叠的元素 所有的CSS定位属性

jOOQ配置

本文是我们学院课程的一部分&#xff0c;标题为jOOQ –类型安全的数据库查询 。 在SQL和特定关系数据库很重要的Java应用程序中&#xff0c;jOOQ是一个不错的选择。 当JPA / Hibernate抽象过多&#xff0c;JDBC过多时&#xff0c;这是一种替代方法。 它显示了一种现代的领域特…

可隐藏选项卡html,隐藏HTML中选定选项卡的底部边框

我有一个水平HTML选项卡&#xff0c;其中我希望隐藏所选选项卡的底部边框。隐藏HTML中选定选项卡的底部边框这个问题已经被问here和here但这两种解决方案都不能正常工作&#xff0c;因为我使用一个div结构&#xff0c;没有太大的JavaScript。jQuery(.tab-links a).on(click, fu…