Spring Boot 3迁移后MonitoringService循环引用问题求助
解决Spring Boot 2→3迁移中的循环引用问题
问题背景
将Spring Boot从2.x升级到3.x后,出现了循环引用问题,涉及自定义MonitoringService和Spring Boot自动配置类HealthEndpointConfiguration中的healthContributorRegistry,循环关系如下:
┌─────┐ | monitoringService defined in URL [jar:file:/C:/dev/sources/project/target/myproject-0.3.0-SNAPSHOT.jar!/BOOT-INF/classes!/com/omb/project/services/MonitoringService.class] ↑ ↓ | healthContributorRegistry defined in class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.class] └─────┘
我的MonitoringService仅注入了HealthCheckSimpleService,而该服务无其他依赖,理论上不该出现循环引用,完全不知道如何解决。
相关代码
MonitoringService代码
package com.omb.services; import com.omb.restcore.services.HealthCheckSimpleService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.health.*; import org.springframework.stereotype.Component; @Component @Endpoint(id = "health") public class MonitoringService extends HealthEndpoint implements HealthIndicator { @Autowired private HealthCheckSimpleService healthCheckSimpleService; public MonitoringService(HealthContributorRegistry registry, HealthEndpointGroups groups) { super(registry, groups, null); } @Override public Health getHealth(boolean includeDetails) { return HealthIndicator.super.getHealth(includeDetails); } @Override public Health health() { String status = healthCheckSimpleService.healthCheck(null, null).getBody(); if ("RUNNING".equalsIgnoreCase(status)) { return new Health.Builder(Status.UP).build(); } return new Health.Builder(Status.DOWN).build(); } }
HealthCheckSimpleService代码
package com.omb.restcore.services; import com.omb.restcore.exception.InternalServerException; import com.omb.restcore.services.utils.HTTPHelper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.logging.Level; import java.util.logging.Logger; @CrossOrigin @RestController public class HealthCheckSimpleService { private static final Logger LOG = Logger.getLogger(HealthCheckSimpleService.class.getName()); @Value("${api.healthMessage:API is healthy}") private String healthMessage; @Value("${api.maintenanceMessage:API is in maintenance}") private String maintenanceMessage; @Value("${apiStatus:RUNNING}") private Status status; private static void waitToDiscourageAttacks() { try { Thread.sleep(500); } catch (InterruptedException e) { // Spend some time waiting to discourage DOS attacks throw new RuntimeException(e); } } @RequestMapping(value = "healthCheck", method = RequestMethod.GET) public ResponseEntity<String> healthCheck(HttpServletRequest request, HttpServletResponse response) { String message; if (status == Status.RUNNING) { message = healthMessage; } else if (status == Status.MAINTENANCE) { message = maintenanceMessage; } else { throw new InternalServerException("Unsupported status -> " + status); } waitToDiscourageAttacks(); return new ResponseEntity<String>(message, HttpStatus.OK); } @RequestMapping(value = "status", method = RequestMethod.GET) public @ResponseBody Status[] getStatusList(HttpServletRequest request, HttpServletResponse response) { return Status.values(); } @RequestMapping(value = "status", method = RequestMethod.PUT, consumes = MediaType.TEXT_PLAIN_VALUE) public @ResponseBody Status setStatus(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = true) String status) { this.status = Status.valueOf(status); LOG.log(Level.WARNING, "User " + HTTPHelper.getUserFromAuthentication() + " changed status to " + status + " from IP " + HTTPHelper.getRequesterIp(request)); return this.status; } private enum Status { RUNNING, MAINTENANCE } }
问题根源
循环引用的本质原因:
- 自定义
MonitoringService同时做了三件事:标注@Endpoint(id = "health")覆盖默认健康端点、继承HealthEndpoint、实现HealthIndicator - Spring Boot的
HealthContributorRegistry会自动收集所有HealthIndicator类型的Bean并注册,包括你的MonitoringService - 而
MonitoringService的构造函数又依赖HealthContributorRegistry,形成闭环:MonitoringService→HealthContributorRegistry→MonitoringService
解决方案
方案1:仅实现HealthIndicator(推荐)
不需要继承HealthEndpoint,也不需要自定义@Endpoint,让MonitoringService作为普通健康指标被默认端点收集:
package com.omb.services; import com.omb.restcore.services.HealthCheckSimpleService; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.stereotype.Component; @Component public class MonitoringService implements HealthIndicator { private final HealthCheckSimpleService healthCheckSimpleService; // 用构造注入替代字段注入(Spring官方推荐) public MonitoringService(HealthCheckSimpleService healthCheckSimpleService) { this.healthCheckSimpleService = healthCheckSimpleService; } @Override public Health health() { String status = healthCheckSimpleService.healthCheck(null, null).getBody(); if ("RUNNING".equalsIgnoreCase(status)) { return Health.up().build(); } return Health.down().build(); } }
修改后,默认的/actuator/health端点会自动包含这个自定义指标的结果,循环引用也会消失。
方案2:自定义独立端点
如果确实需要自定义健康检查端点,不要复用默认的healthID,改用其他名称(比如custom-health),同时不要继承HealthEndpoint:
package com.omb.services; import com.omb.restcore.services.HealthCheckSimpleService; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.health.Health; import org.springframework.stereotype.Component; @Component @Endpoint(id = "custom-health") public class MonitoringService { private final HealthCheckSimpleService healthCheckSimpleService; public MonitoringService(HealthCheckSimpleService healthCheckSimpleService) { this.healthCheckSimpleService = healthCheckSimpleService; } @ReadOperation public Health getHealth() { String status = healthCheckSimpleService.healthCheck(null, null).getBody(); if ("RUNNING".equalsIgnoreCase(status)) { return Health.up().build(); } return Health.down().build(); } }
此时访问/actuator/custom-health即可获取自定义健康状态,和默认端点互不干扰,无循环引用。
额外优化建议
- 优先使用构造注入替代字段注入,更利于依赖管理和单元测试
HealthCheckSimpleService中的waitToDiscourageAttacks方法会给健康检查固定增加500ms延迟,可能影响监控系统的探测频率,建议根据实际场景评估是否保留
内容的提问来源于stack exchange,提问作者Ousmane MBINTE
相关产品推荐
相关产品推荐

