From 2aba8e1113a4d57050c7d3626ae132d6d09154f2 Mon Sep 17 00:00:00 2001
From: myProjectsRavi <95069115+myProjectsRavi@users.noreply.github.com>
Date: Sun, 4 Oct 2026 10:24:10 +0530
Subject: [PATCH 1/2] feat: support Spring Cloud InetUtils
Use conditional servlet and reactive factories for host resolution.
Keep Spring Cloud optional and preserve existing URL handling.
Closes #1323.
---
spring-boot-admin-client/pom.xml | 5 +
...AdminClientInetUtilsAutoConfiguration.java | 97 +++++++
.../InetUtilsReactiveApplicationFactory.java | 48 ++++
.../InetUtilsServletApplicationFactory.java | 51 ++++
...ot.autoconfigure.AutoConfiguration.imports | 1 +
...nClientInetUtilsAutoConfigurationTest.java | 230 ++++++++++++++++
.../InetUtilsApplicationFactoryTest.java | 249 ++++++++++++++++++
.../site/docs/03-client/20-registration.md | 27 ++
8 files changed, 708 insertions(+)
create mode 100644 spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientInetUtilsAutoConfiguration.java
create mode 100644 spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/InetUtilsReactiveApplicationFactory.java
create mode 100644 spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/InetUtilsServletApplicationFactory.java
create mode 100644 spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientInetUtilsAutoConfigurationTest.java
create mode 100644 spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/InetUtilsApplicationFactoryTest.java
diff --git a/spring-boot-admin-client/pom.xml b/spring-boot-admin-client/pom.xml
index ead3f1533f8..2c5991703a1 100644
--- a/spring-boot-admin-client/pom.xml
+++ b/spring-boot-admin-client/pom.xml
@@ -54,6 +54,11 @@
spring-boot-starter-webmvc
true
+
+ org.springframework.cloud
+ spring-cloud-commons
+ true
+
org.springframework.boot
spring-boot-autoconfigure-processor
diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientInetUtilsAutoConfiguration.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientInetUtilsAutoConfiguration.java
new file mode 100644
index 00000000000..50e95b9be2a
--- /dev/null
+++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientInetUtilsAutoConfiguration.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2014-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.codecentric.boot.admin.client.config;
+
+import java.util.Collections;
+import java.util.List;
+
+import jakarta.servlet.ServletContext;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
+import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties;
+import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
+import org.springframework.boot.web.server.autoconfigure.ServerProperties;
+import org.springframework.boot.webflux.autoconfigure.WebFluxProperties;
+import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath;
+import org.springframework.cloud.commons.util.InetUtils;
+import org.springframework.cloud.commons.util.UtilAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+import de.codecentric.boot.admin.client.registration.ApplicationFactory;
+import de.codecentric.boot.admin.client.registration.InetUtilsReactiveApplicationFactory;
+import de.codecentric.boot.admin.client.registration.InetUtilsServletApplicationFactory;
+import de.codecentric.boot.admin.client.registration.metadata.CompositeMetadataContributor;
+import de.codecentric.boot.admin.client.registration.metadata.MetadataContributor;
+
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnWebApplication
+@ConditionalOnClass(InetUtils.class)
+@ConditionalOnBean(InetUtils.class)
+@Conditional(SpringBootAdminClientEnabledCondition.class)
+@AutoConfigureAfter({ UtilAutoConfiguration.class, SpringBootAdminClientCloudFoundryAutoConfiguration.class })
+@AutoConfigureBefore(SpringBootAdminClientAutoConfiguration.class)
+public class SpringBootAdminClientInetUtilsAutoConfiguration {
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnWebApplication(type = Type.SERVLET)
+ public static class ServletConfiguration {
+
+ @Bean
+ @Lazy(false)
+ @ConditionalOnMissingBean(ApplicationFactory.class)
+ public ApplicationFactory applicationFactory(InstanceProperties instance, ManagementServerProperties management,
+ ServerProperties server, ServletContext servletContext, PathMappedEndpoints pathMappedEndpoints,
+ WebEndpointProperties webEndpoint, ObjectProvider> metadataContributors,
+ DispatcherServletPath dispatcherServletPath, InetUtils inetUtils) {
+ return new InetUtilsServletApplicationFactory(instance, management, server, servletContext,
+ pathMappedEndpoints, webEndpoint,
+ new CompositeMetadataContributor(metadataContributors.getIfAvailable(Collections::emptyList)),
+ dispatcherServletPath, inetUtils);
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnWebApplication(type = Type.REACTIVE)
+ public static class ReactiveConfiguration {
+
+ @Bean
+ @Lazy(false)
+ @ConditionalOnMissingBean(ApplicationFactory.class)
+ public ApplicationFactory applicationFactory(InstanceProperties instance, ManagementServerProperties management,
+ ServerProperties server, PathMappedEndpoints pathMappedEndpoints, WebEndpointProperties webEndpoint,
+ ObjectProvider> metadataContributors, WebFluxProperties webFluxProperties,
+ InetUtils inetUtils) {
+ return new InetUtilsReactiveApplicationFactory(instance, management, server, pathMappedEndpoints,
+ webEndpoint,
+ new CompositeMetadataContributor(metadataContributors.getIfAvailable(Collections::emptyList)),
+ webFluxProperties, inetUtils);
+ }
+
+ }
+
+}
diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/InetUtilsReactiveApplicationFactory.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/InetUtilsReactiveApplicationFactory.java
new file mode 100644
index 00000000000..42b84b4d956
--- /dev/null
+++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/InetUtilsReactiveApplicationFactory.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2014-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.codecentric.boot.admin.client.registration;
+
+import java.net.InetAddress;
+
+import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
+import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties;
+import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
+import org.springframework.boot.web.server.autoconfigure.ServerProperties;
+import org.springframework.boot.webflux.autoconfigure.WebFluxProperties;
+import org.springframework.cloud.commons.util.InetUtils;
+
+import de.codecentric.boot.admin.client.config.InstanceProperties;
+import de.codecentric.boot.admin.client.registration.metadata.MetadataContributor;
+
+public class InetUtilsReactiveApplicationFactory extends ReactiveApplicationFactory {
+
+ private final InetUtils inetUtils;
+
+ public InetUtilsReactiveApplicationFactory(InstanceProperties instance, ManagementServerProperties management,
+ ServerProperties server, PathMappedEndpoints pathMappedEndpoints, WebEndpointProperties webEndpoint,
+ MetadataContributor metadataContributor, WebFluxProperties webFluxProperties, InetUtils inetUtils) {
+ super(instance, management, server, pathMappedEndpoints, webEndpoint, metadataContributor, webFluxProperties);
+ this.inetUtils = inetUtils;
+ }
+
+ @Override
+ protected InetAddress getLocalHost() {
+ InetAddress address = this.inetUtils.findFirstNonLoopbackAddress();
+ return (address != null) ? address : super.getLocalHost();
+ }
+
+}
diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/InetUtilsServletApplicationFactory.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/InetUtilsServletApplicationFactory.java
new file mode 100644
index 00000000000..9212a89d1fb
--- /dev/null
+++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/InetUtilsServletApplicationFactory.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2014-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.codecentric.boot.admin.client.registration;
+
+import java.net.InetAddress;
+
+import jakarta.servlet.ServletContext;
+import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
+import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties;
+import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
+import org.springframework.boot.web.server.autoconfigure.ServerProperties;
+import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath;
+import org.springframework.cloud.commons.util.InetUtils;
+
+import de.codecentric.boot.admin.client.config.InstanceProperties;
+import de.codecentric.boot.admin.client.registration.metadata.MetadataContributor;
+
+public class InetUtilsServletApplicationFactory extends ServletApplicationFactory {
+
+ private final InetUtils inetUtils;
+
+ public InetUtilsServletApplicationFactory(InstanceProperties instance, ManagementServerProperties management,
+ ServerProperties server, ServletContext servletContext, PathMappedEndpoints pathMappedEndpoints,
+ WebEndpointProperties webEndpoint, MetadataContributor metadataContributor,
+ DispatcherServletPath dispatcherServletPath, InetUtils inetUtils) {
+ super(instance, management, server, servletContext, pathMappedEndpoints, webEndpoint, metadataContributor,
+ dispatcherServletPath);
+ this.inetUtils = inetUtils;
+ }
+
+ @Override
+ protected InetAddress getLocalHost() {
+ InetAddress address = this.inetUtils.findFirstNonLoopbackAddress();
+ return (address != null) ? address : super.getLocalHost();
+ }
+
+}
diff --git a/spring-boot-admin-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-admin-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index d9e84cbb914..b2542433380 100644
--- a/spring-boot-admin-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-boot-admin-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1,3 +1,4 @@
de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration
de.codecentric.boot.admin.client.config.SpringBootAdminClientCloudFoundryAutoConfiguration
+de.codecentric.boot.admin.client.config.SpringBootAdminClientInetUtilsAutoConfiguration
de.codecentric.boot.admin.client.config.SpringNativeClientAutoConfiguration
diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientInetUtilsAutoConfigurationTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientInetUtilsAutoConfigurationTest.java
new file mode 100644
index 00000000000..ad21cca0275
--- /dev/null
+++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientInetUtilsAutoConfigurationTest.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2014-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.codecentric.boot.admin.client.config;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
+import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
+import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.FilteredClassLoader;
+import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
+import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
+import org.springframework.boot.web.server.WebServer;
+import org.springframework.boot.web.server.context.WebServerApplicationContext;
+import org.springframework.boot.web.server.context.WebServerInitializedEvent;
+import org.springframework.boot.webflux.autoconfigure.WebFluxProperties;
+import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
+import org.springframework.cloud.commons.util.InetUtils;
+import org.springframework.cloud.commons.util.InetUtilsProperties;
+import org.springframework.cloud.commons.util.UtilAutoConfiguration;
+
+import de.codecentric.boot.admin.client.registration.ApplicationFactory;
+import de.codecentric.boot.admin.client.registration.CloudFoundryApplicationFactory;
+import de.codecentric.boot.admin.client.registration.DefaultApplicationFactory;
+import de.codecentric.boot.admin.client.registration.InetUtilsReactiveApplicationFactory;
+import de.codecentric.boot.admin.client.registration.InetUtilsServletApplicationFactory;
+import de.codecentric.boot.admin.client.registration.ReactiveApplicationFactory;
+import de.codecentric.boot.admin.client.registration.ServletApplicationFactory;
+import de.codecentric.boot.admin.client.registration.metadata.MetadataContributor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class SpringBootAdminClientInetUtilsAutoConfigurationTest {
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void without_spring_cloud(WebApplicationType type) {
+ contextRunner(type).withClassLoader(new FilteredClassLoader("org.springframework.cloud")).run((context) -> {
+ assertThat(context).hasSingleBean(ApplicationFactory.class);
+ assertThat(context.getBean(ApplicationFactory.class)).isExactlyInstanceOf(defaultFactoryType(type));
+ });
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void without_inetutils_bean(WebApplicationType type) {
+ contextRunner(type).run((context) -> {
+ assertThat(context).doesNotHaveBean(InetUtils.class);
+ assertThat(context).hasSingleBean(ApplicationFactory.class);
+ assertThat(context.getBean(ApplicationFactory.class)).isExactlyInstanceOf(defaultFactoryType(type));
+ });
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void with_inetutils_bean(WebApplicationType type) {
+ contextRunner(type).withBean(InetUtils.class, () -> mock(InetUtils.class)).run((context) -> {
+ assertThat(context).hasSingleBean(ApplicationFactory.class);
+ assertThat(context.getBean(ApplicationFactory.class)).isExactlyInstanceOf(inetUtilsFactoryType(type));
+ });
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void with_spring_cloud_auto_configuration(WebApplicationType type) {
+ contextRunner(type).withConfiguration(AutoConfigurations.of(UtilAutoConfiguration.class))
+ .withPropertyValues("spring.cloud.inetutils.preferred-networks[0]=192.168.",
+ "spring.cloud.inetutils.ignored-interfaces[0]=docker.*")
+ .run((context) -> {
+ assertThat(context).hasSingleBean(InetUtils.class);
+ assertThat(context).hasSingleBean(ApplicationFactory.class);
+ assertThat(context.getBean(ApplicationFactory.class)).isExactlyInstanceOf(inetUtilsFactoryType(type));
+ InetUtilsProperties properties = context.getBean(InetUtilsProperties.class);
+ assertThat(properties.getPreferredNetworks()).containsExactly("192.168.");
+ assertThat(properties.getIgnoredInterfaces()).containsExactly("docker.*");
+ });
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void with_spring_cloud_util_disabled(WebApplicationType type) {
+ contextRunner(type).withConfiguration(AutoConfigurations.of(UtilAutoConfiguration.class))
+ .withPropertyValues("spring.cloud.util.enabled=false")
+ .run((context) -> {
+ assertThat(context).doesNotHaveBean(InetUtils.class);
+ assertThat(context).hasSingleBean(ApplicationFactory.class);
+ assertThat(context.getBean(ApplicationFactory.class)).isExactlyInstanceOf(defaultFactoryType(type));
+ });
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void disabled_client(WebApplicationType type) {
+ contextRunner(type).withBean(InetUtils.class, () -> mock(InetUtils.class))
+ .withPropertyValues("spring.boot.admin.client.enabled=false")
+ .run((context) -> assertThat(context).doesNotHaveBean(ApplicationFactory.class));
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void unconfigured_client(WebApplicationType type) {
+ contextRunner(type).withBean(InetUtils.class, () -> mock(InetUtils.class))
+ .withPropertyValues("spring.boot.admin.client.url=")
+ .run((context) -> assertThat(context).doesNotHaveBean(ApplicationFactory.class));
+ }
+
+ @Test
+ void non_web_application() {
+ new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(SpringBootAdminClientAutoConfiguration.class,
+ SpringBootAdminClientInetUtilsAutoConfiguration.class, UtilAutoConfiguration.class))
+ .withPropertyValues("spring.boot.admin.client.url=http://localhost:8081")
+ .run((context) -> assertThat(context).doesNotHaveBean(ApplicationFactory.class));
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void custom_factory_takes_precedence(WebApplicationType type) {
+ ApplicationFactory factory = mock(ApplicationFactory.class);
+ contextRunner(type).withBean(InetUtils.class, () -> mock(InetUtils.class))
+ .withBean(ApplicationFactory.class, () -> factory)
+ .run((context) -> {
+ assertThat(context).hasSingleBean(ApplicationFactory.class);
+ assertThat(context.getBean(ApplicationFactory.class)).isSameAs(factory);
+ });
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void cloud_foundry_takes_precedence(WebApplicationType type) {
+ contextRunner(type).withBean(InetUtils.class, () -> mock(InetUtils.class))
+ .withPropertyValues("VCAP_APPLICATION:{}")
+ .run((context) -> {
+ assertThat(context).hasSingleBean(ApplicationFactory.class);
+ assertThat(context.getBean(ApplicationFactory.class))
+ .isExactlyInstanceOf(CloudFoundryApplicationFactory.class);
+ });
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void preserves_metadata_contributors(WebApplicationType type) {
+ contextRunner(type).withBean(InetUtils.class, () -> mock(InetUtils.class))
+ .withBean(MetadataContributor.class, () -> () -> Map.of("custom", "contributor", "component", "test"))
+ .withPropertyValues("spring.boot.admin.client.instance.service-url=https://service.example",
+ "spring.boot.admin.client.instance.management-url=https://management.example",
+ "spring.boot.admin.client.instance.health-url=https://health.example",
+ "spring.boot.admin.client.instance.metadata.custom=instance")
+ .run((context) -> assertThat(context.getBean(ApplicationFactory.class).createApplication().getMetadata())
+ .containsEntry("component", "test")
+ .containsEntry("custom", "instance"));
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void uses_primary_inetutils(WebApplicationType type) throws UnknownHostException {
+ InetUtils primary = mock(InetUtils.class);
+ when(primary.findFirstNonLoopbackAddress())
+ .thenReturn(InetAddress.getByAddress(new byte[] { (byte) 192, 0, 2, 20 }));
+ contextRunner(type).withBean("secondaryInetUtils", InetUtils.class, () -> mock(InetUtils.class))
+ .withBean("primaryInetUtils", InetUtils.class, () -> primary, (definition) -> definition.setPrimary(true))
+ .withPropertyValues("spring.boot.admin.client.instance.service-host-type=IP",
+ "spring.boot.admin.client.instance.health-url=http://localhost:8080/actuator/health")
+ .run((context) -> {
+ DefaultApplicationFactory factory = context.getBean(DefaultApplicationFactory.class);
+ WebServerInitializedEvent event = mock(WebServerInitializedEvent.class);
+ WebServerApplicationContext applicationContext = mock(WebServerApplicationContext.class);
+ WebServer webServer = mock(WebServer.class);
+ when(applicationContext.getServerNamespace()).thenReturn("server");
+ when(webServer.getPort()).thenReturn(8080);
+ when(event.getApplicationContext()).thenReturn(applicationContext);
+ when(event.getWebServer()).thenReturn(webServer);
+ factory.onWebServerInitialized(event);
+ assertThat(factory.createApplication().getServiceUrl()).isEqualTo("http://192.0.2.20:8080/");
+ });
+ }
+
+ private AbstractApplicationContextRunner, ?, ?> contextRunner(WebApplicationType type) {
+ AutoConfigurations configurations = AutoConfigurations.of(EndpointAutoConfiguration.class,
+ WebEndpointAutoConfiguration.class, HttpClientAutoConfiguration.class,
+ RestClientAutoConfiguration.class, SpringBootAdminClientAutoConfiguration.class,
+ SpringBootAdminClientInetUtilsAutoConfiguration.class,
+ SpringBootAdminClientCloudFoundryAutoConfiguration.class);
+ if (type == WebApplicationType.SERVLET) {
+ return new WebApplicationContextRunner().withConfiguration(configurations)
+ .withConfiguration(AutoConfigurations.of(DispatcherServletAutoConfiguration.class))
+ .withPropertyValues("spring.boot.admin.client.url=http://localhost:8081");
+ }
+ return new ReactiveWebApplicationContextRunner().withConfiguration(configurations)
+ .withBean(WebFluxProperties.class)
+ .withPropertyValues("spring.boot.admin.client.url=http://localhost:8081");
+ }
+
+ private Class extends ApplicationFactory> defaultFactoryType(WebApplicationType type) {
+ return (type == WebApplicationType.SERVLET) ? ServletApplicationFactory.class
+ : ReactiveApplicationFactory.class;
+ }
+
+ private Class extends ApplicationFactory> inetUtilsFactoryType(WebApplicationType type) {
+ return (type == WebApplicationType.SERVLET) ? InetUtilsServletApplicationFactory.class
+ : InetUtilsReactiveApplicationFactory.class;
+ }
+
+}
diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/InetUtilsApplicationFactoryTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/InetUtilsApplicationFactoryTest.java
new file mode 100644
index 00000000000..c32ce1e87c7
--- /dev/null
+++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/InetUtilsApplicationFactoryTest.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2014-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.codecentric.boot.admin.client.registration;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Collections;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.MockedStatic;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
+import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties;
+import org.springframework.boot.actuate.endpoint.EndpointId;
+import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
+import org.springframework.boot.web.server.Ssl;
+import org.springframework.boot.web.server.WebServer;
+import org.springframework.boot.web.server.autoconfigure.ServerProperties;
+import org.springframework.boot.web.server.context.WebServerApplicationContext;
+import org.springframework.boot.web.server.context.WebServerInitializedEvent;
+import org.springframework.boot.webflux.autoconfigure.WebFluxProperties;
+import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath;
+import org.springframework.cloud.commons.util.InetUtils;
+import org.springframework.mock.web.MockServletContext;
+
+import de.codecentric.boot.admin.client.config.InstanceProperties;
+import de.codecentric.boot.admin.client.config.ServiceHostType;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+class InetUtilsApplicationFactoryTest {
+
+ private final InstanceProperties instance = new InstanceProperties();
+
+ private final ServerProperties server = new ServerProperties();
+
+ private final ManagementServerProperties management = new ManagementServerProperties();
+
+ private final PathMappedEndpoints pathMappedEndpoints = mock(PathMappedEndpoints.class);
+
+ private final WebEndpointProperties webEndpoint = new WebEndpointProperties();
+
+ private final InetUtils inetUtils = mock(InetUtils.class);
+
+ @BeforeEach
+ void setup() {
+ instance.setServiceHostType(ServiceHostType.IP);
+ management.setBasePath("/management");
+ when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/actuator/health");
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void uses_preferred_address(WebApplicationType type) throws UnknownHostException {
+ InetAddress address = InetAddress.getByAddress("preferred-host", new byte[] { (byte) 192, 0, 2, 10 });
+ when(inetUtils.findFirstNonLoopbackAddress()).thenReturn(address);
+ DefaultApplicationFactory factory = createFactory(type);
+ publishPort(factory, "server", 8080);
+ publishPort(factory, "management", 9090);
+
+ Application application = factory.createApplication();
+ assertThat(application.getServiceUrl()).isEqualTo("http://192.0.2.10:8080/app");
+ assertThat(application.getManagementUrl()).isEqualTo("http://192.0.2.10:9090/management/actuator");
+ assertThat(application.getHealthUrl()).isEqualTo("http://192.0.2.10:9090/management/actuator/health");
+ }
+
+ @ParameterizedTest
+ @MethodSource("hostTypes")
+ void preserves_host_type(WebApplicationType type, ServiceHostType hostType, String host) {
+ InetAddress address = mock(InetAddress.class);
+ when(address.getHostAddress()).thenReturn("192.0.2.10");
+ when(address.getHostName()).thenReturn("preferred-host");
+ when(address.getCanonicalHostName()).thenReturn("preferred-host.example");
+ when(inetUtils.findFirstNonLoopbackAddress()).thenReturn(address);
+ instance.setServiceHostType(hostType);
+ DefaultApplicationFactory factory = createFactory(type);
+ publishPort(factory, "server", 8080);
+
+ assertThat(factory.createApplication().getServiceUrl()).isEqualTo("http://" + host + ":8080/app");
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void falls_back_when_no_address_is_available(WebApplicationType type) throws UnknownHostException {
+ assertThat(createFactory(type).getLocalHost()).isEqualTo(InetAddress.getLocalHost());
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void preserves_host_resolution_failure(WebApplicationType type) {
+ DefaultApplicationFactory factory = createFactory(type);
+ UnknownHostException failure = new UnknownHostException("Local host unavailable");
+ try (MockedStatic addresses = mockStatic(InetAddress.class)) {
+ addresses.when(InetAddress::getLocalHost).thenThrow(failure);
+ assertThatThrownBy(factory::getLocalHost).isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(failure.getMessage())
+ .hasCause(failure);
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void preserves_explicit_addresses(WebApplicationType type) throws UnknownHostException {
+ server.setAddress(InetAddress.getByName("127.0.0.1"));
+ management.setAddress(InetAddress.getByName("127.0.0.2"));
+ DefaultApplicationFactory factory = createFactory(type);
+ publishPort(factory, "server", 8080);
+ publishPort(factory, "management", 9090);
+
+ Application application = factory.createApplication();
+ assertThat(application.getServiceUrl()).isEqualTo("http://127.0.0.1:8080/app");
+ assertThat(application.getManagementUrl()).isEqualTo("http://127.0.0.2:9090/management/actuator");
+ assertThat(application.getHealthUrl()).isEqualTo("http://127.0.0.2:9090/management/actuator/health");
+ verifyNoInteractions(inetUtils);
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void preserves_explicit_management_address(WebApplicationType type) throws UnknownHostException {
+ when(inetUtils.findFirstNonLoopbackAddress())
+ .thenReturn(InetAddress.getByAddress(new byte[] { (byte) 192, 0, 2, 10 }));
+ management.setAddress(InetAddress.getByName("127.0.0.2"));
+ DefaultApplicationFactory factory = createFactory(type);
+ publishPort(factory, "server", 8080);
+ publishPort(factory, "management", 9090);
+
+ Application application = factory.createApplication();
+ assertThat(application.getServiceUrl()).isEqualTo("http://192.0.2.10:8080/app");
+ assertThat(application.getManagementUrl()).isEqualTo("http://127.0.0.2:9090/management/actuator");
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void preserves_explicit_urls(WebApplicationType type) {
+ instance.setServiceUrl("https://service.example");
+ instance.setManagementUrl("https://management.example");
+ instance.setHealthUrl("https://health.example");
+
+ Application application = createFactory(type).createApplication();
+ assertThat(application.getServiceUrl()).isEqualTo("https://service.example");
+ assertThat(application.getManagementUrl()).isEqualTo("https://management.example");
+ assertThat(application.getHealthUrl()).isEqualTo("https://health.example");
+ verifyNoInteractions(inetUtils);
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void preserves_base_urls(WebApplicationType type) {
+ instance.setServiceBaseUrl("https://service.example");
+ instance.setManagementBaseUrl("https://management.example");
+
+ Application application = createFactory(type).createApplication();
+ assertThat(application.getServiceUrl()).isEqualTo("https://service.example/app");
+ assertThat(application.getManagementUrl()).isEqualTo("https://management.example/actuator");
+ assertThat(application.getHealthUrl()).isEqualTo("https://management.example/actuator/health");
+ verifyNoInteractions(inetUtils);
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void preserves_paths_on_shared_port(WebApplicationType type) throws UnknownHostException {
+ when(inetUtils.findFirstNonLoopbackAddress())
+ .thenReturn(InetAddress.getByAddress(new byte[] { (byte) 192, 0, 2, 10 }));
+ DefaultApplicationFactory factory = createFactory(type);
+ publishPort(factory, "server", 8080);
+ String managementBaseUrl = "http://192.0.2.10:8080/app"
+ + ((type == WebApplicationType.SERVLET) ? "/dispatcher/management" : "");
+
+ Application application = factory.createApplication();
+ assertThat(application.getServiceUrl()).isEqualTo("http://192.0.2.10:8080/app");
+ assertThat(application.getManagementUrl()).isEqualTo(managementBaseUrl + "/actuator");
+ assertThat(application.getHealthUrl()).isEqualTo(managementBaseUrl + "/actuator/health");
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = WebApplicationType.class, names = { "SERVLET", "REACTIVE" })
+ void preserves_management_ssl(WebApplicationType type) throws UnknownHostException {
+ when(inetUtils.findFirstNonLoopbackAddress())
+ .thenReturn(InetAddress.getByAddress(new byte[] { (byte) 192, 0, 2, 10 }));
+ management.setSsl(new Ssl());
+ management.getSsl().setEnabled(true);
+ DefaultApplicationFactory factory = createFactory(type);
+ publishPort(factory, "server", 8080);
+ publishPort(factory, "management", 9090);
+
+ Application application = factory.createApplication();
+ assertThat(application.getServiceUrl()).isEqualTo("http://192.0.2.10:8080/app");
+ assertThat(application.getManagementUrl()).isEqualTo("https://192.0.2.10:9090/management/actuator");
+ assertThat(application.getHealthUrl()).isEqualTo("https://192.0.2.10:9090/management/actuator/health");
+ }
+
+ private DefaultApplicationFactory createFactory(WebApplicationType type) {
+ if (type == WebApplicationType.SERVLET) {
+ MockServletContext servletContext = new MockServletContext();
+ servletContext.setContextPath("/app");
+ DispatcherServletPath dispatcherServletPath = mock(DispatcherServletPath.class);
+ when(dispatcherServletPath.getPrefix()).thenReturn("/dispatcher");
+ return new InetUtilsServletApplicationFactory(instance, management, server, servletContext,
+ pathMappedEndpoints, webEndpoint, Collections::emptyMap, dispatcherServletPath, inetUtils);
+ }
+ WebFluxProperties webFluxProperties = new WebFluxProperties();
+ webFluxProperties.setBasePath("/app");
+ return new InetUtilsReactiveApplicationFactory(instance, management, server, pathMappedEndpoints, webEndpoint,
+ Collections::emptyMap, webFluxProperties, inetUtils);
+ }
+
+ private void publishPort(DefaultApplicationFactory factory, String namespace, int port) {
+ WebServerInitializedEvent event = mock(WebServerInitializedEvent.class);
+ WebServerApplicationContext context = mock(WebServerApplicationContext.class);
+ WebServer webServer = mock(WebServer.class);
+ when(context.getServerNamespace()).thenReturn(namespace);
+ when(webServer.getPort()).thenReturn(port);
+ when(event.getApplicationContext()).thenReturn(context);
+ when(event.getWebServer()).thenReturn(webServer);
+ factory.onWebServerInitialized(event);
+ }
+
+ private static Stream hostTypes() {
+ return Stream.of(WebApplicationType.SERVLET, WebApplicationType.REACTIVE)
+ .flatMap((type) -> Stream.of(Arguments.of(type, ServiceHostType.IP, "192.0.2.10"),
+ Arguments.of(type, ServiceHostType.HOST_NAME, "preferred-host"),
+ Arguments.of(type, ServiceHostType.CANONICAL_HOST_NAME, "preferred-host.example")));
+ }
+
+}
diff --git a/spring-boot-admin-docs/src/site/docs/03-client/20-registration.md b/spring-boot-admin-docs/src/site/docs/03-client/20-registration.md
index 9fbcebc08e2..fba4a8264b2 100644
--- a/spring-boot-admin-docs/src/site/docs/03-client/20-registration.md
+++ b/spring-boot-admin-docs/src/site/docs/03-client/20-registration.md
@@ -184,6 +184,33 @@ spring:
- **`IP`**: Use the IP address
- **`CANONICAL`**: Use the canonical hostname
+### Spring Cloud InetUtils
+
+When a Spring Cloud `InetUtils` bean is available, the client uses it to select the local address for registration.
+The servlet and reactive factories retain their usual context paths and management URL handling.
+
+For example, prefer a network and ignore interfaces through Spring Cloud configuration:
+
+```yaml
+spring:
+ cloud:
+ inetutils:
+ preferred-networks:
+ - '192.168.'
+ ignored-interfaces:
+ - 'docker.*'
+ boot:
+ admin:
+ client:
+ instance:
+ service-host-type: IP
+```
+
+Explicit registration URLs, `server.address`, and `management.server.address` retain their usual precedence.
+If `InetUtils` returns no address, the client falls back to `InetAddress.getLocalHost()`.
+Applications without Spring Cloud continue to use the default factories.
+Custom `ApplicationFactory` beans and the Cloud Foundry factory take precedence over the InetUtils factories.
+
### Custom ApplicationFactory
Create a custom factory for specialized registration logic:
From b88359d1b2b25bc33bbf415132bb8c9fb35e5f99 Mon Sep 17 00:00:00 2001
From: myProjectsRavi <95069115+myProjectsRavi@users.noreply.github.com>
Date: Sat, 3 Oct 2026 22:26:54 -0700
Subject: [PATCH 2/2] test: isolate default client registration tests
Exclude InetUtils auto-configuration from tests that expect the default local host lookup.
---
.../boot/admin/client/AbstractClientApplicationTest.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/AbstractClientApplicationTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/AbstractClientApplicationTest.java
index dcf04f819ac..828cbf2c8da 100644
--- a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/AbstractClientApplicationTest.java
+++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/AbstractClientApplicationTest.java
@@ -37,6 +37,7 @@
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.event.EventListener;
+import de.codecentric.boot.admin.client.config.SpringBootAdminClientInetUtilsAutoConfiguration;
import de.codecentric.boot.admin.client.registration.ApplicationRegistrator;
import static com.github.tomakehurst.wiremock.client.WireMock.created;
@@ -144,7 +145,7 @@ private int getManagementPort() {
}
@SpringBootConfiguration
- @EnableAutoConfiguration
+ @EnableAutoConfiguration(exclude = SpringBootAdminClientInetUtilsAutoConfiguration.class)
public static class TestClientApplication {
@Autowired