You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 08:04:57