在Web浏览器中显示Spring应用程序启动的进度
重新啟動(dòng)企業(yè)應(yīng)用程序時(shí),客戶(hù)打開(kāi)Web瀏覽器時(shí)會(huì)看到什么?
處于情況4和5.絕對(duì)更好。但是,在本文中,我們將介紹對(duì)情況1和3的更強(qiáng)大的處理。
典型的Spring Boot應(yīng)用程序會(huì)在所有Bean都加載完畢時(shí)(狀態(tài)1),在最后啟動(dòng)Web容器(例如Tomcat)。這是一個(gè)非常合理的默認(rèn)值,因?yàn)樗鼤?huì)阻止客戶(hù)端在完全配置之前無(wú)法訪問(wèn)我們的端點(diǎn)。 但是,這意味著我們無(wú)法區(qū)分啟動(dòng)了幾秒鐘的應(yīng)用程序和關(guān)閉了的應(yīng)用程序。 因此,想法是要有一個(gè)應(yīng)用程序在加載時(shí)顯示一些有意義的啟動(dòng)頁(yè)面,類(lèi)似于顯示“ 服務(wù)不可用 ”的Web代理。 但是,由于此類(lèi)啟動(dòng)頁(yè)面是我們應(yīng)用程序的一部分,因此它可能會(huì)更深入地了解啟動(dòng)進(jìn)度。 我們希望在初始化生命周期中更早地啟動(dòng)Tomcat,但是要提供特殊目的的啟動(dòng)頁(yè)面,直到Spring完全引導(dǎo)為止。 這個(gè)特殊頁(yè)面應(yīng)該攔截所有可能的請(qǐng)求-因此聽(tīng)起來(lái)像一個(gè)servlet過(guò)濾器。
渴望并盡早啟動(dòng)Tomcat。
在Spring啟動(dòng)servlet容器通過(guò)初始化EmbeddedServletContainerFactory創(chuàng)建的實(shí)例EmbeddedServletContainer 。 我們有機(jī)會(huì)使用EmbeddedServletContainerCustomizer攔截此過(guò)程。 容器是在應(yīng)用程序生命周期的早期創(chuàng)建的,但是在整個(gè)上下文完成后才開(kāi)始 。 所以我想我將只在自己的定制器中調(diào)用start()就是這樣。 不幸的是ConfigurableEmbeddedServletContainer沒(méi)有公開(kāi)這樣的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;}};} }您可能會(huì)認(rèn)為BeanPostProcessor是一個(gè)過(guò)大的功能,但是稍后它將變得非常有用。 我們?cè)谶@里所做的是,如果遇到從應(yīng)用程序上下文中被請(qǐng)求的EmbeddedServletContainerFactory ,我們將返回一個(gè)裝飾器,該裝飾器急切地啟動(dòng)Tomcat。 這給我們帶來(lái)了相當(dāng)不穩(wěn)定的設(shè)置,即Tomcat接受到尚未初始化的上下文的連接。 因此,讓我們放置一個(gè)servlet過(guò)濾器來(lái)攔截所有請(qǐng)求,直到上下文完成為止。
啟動(dòng)期間攔截請(qǐng)求
我只是通過(guò)在Spring上下文中添加FilterRegistrationBean來(lái)開(kāi)始的,希望它會(huì)攔截傳入的請(qǐng)求,直到上下文啟動(dòng)為止。 這是徒勞的:我不得不等待很長(zhǎng)時(shí)間,直到注冊(cè)過(guò)濾器并準(zhǔn)備就緒,因此從用戶(hù)的角度來(lái)看,應(yīng)用程序已掛起。 后來(lái)我什至嘗試使用Servlet API( javax.servlet.ServletContext.addFilter() )在Tomcat中直接注冊(cè)過(guò)濾器,但是顯然必須預(yù)先引導(dǎo)整個(gè)DispatcherServlet 。 記住,我想要的只是來(lái)自即將初始化的應(yīng)用程序的快速反饋。 因此,我最終得到了Tomcat的專(zhuān)有API: org.apache.catalina.Valve 。 Valve與Servlet過(guò)濾器類(lèi)似,但它是Tomcat體系結(jié)構(gòu)的一部分。 Tomcat自己捆綁了多個(gè)閥門(mén),以處理各種容器功能,例如SSL,會(huì)話群集和X-Forwarded-For處理。 Logback Access也使用此API,因此我不會(huì)感到內(nèi)。 閥門(mén)看起來(lái)像這樣:
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());}} }閥門(mén)通常委托給鏈中的下一個(gè)閥門(mén),但是這次我們只為每個(gè)單個(gè)請(qǐng)求返回static loading.html頁(yè)面。 注冊(cè)這樣的閥門(mén)非常簡(jiǎn)單,Spring Boot為此提供了一個(gè)API!
if (factory instanceof TomcatEmbeddedServletContainerFactory) {((TomcatEmbeddedServletContainerFactory) factory).addContextValves(new ProgressValve()); }定制閥門(mén)原來(lái)是一個(gè)好主意,它從Tomcat立即開(kāi)始并且非常易于使用。 但是,您可能已經(jīng)注意到,即使在應(yīng)用程序啟動(dòng)后,我們也不會(huì)放棄提供loading.html 。 那很糟。 Spring上下文可以通過(guò)多種方式發(fā)出初始化信號(hào),例如,使用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內(nèi)部,我根本不想接觸Spring上下文,因?yàn)槿绻以阱e(cuò)誤的時(shí)間點(diǎn)從隨機(jī)線程請(qǐng)??求某個(gè)bean,它可能會(huì)引入阻塞甚至死鎖。 完成promise , Valve注銷(xiāo)其自身:
public class ProgressValve extends ValveBase {public ProgressValve() {Listener.initialization().thenRun(this::removeMyself);}private void removeMyself() {getContainer().getPipeline().removeValve(this);}//...}這是令人驚訝的干凈解決方案:當(dāng)不再需要Valve我們無(wú)需從處理管道中刪除它,而不必為每個(gè)請(qǐng)求支付費(fèi)用。 我不會(huì)演示它如何工作以及為什么起作用,讓我們直接轉(zhuǎn)向目標(biāo)解決方案。
監(jiān)控進(jìn)度
監(jiān)視Spring應(yīng)用程序上下文啟動(dòng)的進(jìn)度非常簡(jiǎn)單。 另外,與基于API和規(guī)范驅(qū)動(dòng)的框架(如EJB或JSF)相反,我也驚訝于Spring框架的“可破解性”。 在Spring中,我可以簡(jiǎn)單地實(shí)現(xiàn)BeanPostProcessor ,以通知每個(gè)正在創(chuàng)建和初始化的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時(shí),我將其名稱(chēng)發(fā)布到RxJava的可觀察對(duì)象中。 整個(gè)應(yīng)用程序初始化后,我完成了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現(xiàn)在變得更復(fù)雜了,我們還沒(méi)有完成。 它可以處理多個(gè)不同的請(qǐng)求,例如,我有意在/health和/info Actuator端點(diǎn)上返回503,以便該應(yīng)用程序看起來(lái)像在啟動(dòng)期間處于關(guān)閉狀態(tài)。 除了所有其他請(qǐng)求init.stream表明熟悉loading.html 。 /init.stream是特殊的。 這是服務(wù)器發(fā)送的事件端點(diǎn),它將在每次初始化新bean時(shí)推送消息(很抱歉,上面沒(méi)有代碼):
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);} }這意味著我們可以使用簡(jiǎn)單的HTTP接口(!)來(lái)跟蹤Spring應(yīng)用程序上下文啟動(dòng)的進(jìn)度:
$ 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該端點(diǎn)將實(shí)時(shí)初始化(請(qǐng)參閱: 使用RxJava和SseEmitter的服務(wù)器發(fā)送的事件 )每個(gè)初始化的單個(gè)bean名稱(chēng)。 有了如此出色的工具,我們將構(gòu)建更強(qiáng)大的( 反應(yīng)性的 ,在這里,我說(shuō)過(guò)) loading.html頁(yè)面。
花式進(jìn)度前端
首先,我們需要確定哪些Spring bean代表了系統(tǒng)中的哪些子系統(tǒng) ,高級(jí)組件(甚至可能是有限的上下文 )。 我使用data-bean自定義屬性在HTML內(nèi)部對(duì)此進(jìn)行了編碼:
<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尚未出現(xiàn)在SSE流中。 最初,所有組件都處于"waiting"狀態(tài)。 然后,我訂閱init.stream并更改CSS類(lèi)以反映模塊狀態(tài)更改:
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';} });簡(jiǎn)單吧? 顯然,沒(méi)有jQuery的人就可以使用純JavaScript編寫(xiě)前端。 加載所有bean后, Observable在服務(wù)器端event: complete ,SSE發(fā)出event: complete ,讓我們處理一下:
source.addEventListener('complete', function (e) {window.location.reload(); });因?yàn)榍岸耸窃趹?yīng)用程序上下文啟動(dòng)時(shí)通知的,所以我們可以簡(jiǎn)單地重新加載當(dāng)前頁(yè)面。 到那時(shí),我們的ProgressValve已經(jīng)注銷(xiāo),因此重新加載將打開(kāi)真實(shí)的應(yīng)用程序,而不是loading.html占位符。 我們的工作完成了。 另外,我還計(jì)算了啟動(dòng)的bean數(shù)量,并知道總共有多少bean(我用JavaScript對(duì)其進(jìn)行了硬編碼,請(qǐng)?jiān)?#xff09;,我可以用百分比來(lái)計(jì)算啟動(dòng)進(jìn)度。 圖片值一千個(gè)字,下面的屏幕截圖向您展示了我們所取得的成果:
后續(xù)模塊啟動(dòng)良好,我們不再關(guān)注瀏覽器錯(cuò)誤。 以百分比衡量的進(jìn)度使整個(gè)啟動(dòng)進(jìn)度感覺(jué)非常順利。 最后但并非最不重要的一點(diǎn)是,當(dāng)應(yīng)用程序啟動(dòng)時(shí),我們將自動(dòng)重定向。 希望您喜歡這個(gè)概念證明,整個(gè)工作示例應(yīng)用程序都可以在GitHub上找到。
翻譯自: https://www.javacodegeeks.com/2015/09/displaying-progress-of-spring-application-startup-in-web-browser.html
總結(jié)
以上是生活随笔為你收集整理的在Web浏览器中显示Spring应用程序启动的进度的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: j2ee 和 j2se_在J2SE应用中
- 下一篇: (ftp linux权限)