diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfiguration.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfiguration.java index bc568660c04..a73c3b71e78 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfiguration.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfiguration.java @@ -30,18 +30,23 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; import org.springframework.boot.web.client.RestTemplateBuilder; 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 org.springframework.core.env.Environment; +import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; @@ -53,6 +58,7 @@ import de.codecentric.boot.admin.client.registration.ReactiveRegistrationClient; import de.codecentric.boot.admin.client.registration.RegistrationApplicationListener; import de.codecentric.boot.admin.client.registration.RegistrationClient; +import de.codecentric.boot.admin.client.registration.RestClientRegistrationClient; import de.codecentric.boot.admin.client.registration.ServletApplicationFactory; import de.codecentric.boot.admin.client.registration.metadata.CompositeMetadataContributor; import de.codecentric.boot.admin.client.registration.metadata.MetadataContributor; @@ -64,8 +70,8 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication @Conditional(SpringBootAdminClientEnabledCondition.class) -@AutoConfigureAfter({ WebEndpointAutoConfiguration.class, RestTemplateAutoConfiguration.class, - WebClientAutoConfiguration.class }) +@AutoConfigureAfter({ WebEndpointAutoConfiguration.class, RestClientAutoConfiguration.class, + RestTemplateAutoConfiguration.class, WebClientAutoConfiguration.class }) @EnableConfigurationProperties({ ClientProperties.class, InstanceProperties.class, ServerProperties.class, ManagementServerProperties.class }) public class SpringBootAdminClientAutoConfiguration { @@ -153,8 +159,32 @@ public RegistrationClient registrationClient(ClientProperties client) { } @Configuration(proxyBeanMethods = false) - @ConditionalOnBean(WebClient.Builder.class) + @ConditionalOnBean(RestClient.Builder.class) @ConditionalOnMissingBean(RestTemplateBuilder.class) + public static class RestClientRegistrationClientConfig { + + @Bean + @ConditionalOnMissingBean + public RegistrationClient registrationClient(ClientProperties client, RestClient.Builder restClientBuilder, + ClientHttpRequestFactoryBuilder clientHttpRequestFactoryBuilder) { + var factorySettings = ClientHttpRequestFactorySettings.defaults() + .withConnectTimeout(client.getConnectTimeout()) + .withReadTimeout(client.getReadTimeout()); + var clientHttpRequestFactory = clientHttpRequestFactoryBuilder.build(factorySettings); + restClientBuilder = restClientBuilder.requestFactory(clientHttpRequestFactory); + if (client.getUsername() != null && client.getPassword() != null) { + restClientBuilder = restClientBuilder + .requestInterceptor(new BasicAuthenticationInterceptor(client.getUsername(), client.getPassword())); + } + var restClient = restClientBuilder.build(); + return new RestClientRegistrationClient(restClient); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(WebClient.Builder.class) + @ConditionalOnMissingBean({ RestTemplateBuilder.class, RestClient.Builder.class }) public static class ReactiveRegistrationClientConfig { @Bean diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/DefaultApplicationRegistrator.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/DefaultApplicationRegistrator.java index c39c8e97681..5724ad6b9e2 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/DefaultApplicationRegistrator.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/DefaultApplicationRegistrator.java @@ -90,11 +90,11 @@ protected boolean register(Application application, String adminUrl, boolean fir if (firstAttempt) { LOGGER.warn( "Failed to register application as {} at spring-boot-admin ({}): {}. Further attempts are logged on DEBUG level", - application, this.adminUrls, ex.getMessage()); + application, this.adminUrls, ex.getMessage(), ex); } else { LOGGER.debug("Failed to register application as {} at spring-boot-admin ({}): {}", application, - this.adminUrls, ex.getMessage()); + this.adminUrls, ex.getMessage(), ex); } return false; } diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/RestClientRegistrationClient.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/RestClientRegistrationClient.java new file mode 100644 index 00000000000..06d58b115a2 --- /dev/null +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/RestClientRegistrationClient.java @@ -0,0 +1,59 @@ +/* + * Copyright 2014-2024 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.util.Collections; +import java.util.Map; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.client.RestClient; + +public class RestClientRegistrationClient implements RegistrationClient { + + private static final ParameterizedTypeReference> RESPONSE_TYPE = new ParameterizedTypeReference<>() { + }; + + private final RestClient restClient; + + public RestClientRegistrationClient(RestClient restClient) { + this.restClient = restClient; + } + + @Override + public String register(String adminUrl, Application application) { + Map response = this.restClient.post() + .uri(adminUrl) + .headers(this::setRequestHeaders) + .body(application) + .retrieve() + .body(RESPONSE_TYPE); + return response.get("id").toString(); + } + + @Override + public void deregister(String adminUrl, String id) { + this.restClient.delete().uri(adminUrl + '/' + id).retrieve().toBodilessEntity(); + } + + protected void setRequestHeaders(HttpHeaders headers) { + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + } + +} diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/ClientPropertiesTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/ClientPropertiesTest.java index dfde77c6afd..fc3e15e0157 100644 --- a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/ClientPropertiesTest.java +++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/ClientPropertiesTest.java @@ -53,7 +53,7 @@ public void should_default_autoDeregister_to_true() { } @Test - public void should_return_all_admiUrls() { + public void should_return_all_adminUrls() { ClientProperties clientProperties = new ClientProperties(); clientProperties.setApiPath("register"); clientProperties.setUrl(new String[] { "http://first", "http://second" }); diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfigurationTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfigurationTest.java index 664be968c97..81be039c577 100644 --- a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfigurationTest.java +++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfigurationTest.java @@ -22,6 +22,9 @@ 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.autoconfigure.http.client.HttpClientAutoConfiguration; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; @@ -34,6 +37,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import de.codecentric.boot.admin.client.registration.ApplicationRegistrator; @@ -113,6 +117,32 @@ public void blockingClientInBlockingEnvironment() { }); } + @Test + public void restClientRegistrationClientInBlockingEnvironment() { + WebApplicationContextRunner webApplicationContextRunner = new WebApplicationContextRunner().withConfiguration( + AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, HttpClientAutoConfiguration.class, + RestClientAutoConfiguration.class, SpringBootAdminClientAutoConfiguration.class)); + + webApplicationContextRunner + .withPropertyValues("spring.boot.admin.client.url:http://localhost:8081", + "spring.boot.admin.client.connectTimeout=1337", "spring.boot.admin.client.readTimeout=42") + .withInitializer(new ConditionEvaluationReportLoggingListener()) + .run((context) -> { + RegistrationClient registrationClient = context.getBean(RegistrationClient.class); + RestClient restClient = (RestClient) ReflectionTestUtils.getField(registrationClient, "restClient"); + assertThat(restClient).isNotNull(); + + ClientHttpRequestFactory requestFactory = (ClientHttpRequestFactory) ReflectionTestUtils + .getField(restClient, "clientRequestFactory"); + + Integer connectTimeout = (Integer) ReflectionTestUtils.getField(requestFactory, "connectTimeout"); + assertThat(connectTimeout).isEqualTo(1337); + Duration readTimeout = (Duration) ReflectionTestUtils.getField(requestFactory, "readTimeout"); + assertThat(readTimeout).isEqualTo(Duration.ofMillis(42)); + }); + } + @Test public void customBlockingClientInReactiveEnvironment() { ReactiveWebApplicationContextRunner reactiveContextRunner = new ReactiveWebApplicationContextRunner() diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/RestClientRegistrationClientTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/RestClientRegistrationClientTest.java new file mode 100644 index 00000000000..42d86d670f1 --- /dev/null +++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/RestClientRegistrationClientTest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2014-2024 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 org.junit.jupiter.api.BeforeEach; +import org.springframework.web.client.RestClient; + +public class RestClientRegistrationClientTest extends AbstractRegistrationClientTest { + + @BeforeEach + public void setUp() { + super.setUp(new RestClientRegistrationClient(RestClient.create())); + } + +}