如何延迟或修改Spring Boot初始Readiness状态的AvailabilityChangeEvent?
如何延迟或修改Spring Boot中AvailabilityChangeEvent<Readiness>状态的初始值?
我有一个作为Worker的Spring Boot应用,负责从RabbitMQ队列轮询消息。轮询需满足特定条件才启动,条件不满足时停止。我计划使用@EventListener来控制轮询的启停,这种方式能正常实现Worker的启停,但Spring Boot启动时会自动广播状态为Readiness.ACCEPTING_TRAFFIC的AvailabilityChangeEvent,这一非预期事件会覆盖我通过ApplicationRunner发送的Readiness.REFUSING_TRAFFIC事件。
我已经实现了ApplicationRunner在启动时检查条件并发送对应状态的AvailabilityChangeEvent,同时实现了带@Scheduled注解的pollCondition()方法模拟条件状态变化:
package com.example.demo; import java.util.Random; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.availability.AvailabilityChangeEvent; import org.springframework.boot.availability.ReadinessState; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; @SpringBootApplication @EnableScheduling public class DemoApplication { private static final Logger LOG = LoggerFactory.getLogger(DemoApplication.class); private boolean condition = false; private final ApplicationEventPublisher applicationEventPublisher; public DemoApplication(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Bean ApplicationRunner runner(ApplicationEventPublisher applicationEventPublisher) { return ignored -> checkConditionAndBroadcast(); } @Scheduled(fixedRate = 5, initialDelay = 5, timeUnit = TimeUnit.SECONDS) void pollCondition() { // 模拟随时间变化的条件 boolean nextCondition = new Random().nextBoolean(); LOG.info("轮询条件,新状态: {}", nextCondition); if (condition != nextCondition) { condition = nextCondition; checkConditionAndBroadcast(); } } private void checkConditionAndBroadcast() { if (condition) { applicationEventPublisher.publishEvent(new AvailabilityChangeEvent<>(this, ReadinessState.ACCEPTING_TRAFFIC)); } else { applicationEventPublisher.publishEvent(new AvailabilityChangeEvent<>(this, ReadinessState.REFUSING_TRAFFIC)); } } /* * 控制RabbitMQ轮询启停的事件监听器 */ @EventListener public void onApplicationEvent(AvailabilityChangeEvent<ReadinessState> event) { LOG.info("就绪状态事件,当前状态: {}", event.getState()); /* if (event.getState() == ReadinessState.ACCEPTING_TRAFFIC) { 启动轮询 } else { 停止轮询 } */ } }
运行应用时,REFUSING_TRAFFIC事件能正常发送和接收,但随即会被启动时的ACCEPTING_TRAFFIC事件覆盖。日志如下:
2024-03-15T11:26:15.320+01:00 INFO 9169 --- [demo] [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.768 seconds (process running for 2.286) 2024-03-15T11:26:15.323+01:00 INFO 9169 --- [demo] [ main] com.example.demo.DemoApplication : 就绪状态事件,当前状态: REFUSING_TRAFFIC 2024-03-15T11:26:15.325+01:00 INFO 9169 --- [demo] [ main] com.example.demo.DemoApplication : 就绪状态事件,当前状态: ACCEPTING_TRAFFIC <--- 此处不符合预期,因为条件尚未满足 2024-03-15T11:26:20.330+01:00 INFO 9169 --- [demo] [ scheduling-1] com.example.demo.DemoApplication : 轮询条件,新状态: true 2024-03-15T11:26:20.331+01:00 INFO 9169 --- [demo] [ scheduling-1] com.example.demo.DemoApplication : 就绪状态事件,当前状态: ACCEPTING_TRAFFIC 2024-03-15T11:26:25.319+01:00 INFO 9169 --- [demo] [ scheduling-1] com.example.demo.DemoApplication : 轮询条件,新状态: true 2024-03-15T11:26:30.318+01:00 INFO 9169 --- [demo] [ scheduling-1] com.example.demo.DemoApplication : 轮询条件,新状态: false 2024-03-15T11:26:30.318+01:00 INFO 9169 --- [demo] [ scheduling-1] com.example.demo.DemoApplication : 就绪状态事件,当前状态: REFUSING_TRAFFIC
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.3</version> <relativePath/> <!-- 从仓库查找父依赖 --> </parent> <groupId>com.example</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.properties
spring.application.name=demo
我可以实现自定义事件,但希望复用AvailabilityChangeEvent,因为条件应控制应用状态(除RabbitMQ轮询器外)。其次,我不想将条件的领域特定逻辑与属于应用另一领域的RabbitMQ轮询器耦合。请问有哪些利用Spring(Boot)标准生命周期事件或组件的解决方案?
方案1:自定义ReadinessStateIndicator控制初始就绪状态
Spring Boot的就绪状态由ReadinessStateHealthIndicator维护,自定义一个ReadinessStateIndicator可以覆盖默认初始状态,避免启动时自动发送ACCEPTING_TRAFFIC事件。
实现自定义Indicator
import org.springframework.boot.availability.ReadinessState; import org.springframework.boot.availability.ReadinessStateIndicator; import org.springframework.stereotype.Component; @Component public class CustomReadinessStateIndicator implements ReadinessStateIndicator { private volatile ReadinessState state = ReadinessState.REFUSING_TRAFFIC; @Override public ReadinessState getState() { return state; } public void setState(ReadinessState state) { this.state = state; } }
修改状态更新逻辑
注入自定义Indicator,在更新状态时同步修改Indicator的状态:
private final CustomReadinessStateIndicator readinessStateIndicator; // 构造函数注入 public DemoApplication(ApplicationEventPublisher applicationEventPublisher, CustomReadinessStateIndicator readinessStateIndicator) { this.applicationEventPublisher = applicationEventPublisher; this.readinessStateIndicator = readinessStateIndicator; } private void checkConditionAndBroadcast() { ReadinessState targetState = condition ? ReadinessState.ACCEPTING_TRAFFIC : ReadinessState.REFUSING_TRAFFIC; readinessStateIndicator.setState(targetState); applicationEventPublisher.publishEvent(new AvailabilityChangeEvent<>(this, targetState)); }
这样启动时初始状态就是REFUSING_TRAFFIC,不会触发默认的ACCEPTING_TRAFFIC事件。
方案2:监听ApplicationReadyEvent后设置初始状态
ApplicationReadyEvent在Spring Boot启动完成后触发,晚于默认的ACCEPTING_TRAFFIC事件。放弃ApplicationRunner,改用该事件监听器发送自定义状态,覆盖默认初始值。
修改代码
移除原ApplicationRunner Bean,添加ApplicationReadyEvent监听器:
// 移除原来的ApplicationRunner // @Bean // ApplicationRunner runner(ApplicationEventPublisher applicationEventPublisher) { // return ignored -> checkConditionAndBroadcast(); // } @EventListener(ApplicationReadyEvent.class) public void onApplicationReady() { checkConditionAndBroadcast(); }
此时启动流程中会先收到默认的ACCEPTING_TRAFFIC事件,随后你的自定义事件会覆盖状态,最终结果符合预期。
方案3:在监听器中过滤非预期事件
记录当前预期的状态,在监听器中忽略不符合预期的默认事件,只处理你主动发送的状态变更。
修改代码
private ReadinessState expectedState = ReadinessState.REFUSING_TRAFFIC; private void checkConditionAndBroadcast() { expectedState = condition ? ReadinessState.ACCEPTING_TRAFFIC : ReadinessState.REFUSING_TRAFFIC; applicationEventPublisher.publishEvent(new AvailabilityChangeEvent<>(this, expectedState)); } @EventListener public void onApplicationEvent(AvailabilityChangeEvent<ReadinessState> event) { if (event.getState() != expectedState) { // 忽略默认的ACCEPTING_TRAFFIC事件 return; } LOG.info("就绪状态事件,当前状态: {}", event.getState()); // 处理轮询启停逻辑 }
这种方式下,默认事件会被直接过滤,只有符合你预期的状态变更才会触发业务逻辑。
内容的提问来源于Stack Exchange,提问作者matsev

