如何在生產環境中通過Restful API的方式請求重啟Spring Boot應用?

羅摩爾發表於2019-04-04

通過HTTP重啟Spring Boot應用程式

需求背景

在一個很奇葩的需求下,要求在客戶端動態修改Spring Boot配置檔案中的屬性,例如埠號、應用名稱、資料庫連線資訊等,然後通過一個Http請求重啟Spring Boot程式。這個需求類似於作業系統更新配置後需要進行重啟系統才能生效的應用場景。

動態配置系統並更新生效是應用的一種通用性需求,實現的方式也有很多種。例如監聽配置檔案變化、使用配置中心等等。網路上也有很多類似的教程存在,但大多數都是在開發階段,藉助Spring Boot DevTools外掛實現應用程式的重啟,或者是使用spring-boot-starter-actuator和spring-cloud-starter-config來提供端點(Endpoint)的重新整理。

第一種方式無法在生產環境中使用(不考慮),第二種方式需要引入Spring Cloud相關內容,這無疑是殺雞用了宰牛刀。

接下來,我將嘗試採用另外一種方式實現HTTP請求重啟Spring Boot應用程式這個怪異的需求。

嘗試思路

重啟Spring Boot應用程式的關鍵步驟是對主類中**SpringApplication.run(Application.class,args);方法返回值的處理。SpringApplication#run()方法將會返回一個ConfigurableApplicationContext型別物件,通過檢視官方文件可以看到,ConfigurableApplicationContext介面類中定義了一個close()**方法,可以用來關閉當前應用的上下文:

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()**方法,下面是實現類中的方法摘要:

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工廠。

官方文件看到這裡,就產生了解決一個結局重啟應用應用程式的大膽猜想。在應用程式的main()方法中,我們可以使用一個臨時變數來存放SpringApplication.run()返回的ConfigurableApplicationContext物件,當我們完成對Spring Boot應用程式中屬性的設定後,呼叫ConfigurableApplicationContext的**#close()**方法,最後再呼叫SpringApplication.run()方法重新給ConfigurableApplicationContext物件進行賦值已達到重啟的效果。

現在,我們再來看一下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中定義了三種種型別:NONESERVLETREACTIVE。通常情況下,將會建立servlet型別的ApplicationContext物件。

接下來,我將以一個簡單的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 ,檢視一下返回的資訊:

如何在生產環境中通過Restful API的方式請求重啟Spring Boot應用?

成功返回SPRING-BOOT-APPLICATION提示資訊。

然後,訪問:http://localhost:8080/refresh ,設定應用應用程式spring.application.name的值為SPRING-DYNAMIC-SERVER,觀察控制檯輸出的日誌資訊:

如何在生產環境中通過Restful API的方式請求重啟Spring Boot應用?

可以看到,Spring Boot應用程式已經重新啟動成功,最後,在此訪問:http://localhost:8080/info ,驗證之前的修改是否生效:

如何在生產環境中通過Restful API的方式請求重啟Spring Boot應用?

請求成功返回了SPRING-DYNAMIC-SERVER資訊,最後在看一眼application.properties檔案中的配置資訊是否真的被修改了:

如何在生產環境中通過Restful API的方式請求重啟Spring Boot應用?

配置檔案的屬性也被成功的修改,證明之前的猜想驗證成功了。

本次內容所描述的方法不適用於以JAR檔案啟動的Spring Boot應用程式,以WAR包的方式啟動應用程式親測可用。┏ (^ω^)=☞目前該藥方副作用未知,如有大牛路過,還望留步指點迷津,不勝感激。

結束語

本次內容記錄了自己驗證HTTP請求重啟Spring Boot應用程式試驗的一次經歷,文章中所涉及到的內容僅代表個人的一些觀點和不成熟的想法,並未將此方法應用到實際的專案中去,如因引用本次內容中的方法應用到實際生產開發工作中所帶來的風險,需引用者自行承擔因風險帶來的後遺症(๑→ܫ←)——此藥方還有待商榷(O_o)(o_O)。

原文:如何在生產環境中重啟Spring Boot應用?,你可以訪問我的部落格檢視更多相關內容:羅摩爾·Ramostear

相關文章