Skip to content

Spring Boot Admin & management.endpoints.web.base-path

🏷️ Spring Boot Admin

基于前一篇 Spring Boot Admin & Nacos 的博客,如果有服务通过 management.endpoints.web.base-path 属性修改了 Actuator 暴露的端点路径,SBA Server 检查服务健康状态时会失败。原因是 SBA Server 请求的路径和实际暴露的路径不一致。

假设我们将 management.endpoints.web.base-path 设置为 act ,则对应 health 端点实际的路径为 /act/health ,但是 SBA Server 仍然访问的是 /actuator/health

查看 Nacos 中注册的服务信息,可以看到服务的元数据( metadata )中是有 management.endpoints.web.base-path=/act 这条记录的。

在 SBA Server 中,服务实例的端点地址是通过 DefaultServiceInstanceConverter 转换的,获取端点路径的是 getManagementPath() 方法,详细代码如下。

java
private static final String KEY_MANAGEMENT_PATH = "management.context-path";

/**
 * Default context-path to be appended to the url of the discovered service for the
 * managment-url.
 */
private String managementContextPath = "/actuator";

protected String getManagementPath(ServiceInstance instance) {
    String managementPath = instance.getMetadata()
        .get(DefaultServiceInstanceConverter.KEY_MANAGEMENT_PATH);
    if (hasText(managementPath)) {
        return managementPath;
    }
    return this.managementContextPath;
}

可以看到 SBA Server 使用了 management.context-path 元数据作为自定义端点地址的手段(SBA Server 结合 Spring Cloud Discovery 时支持的元数据见官方文档)。

另外还有一个要注意的是 managementContextPath 字段的值,这个值默认为 /actuator ,但是可以通过 spring.boot.admin.discovery.converter.management-context-path 属性配置。

management.context-path 这个属性在 Nacos 服务的元数据中也是有的,但对应的并不是 management.endpoints.web.base-path 的值,而是 management.server.servlet.context-path 的值(仅在配置了 management.server.port 时才会出现在元数据中)。

具体的代码见 NacosRegistrationinit() 方法:

java
/**
 * The metadata key of management port.
 */
public static final String MANAGEMENT_PORT = "management.port";

/**
 * The metadata key of management context-path.
 */
public static final String MANAGEMENT_CONTEXT_PATH = "management.context-path";

/**
 * The metadata key of management address.
 */
public static final String MANAGEMENT_ADDRESS = "management.address";

/**
 * The metadata key of management endpoints web base path.
 */
public static final String MANAGEMENT_ENDPOINT_BASE_PATH = "management.endpoints.web.base-path";

@PostConstruct
public void init() {

    Map<String, String> metadata = nacosDiscoveryProperties.getMetadata();
    Environment env = context.getEnvironment();

    String endpointBasePath = env.getProperty(MANAGEMENT_ENDPOINT_BASE_PATH);
    if (StringUtils.hasLength(endpointBasePath)) {
        metadata.put(MANAGEMENT_ENDPOINT_BASE_PATH, endpointBasePath);
    }

    Integer managementPort = ManagementServerPortUtils.getPort(context);
    if (null != managementPort) {
        metadata.put(MANAGEMENT_PORT, managementPort.toString());
        String contextPath = env
                .getProperty("management.server.servlet.context-path");
        String address = env.getProperty("management.server.address");
        if (StringUtils.hasLength(contextPath)) {
            metadata.put(MANAGEMENT_CONTEXT_PATH, contextPath);
        }
        if (StringUtils.hasLength(address)) {
            metadata.put(MANAGEMENT_ADDRESS, address);
        }
    }

    if (null != nacosDiscoveryProperties.getHeartBeatInterval()) {
        metadata.put(PreservedMetadataKeys.HEART_BEAT_INTERVAL,
                nacosDiscoveryProperties.getHeartBeatInterval().toString());
    }
    if (null != nacosDiscoveryProperties.getHeartBeatTimeout()) {
        metadata.put(PreservedMetadataKeys.HEART_BEAT_TIMEOUT,
                nacosDiscoveryProperties.getHeartBeatTimeout().toString());
    }
    if (null != nacosDiscoveryProperties.getIpDeleteTimeout()) {
        metadata.put(PreservedMetadataKeys.IP_DELETE_TIMEOUT,
                nacosDiscoveryProperties.getIpDeleteTimeout().toString());
    }
    customize(registrationCustomizers);
}

