在Web浏览器中显示Spring应用程序启动的进度
重新啟動企業應用程序時,客戶打開Web瀏覽器時會看到什么?
處于情況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.Valve 。 Valve與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,它可能會引入阻塞甚至死鎖。 完成promise , Valve注銷其自身:
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
總結
以上是生活随笔為你收集整理的在Web浏览器中显示Spring应用程序启动的进度的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: j2ee 和 j2se_在J2SE应用中
- 下一篇: (ftp linux权限)