繼續分析其他接入點。
其他需要初始化的接入點分析
我們有時候還需要做一些自定義的初始化操作,但是如何在註冊到註冊中心狀態為 UP 也就是開始處理請求之前做這些操作呢?
為了更加與雲環境相容,Spring Boot 從 2.3.0 版本之後引入了一些雲上部署相關的概念:
- LivenessState(存活狀態):就應用程式而言,存活狀態是指應用程式的狀態是否正常。如果存活狀態不正常,則意味著應用程式本身已損壞,無法恢復。在 k8s 中,如果存活檢測失敗,則 kubelet 將殺死 Container,並且根據其重新啟動策略進行重啟:
- 在 spring boot 中對應的介面是
/actuator/health/liveness
- 對應的列舉類是
org.springframework.boot.availability.LivenessState
,包括下面兩個狀態:- CORRECT:存活狀態正常
- BROKEN:存活狀態不正常
- 在 spring boot 中對應的介面是
- Readiness(就緒狀態):指的是應用程式是否已準備好接受並處理客戶端請求。出於任何原因,如果應用程式尚未準備好處理服務請求,則應將其宣告為繁忙,直到能夠正常響應請求為止。如果 Readiness 狀態尚未就緒,則不應將流量路由到該例項。在 k8s 中,如果就緒檢測失敗,則 Endpoints 控制器將從 Endpoints 中刪除這個 Pod 的 IP 地址,如果你沒有使用 k8s 的服務發現的話,就不用太關心這個:
- 在 spring boot 中對應的介面是
/actuator/health/readiness
- 對應的列舉類是
org.springframework.boot.availability.ReadinessState
,包括下面兩個狀態:- ACCEPTING_TRAFFIC:準備好接受請求
- REFUSING_TRAFFIC:目前不能接受請求了
- 在 spring boot 中對應的介面是
預設情況下,Spring Boot 在初始化過程中會修改這些狀態,對應原始碼(我們只關心 listeners 相關,這些標誌著 Spring Boot 生命週期變化):
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
//告訴所有 Listener 啟動中
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
//告訴所有 Listener 啟動完成
listeners.started(context);
//呼叫各種 SpringRunners + CommandRunners
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
//通知所有 Listener 執行中
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
其中,listeners.started
和 listeners.running
裡面做的事情是:
@Override
public void started(ConfigurableApplicationContext context) {
//釋出 ApplicationStartedEvent 事件
context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context));
//設定 LivenessState 為 CORRECT
AvailabilityChangeEvent.publish(context, LivenessState.CORRECT);
}
@Override
public void running(ConfigurableApplicationContext context) {
//釋出 ApplicationReadyEvent 事件
context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context));
//設定 ReadinessState 為 ACCEPTING_TRAFFIC
AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC);
}
由於 ApplicationContext 釋出事件與訂閱該事件的處理是同步進行的,所以如果我們想在 LivenessState 為 CORRECT 之前做點操作,可以監聽 ApplicationStartedEvent 事件。同理,想在 ReadinessState 為 ACCEPTING_TRAFFIC 之前做點操作就監聽 ApplicationStartedEvent 事件。如何將 LivenessState 還有 ReadinessState 與註冊例項到註冊中心的狀態聯絡起來呢?
我們用的註冊中心是 Eureka,註冊中心的例項是有狀態的。我們的 Eureka Client 的配置是:
eureka:
client:
# eureka client 重新整理本地快取時間
# 預設30s
# 對於普通屬性,用駝峰或者橫槓名稱配置都可以,這裡用的駝峰名稱,下面配置用的橫槓名稱
registryFetchIntervalSeconds: 5
healthcheck:
# 啟用健康檢查
enabled: true
# 定時檢查例項資訊以及更新本地例項狀態的任務的間隔
instance-info-replication-interval-seconds: 10
# 初始定時檢查例項資訊以及更新本地例項狀態的任務延遲
initial-instance-info-replication-interval-seconds: 5
我們啟用了 Eureka 的健康檢查,其實就是通過呼叫本地的 /actuator/health 介面的相同服務進行健康檢查。這個健康檢查,會在定時檢查例項資訊以及更新本地例項狀態的任務中呼叫。這個任務的初始延遲我們設定為了 10s,之後檢查間隔設定為了 5s。健康檢查包括存活狀態檢查還有就緒狀態檢查,存活狀態為 CORRECT 的時候 Status 才為 UP,就緒狀態為 ACCEPTING_TRAFFIC 的時候 status 才為 UP。對應的 HealthIndicator 是:
public class ReadinessStateHealthIndicator extends AvailabilityStateHealthIndicator {
public ReadinessStateHealthIndicator(ApplicationAvailability availability) {
super(availability, ReadinessState.class, (statusMappings) -> {
//存活狀態為 CORRECT 的時候 Status 才為 UP
statusMappings.add(ReadinessState.ACCEPTING_TRAFFIC, Status.UP);
statusMappings.add(ReadinessState.REFUSING_TRAFFIC, Status.OUT_OF_SERVICE);
});
}
}
public class ReadinessStateHealthIndicator extends AvailabilityStateHealthIndicator {
public ReadinessStateHealthIndicator(ApplicationAvailability availability) {
super(availability, ReadinessState.class, (statusMappings) -> {
//就緒狀態為 ACCEPTING_TRAFFIC 的時候 status 才為 UP
statusMappings.add(ReadinessState.ACCEPTING_TRAFFIC, Status.UP);
statusMappings.add(ReadinessState.REFUSING_TRAFFIC, Status.OUT_OF_SERVICE);
});
}
}
定時檢查例項資訊以及例項狀態並同步到 Eureka Server 的流程如下:
我們可以使用這個機制,讓初始註冊到 Eureka 的狀態不為 UP,等待存活狀態為 CORRECT 的時候並且就緒狀態為 ACCEPTING_TRAFFIC 的時候,才會通過上面的定時檢查任務將例項狀態設定為 UP 同步到 Eureka Server。
可以加上配置,指定初始註冊狀態:
eureka:
instance:
# 初始例項狀態
initial-status: starting
這樣我們可以監聽 ApplicationStartedEvent 事件實現微服務初始化操作,操作完成後才開始服務。同時還要考慮只執行一次的問題,因為你的 ApplicationContext 不止一個,例如 Spring Cloud 啟用 BootStrap Context 之後,就多了一個 BootStrap Context,我們要保證只執行一次的話,可以像下面這麼寫程式碼,繼承下面這個抽象類集合:
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.springframework.cloud.bootstrap.BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME;
public abstract class AbstractMicronServiceInitializer implements ApplicationListener<ApplicationStartedEvent> {
private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false);
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
if (isBootstrapContext(event)) {
return;
}
//由於spring-cloud的org.springframework.cloud.context.restart.RestartListener導致同一個context觸發多次
//我個人感覺 org.springframework.cloud.context.restart.RestartListener 這個在spring-boot2.0.0之後的spring-cloud版本是沒有必要存在的
//但是官方並沒有正面回應,以防之後官方還拿這個做點事情,這裡我們做個適配,參考我問的這個issue:https://github.com/spring-cloud/spring-cloud-commons/issues/693
synchronized (INITIALIZED) {
if (INITIALIZED.get()) {
return;
}
//每個spring-cloud應用只能初始化一次
init();
INITIALIZED.set(true);
}
}
protected abstract void init();
static boolean isBootstrapContext(ApplicationStartedEvent applicationEvent) {
return applicationEvent.getApplicationContext().getEnvironment().getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME);
}
}
微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer: