在一個很奇葩的需求下,要求在客戶端動態修改Spring Boot配置文件中的屬性,例如端口號、應用名稱、數據庫鏈接信息等,而後經過一個Http請求重啓Spring Boot程序。這個需求相似於操做系統更新配置後須要進行重啓系統才能生效的應用場景。html
動態配置系統並更新生效是應用的一種通用性需求,實現的方式也有不少種。例如監聽配置文件變化、使用配置中心等等。網絡上也有不少相似的教程存在,但大多數都是在開發階段,藉助Spring Boot DevTools插件實現應用程序的重啓,或者是使用spring-boot-starter-actuator和spring-cloud-starter-config來提供端點(Endpoint)的刷新。java
第一種方式沒法在生產環境中使用(不考慮),第二種方式須要引入Spring Cloud相關內容,這無疑是殺雞用了宰牛刀。
接下來,我將嘗試採用另一種方式實現HTTP請求重啓Spring Boot應用程序這個怪異的需求。react
重啓Spring Boot應用程序的關鍵步驟是對主類中SpringApplication.run(Application.class,args);方法返回值的處理。SpringApplication#run()方法將會返回一個ConfigurableApplicationContext類型對象,經過查看官方文檔能夠看到,ConfigurableApplicationContext接口類中定義了一個close()方法,能夠用來關閉當前應用的上下文:web
package org.springframework.context; import java.io.Closeable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.io.ProtocolResolver; import org.springframework.lang.Nullable; public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable { void close(); }
繼續看官方源碼,AbstractApplicationContext類中實現close()方法,下面是實現類中的方法摘要:spring
public void close() { Object var1 = this.startupShutdownMonitor; synchronized(this.startupShutdownMonitor) { this.doClose(); if (this.shutdownHook != null) { try { Runtime.getRuntime().removeShutdownHook(this.shutdownHook); } catch (IllegalStateException var4) { ; } } } }
#close()方法將會調用#doClose()方法,咱們再來看看#doClose()方法作了哪些操做,下面是doClose()方法的摘要:數據庫
protected void doClose() { if (this.active.get() && this.closed.compareAndSet(false, true)) { ... LiveBeansView.unregisterApplicationContext(this); ... this.destroyBeans(); this.closeBeanFactory(); this.onClose(); if (this.earlyApplicationListeners != null) { this.applicationListeners.clear(); this.applicationListeners.addAll(this.earlyApplicationListeners); } this.active.set(false); } }
在#doClose()方法中,首先將應用上下文從註冊表中清除掉,而後是銷燬Bean工廠中的Beans,緊接着關閉Bean工廠。apache
官方文檔看到這裏,就產生了解決一個結局重啓應用應用程序的大膽猜測。在應用程序的main()方法中,咱們能夠使用一個臨時變量來存放SpringApplication.run()返回的ConfigurableApplicationContext對象,當咱們完成對Spring Boot應用程序中屬性的設置後,調用ConfigurableApplicationContext的#close()方法,最後再調用SpringApplication.run()方法從新給ConfigurableApplicationContext對象進行賦值已達到重啓的效果。tomcat
如今,咱們再來看一下SpringApplication.run()方法中是如何從新建立ConfigurableApplicationContext對象的。在SpringApplication類中,run()方法會調用createApplicationContext()方法來建立一個ApplicationContext對象:網絡
protected ConfigurableApplicationContext createApplicationContext() { Class<?> contextClass = this.applicationContextClass; if (contextClass == null) { try { switch(this.webApplicationType) { case SERVLET: contextClass = Class.forName("org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext"); break; case REACTIVE: contextClass = Class.forName("org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext"); break; default: contextClass = Class.forName("org.springframework.context.annotation.AnnotationConfigApplicationContext"); } } catch (ClassNotFoundException var3) { throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3); } } return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass); }
createApplicationContext()方法會根據WebApplicationType類型來建立ApplicationContext對象。在WebApplicationType中定義了三種種類型:NONE、SERVLET和REACTIVE。一般狀況下,將會建立servlet類型的ApplicationContext對象。app
接下來,我將以一個簡單的Spring Boot工程來驗證上述的猜測是否可以達到重啓Spring Boot應用程序的需求。
首先,在application.properties文件中加入以下的配置信息,爲動態修改配置信息提供數據:
spring.application.name= SPRING-BOOT-APPLICATION
接下來,在Spring Boot主類中定義兩個私有變量,用於存放main()方法的參數和SpringApplication.run()方法返回的值。下面的代碼給出了主類的示例:
public class ExampleRestartApplication { @Value ( "${spring.application.name}" ) String appName; private static Logger logger = LoggerFactory.getLogger ( ExampleRestartApplication.class ); private static String[] args; private static ConfigurableApplicationContext context; public static void main(String[] args) { ExampleRestartApplication.args = args; ExampleRestartApplication.context = SpringApplication.run(ExampleRestartApplication.class, args); } }
最後,直接在主類中定義用於刷新並重啓Spring Boot應用程序的端點(Endpoint),並使用@RestController註解對主類進行註釋。
@GetMapping("/refresh") public String restart(){ logger.info ( "spring.application.name:"+appName); try { PropUtil.init ().write ( "spring.application.name","SPRING-DYNAMIC-SERVER" ); } catch (IOException e) { e.printStackTrace ( ); } ExecutorService threadPool = new ThreadPoolExecutor (1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue<> ( 1 ),new ThreadPoolExecutor.DiscardOldestPolicy ()); threadPool.execute (()->{ context.close (); context = SpringApplication.run ( ExampleRestartApplication.class,args ); } ); threadPool.shutdown (); return "spring.application.name:"+appName; }
說明:爲了可以從新啓動Spring Boot應用程序,須要將close()和run()方法放在一個獨立的線程中執行。
爲了驗證Spring Boot應用程序在被修改重啓有相關的屬性有沒有生效,再添加一個獲取屬性信息的端點,返回配置屬性的信息。
@GetMapping("/info") public String info(){ logger.info ( "spring.application.name:"+appName); return appName; }
下面給出了主類的所有代碼:
package com.ramostear.application; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.util.concurrent.*; /** * @author ramostear */ @SpringBootApplication @RestController public class ExampleRestartApplication { @Value ( "${spring.application.name}" ) String appName; private static Logger logger = LoggerFactory.getLogger ( ExampleRestartApplication.class ); private static String[] args; private static ConfigurableApplicationContext context; public static void main(String[] args) { ExampleRestartApplication.args = args; ExampleRestartApplication.context = SpringApplication.run(ExampleRestartApplication.class, args); } @GetMapping("/refresh") public String restart(){ logger.info ( "spring.application.name:"+appName); try { PropUtil.init ().write ( "spring.application.name","SPRING-DYNAMIC-SERVER" ); } catch (IOException e) { e.printStackTrace ( ); } ExecutorService threadPool = new ThreadPoolExecutor (1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue<> ( 1 ),new ThreadPoolExecutor.DiscardOldestPolicy ()); threadPool.execute (()->{ context.close (); context = SpringApplication.run ( ExampleRestartApplication.class,args ); } ); threadPool.shutdown (); return "spring.application.name:"+appName; } @GetMapping("/info") public String info(){ logger.info ( "spring.application.name:"+appName); return appName; } }
接下來,運行Spring Boot程序,下面是應用程序啓動成功後控制檯輸出的日誌信息:
[2019-03-12T19:05:53.053z][org.springframework.scheduling.concurrent.ExecutorConfigurationSupport][main][171][INFO ] Initializing ExecutorService 'applicationTaskExecutor' [2019-03-12T19:05:53.053z][org.apache.juli.logging.DirectJDKLog][main][173][INFO ] Starting ProtocolHandler ["http-nio-8080"] [2019-03-12T19:05:53.053z][org.springframework.boot.web.embedded.tomcat.TomcatWebServer][main][204][INFO ] Tomcat started on port(s): 8080 (http) with context path '' [2019-03-12T19:05:53.053z][org.springframework.boot.StartupInfoLogger][main][59][INFO ] Started ExampleRestartApplication in 1.587 seconds (JVM running for 2.058)
在測試修改系統配置並重啓以前,使用Postman測試工具訪問:http://localhost:8080/info ,查看一下返回的信息:
成功返回SPRING-BOOT-APPLICATION提示信息。
而後,訪問:http://localhost:8080/refresh ,設置應用應用程序spring.application.name的值爲SPRING-DYNAMIC-SERVER,觀察控制檯輸出的日誌信息:
能夠看到,Spring Boot應用程序已經從新啓動成功,最後,在此訪問:http://localhost:8080/info ,驗證以前的修改是否生效:
請求成功返回了SPRING-DYNAMIC-SERVER信息,最後在看一眼application.properties文件中的配置信息是否真的被修改了:
配置文件的屬性也被成功的修改,證實以前的猜測驗證成功了。
本次內容所描述的方法不適用於以JAR文件啓動的Spring Boot應用程序,以WAR包的方式啓動應用程序親測可用。┏ (^ω^)=☞目前該藥方反作用未知,若有大牛路過,還望留步指點迷津,不勝感激。
本次內容記錄了本身驗證HTTP請求重啓Spring Boot應用程序試驗的一次經歷,文章中所涉及到的內容僅表明我的的一些觀點和不成熟的想法,並未將此方法應用到實際的項目中去,如因引用本次內容中的方法應用到實際生產開發工做中所帶來的風險,需引用者自行承擔因風險帶來的後遺症(๑→ܫ←)——此藥方還有待商榷(O_o)(o_O)。
原文做者:譚朝紅
原文標題:如何在生產環境中重啓Spring Boot應用?