从 SBA Server 和 Nacos Discovery 各自的角度来看,这样的设计其实并没有问题,只是 SBA Server 默认的 ServiceInstanceConverter -- DefaultServiceInstanceConverter -- 没有支持 management.endpoints.web.base-path 元数据而已。

下面举个具体的例子,如果这两个字段做了如下配置:

yaml
management:
  endpoints:
    web:
      base-path: /path2
  server:
    port: 9999
    servlet:
      context-path: /path1

则服务暴露的健康端口地址为 /path1/path2/health

此时 metadata 的内容为:

json
{
    "management.endpoints.web.base-path": "/path2",
    "preserved.register.source": "SPRING_CLOUD",
    "management.port": "9999",
    "management.context-path": "/path1"
}

而在 SBA Server 中访问的端点地址则是 /path1/health

解决方案

既然默认的 DefaultServiceInstanceConverter 不支持,添加一个 Nacos Discovery 专用的 ServiceInstanceConverter 就可以了。

自定义的 NacosServiceInstanceConverter.java 代码如下:

java
import com.alibaba.nacos.api.utils.StringUtils;
import de.codecentric.boot.admin.server.cloud.discovery.DefaultServiceInstanceConverter;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

import static org.springframework.util.StringUtils.*;

/**
 * @author 佳佳
 */
public class NacosServiceInstanceConverter extends DefaultServiceInstanceConverter {

    private static final String KEY_MANAGEMENT_CONTEXT_PATH = "management.context-path";
    private static final String KEY_MANAGEMENT_BASE_PATH = "management.endpoints.web.base-path";
    public static final char URI_SEGMENT_CHARACTER = '/';

    @Override
    protected String getManagementPath(ServiceInstance instance) {
        String contextPath = instance.getMetadata().get(KEY_MANAGEMENT_CONTEXT_PATH);
        String basePath = instance.getMetadata().get(KEY_MANAGEMENT_BASE_PATH);
        return UriComponentsBuilder.newInstance().pathSegment(
                hasText(contextPath) ? trimPath(contextPath) : StringUtils.EMPTY,
                hasText(basePath) ? trimPath(basePath) : this.getManagementContextPath()
        ).build().getPath();
    }

    private static String trimPath(String path) {
        return trimTrailingCharacter(
            trimLeadingCharacter(path, URI_SEGMENT_CHARACTER),
            URI_SEGMENT_CHARACTER
        );
    }
}

对应的配置类 ServiceInstanceConverterConfiguration.java

java
import de.codecentric.boot.admin.server.cloud.discovery.DefaultServiceInstanceConverter;
import de.codecentric.boot.admin.server.cloud.discovery.ServiceInstanceConverter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;

/**
 * @author 佳佳
 */
@Configuration
public class ServiceInstanceConverterConfiguration {

    @Bean
    @ConditionalOnMissingBean({ServiceInstanceConverter.class})
    @ConfigurationProperties(prefix = "spring.boot.admin.discovery.converter")
    @Order(0)
    public DefaultServiceInstanceConverter serviceInstanceConverter() {
        return new NacosServiceInstanceConverter();
    }

}

附 1. management.server.servlet.context-path 已废弃

在 Spring Boot 2.6.11 (具体从哪个版本开始的不太清楚)中 management.server.servlet.context-path 属性已经废弃了,取而代之的是 management.server.base-path 属性( management.server 的其他配置项见 ManagementServerProperties )。但是在 nacos-discovery 2021.0.4.0 中仍然使用的是这个属性值,此时如果要配置 context-path 可采用如下形式做兼容,以保持两个属性的值一致。

yaml
management:
  endpoints:
    web:
      base-path: /path2
  server:
    port: 9999
    base-path: /path1
    servlet:
      context-path: ${management.server.base-path}

附 2. 另一种解决方案

上面的解决的方案是修改 SBA Server 服务的代码,以兼容 Nacos Discovery 的元数据。那么反过来其实也可以修改 Nacos Discovery 的元数据来兼容 SBA Server(只是这样修改的地方会比较多,并不推荐)。

对此也有两种实现方式:

  1. 自定义一个 NacosRegistration 来覆盖默认的注册处理;
  2. 在各个服务的 spring.cloud.nacos.discovery.metadata 配置中自定义 management.context-path 元数据的值。

参考链接