diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestTemplateEurekaHttpClient.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestTemplateEurekaHttpClient.java index bf14a2a9f..6218b10fc 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestTemplateEurekaHttpClient.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestTemplateEurekaHttpClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-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. @@ -16,6 +16,7 @@ package org.springframework.cloud.netflix.eureka.http; +import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -38,11 +39,13 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; import static com.netflix.discovery.shared.transport.EurekaHttpResponse.anEurekaHttpResponse; /** * @author Daniel Lavoie + * @author Václav Plic */ public class RestTemplateEurekaHttpClient implements EurekaHttpClient { @@ -68,13 +71,14 @@ public RestTemplate getRestTemplate() { @Override public EurekaHttpResponse register(InstanceInfo info) { - String urlPath = serviceUrl + "apps/" + info.getAppName(); + URI uri = UriComponentsBuilder.fromHttpUrl(serviceUrl).path("apps/{appName}").buildAndExpand(info.getAppName()) + .toUri(); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.ACCEPT_ENCODING, "gzip"); headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); - ResponseEntity response = restTemplate.exchange(urlPath, HttpMethod.POST, new HttpEntity<>(info, headers), + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.POST, new HttpEntity<>(info, headers), Void.class); return anEurekaHttpResponse(response.getStatusCode().value()).headers(headersOf(response)).build(); @@ -82,9 +86,10 @@ public EurekaHttpResponse register(InstanceInfo info) { @Override public EurekaHttpResponse cancel(String appName, String id) { - String urlPath = serviceUrl + "apps/" + appName + '/' + id; + URI uri = UriComponentsBuilder.fromHttpUrl(serviceUrl).path("apps/{appName}/{id}").buildAndExpand(appName, id) + .toUri(); - ResponseEntity response = restTemplate.exchange(urlPath, HttpMethod.DELETE, null, Void.class); + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.DELETE, null, Void.class); return anEurekaHttpResponse(response.getStatusCode().value()).headers(headersOf(response)).build(); } @@ -92,12 +97,17 @@ public EurekaHttpResponse cancel(String appName, String id) { @Override public EurekaHttpResponse sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) { - String urlPath = serviceUrl + "apps/" + appName + '/' + id + "?status=" + info.getStatus().toString() - + "&lastDirtyTimestamp=" + info.getLastDirtyTimestamp().toString() - + (overriddenStatus != null ? "&overriddenstatus=" + overriddenStatus.name() : ""); + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(serviceUrl).path("apps/{appName}/{id}") + .queryParam("status", info.getStatus().toString()) + .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString()); - ResponseEntity response = restTemplate.exchange(urlPath, HttpMethod.PUT, null, - InstanceInfo.class); + if (overriddenStatus != null) { + uriBuilder = uriBuilder.queryParam("overriddenstatus", overriddenStatus.name()); + } + + URI uri = uriBuilder.buildAndExpand(appName, id).toUri(); + + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.PUT, null, InstanceInfo.class); EurekaHttpResponseBuilder eurekaResponseBuilder = anEurekaHttpResponse( response.getStatusCode().value(), InstanceInfo.class).headers(headersOf(response)); @@ -112,20 +122,23 @@ public EurekaHttpResponse sendHeartBeat(String appName, String id, @Override public EurekaHttpResponse statusUpdate(String appName, String id, InstanceStatus newStatus, InstanceInfo info) { - String urlPath = serviceUrl + "apps/" + appName + '/' + id + "/status?value=" + newStatus.name() - + "&lastDirtyTimestamp=" + info.getLastDirtyTimestamp().toString(); + URI uri = UriComponentsBuilder.fromHttpUrl(serviceUrl).path("apps/{appName}/{id}/status") + .queryParam("value", newStatus.name()) + .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString()).buildAndExpand(appName, id) + .toUri(); - ResponseEntity response = restTemplate.exchange(urlPath, HttpMethod.PUT, null, Void.class); + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.PUT, null, Void.class); return anEurekaHttpResponse(response.getStatusCode().value()).headers(headersOf(response)).build(); } @Override public EurekaHttpResponse deleteStatusOverride(String appName, String id, InstanceInfo info) { - String urlPath = serviceUrl + "apps/" + appName + '/' + id + "/status?lastDirtyTimestamp=" - + info.getLastDirtyTimestamp().toString(); + URI uri = UriComponentsBuilder.fromHttpUrl(serviceUrl).path("apps/{appName}/{id}/status") + .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString()).buildAndExpand(appName, id) + .toUri(); - ResponseEntity response = restTemplate.exchange(urlPath, HttpMethod.DELETE, null, Void.class); + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.DELETE, null, Void.class); return anEurekaHttpResponse(response.getStatusCode().value()).headers(headersOf(response)).build(); } @@ -136,13 +149,15 @@ public EurekaHttpResponse getApplications(String... regions) { } private EurekaHttpResponse getApplicationsInternal(String urlPath, String[] regions) { - String url = serviceUrl + urlPath; + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(serviceUrl).path(urlPath); if (regions != null && regions.length > 0) { - url = url + (urlPath.contains("?") ? "&" : "?") + "regions=" + StringUtil.join(regions); + uriBuilder = uriBuilder.queryParam("regions", StringUtil.join(regions)); } - ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, null, + URI uri = uriBuilder.build().toUri(); + + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, null, EurekaApplications.class); return anEurekaHttpResponse(response.getStatusCode().value(), @@ -167,9 +182,9 @@ public EurekaHttpResponse getSecureVip(String secureVipAddress, St @Override public EurekaHttpResponse getApplication(String appName) { - String urlPath = serviceUrl + "apps/" + appName; + URI uri = UriComponentsBuilder.fromHttpUrl(serviceUrl).path("apps/{appName}").buildAndExpand(appName).toUri(); - ResponseEntity response = restTemplate.exchange(urlPath, HttpMethod.GET, null, Application.class); + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, null, Application.class); Application application = response.getStatusCode().value() == HttpStatus.OK.value() && response.hasBody() ? response.getBody() : null; @@ -179,19 +194,18 @@ public EurekaHttpResponse getApplication(String appName) { @Override public EurekaHttpResponse getInstance(String appName, String id) { - return getInstanceInternal("apps/" + appName + '/' + id); + return getInstanceInternal("apps", appName, id); } @Override public EurekaHttpResponse getInstance(String id) { - return getInstanceInternal("instances/" + id); + return getInstanceInternal("instances", id); } - private EurekaHttpResponse getInstanceInternal(String urlPath) { - urlPath = serviceUrl + urlPath; + private EurekaHttpResponse getInstanceInternal(String... pathSegments) { + URI uri = UriComponentsBuilder.fromHttpUrl(serviceUrl).pathSegment(pathSegments).build().toUri(); - ResponseEntity response = restTemplate.exchange(urlPath, HttpMethod.GET, null, - InstanceInfo.class); + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, null, InstanceInfo.class); return anEurekaHttpResponse(response.getStatusCode().value(), response.getStatusCode().value() == HttpStatus.OK.value() && response.hasBody() ? response.getBody() diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/WebClientEurekaHttpClient.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/WebClientEurekaHttpClient.java index 5788c99f5..9684119a0 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/WebClientEurekaHttpClient.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/WebClientEurekaHttpClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-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. @@ -17,6 +17,7 @@ package org.springframework.cloud.netflix.eureka.http; import java.util.Map; +import java.util.Optional; import com.netflix.appinfo.InstanceInfo; import com.netflix.appinfo.InstanceInfo.InstanceStatus; @@ -42,6 +43,7 @@ /** * @author Daniel Lavoie * @author Haytham Mohamed + * @author Václav Plic */ public class WebClientEurekaHttpClient implements EurekaHttpClient { @@ -53,8 +55,8 @@ public WebClientEurekaHttpClient(WebClient webClient) { @Override public EurekaHttpResponse register(InstanceInfo info) { - return webClient.post().uri("apps/" + info.getAppName()).body(BodyInserters.fromValue(info)) - .header(HttpHeaders.ACCEPT_ENCODING, "gzip") + return webClient.post().uri(uriBuilder -> uriBuilder.path("apps/{appName}").build(info.getAppName())) + .body(BodyInserters.fromValue(info)).header(HttpHeaders.ACCEPT_ENCODING, "gzip") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).retrieve() .onStatus(HttpStatusCode::isError, this::ignoreError).toBodilessEntity().map(this::eurekaHttpResponse) .block(); @@ -62,19 +64,19 @@ public EurekaHttpResponse register(InstanceInfo info) { @Override public EurekaHttpResponse cancel(String appName, String id) { - return webClient.delete().uri("apps/" + appName + '/' + id).retrieve() - .onStatus(HttpStatusCode::isError, this::ignoreError).toBodilessEntity().map(this::eurekaHttpResponse) - .block(); + return webClient.delete().uri(uriBuilder -> uriBuilder.path("apps/{appName}/{id}").build(appName, id)) + .retrieve().onStatus(HttpStatusCode::isError, this::ignoreError).toBodilessEntity() + .map(this::eurekaHttpResponse).block(); } @Override public EurekaHttpResponse sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) { - String urlPath = "apps/" + appName + '/' + id + "?status=" + info.getStatus().toString() - + "&lastDirtyTimestamp=" + info.getLastDirtyTimestamp().toString() - + (overriddenStatus != null ? "&overriddenstatus=" + overriddenStatus.name() : ""); - ResponseEntity response = webClient.put().uri(urlPath) + ResponseEntity response = webClient.put() + .uri(uriBuilder -> uriBuilder.path("apps/{appName}/{id}") + .queryParam("status", info.getStatus().toString()) + .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString()).build(appName, id)) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE).retrieve() .onStatus(HttpStatusCode::isError, this::ignoreError).toEntity(InstanceInfo.class).block(); @@ -95,22 +97,22 @@ public EurekaHttpResponse sendHeartBeat(String appName, String id, @Override public EurekaHttpResponse statusUpdate(String appName, String id, InstanceStatus newStatus, InstanceInfo info) { - String urlPath = "apps/" + appName + '/' + id + "/status?value=" + newStatus.name() + "&lastDirtyTimestamp=" - + info.getLastDirtyTimestamp().toString(); - - return webClient.put().uri(urlPath).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .retrieve().onStatus(HttpStatusCode::isError, this::ignoreError).toBodilessEntity() - .map(this::eurekaHttpResponse).block(); + return webClient.put() + .uri(uriBuilder -> uriBuilder.path("apps/{appName}/{id}/status").queryParam("value", newStatus.name()) + .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString()).build(appName, id)) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).retrieve() + .onStatus(HttpStatusCode::isError, this::ignoreError).toBodilessEntity().map(this::eurekaHttpResponse) + .block(); } @Override public EurekaHttpResponse deleteStatusOverride(String appName, String id, InstanceInfo info) { - String urlPath = "apps/" + appName + '/' + id + "/status?lastDirtyTimestamp=" - + info.getLastDirtyTimestamp().toString(); - - return webClient.delete().uri(urlPath).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .retrieve().onStatus(HttpStatusCode::isError, this::ignoreError).toBodilessEntity() - .map(this::eurekaHttpResponse).block(); + return webClient.delete() + .uri(uriBuilder -> uriBuilder.path("apps/{appName}/{id}/status") + .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString()).build(appName, id)) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).retrieve() + .onStatus(HttpStatusCode::isError, this::ignoreError).toBodilessEntity().map(this::eurekaHttpResponse) + .block(); } @Override @@ -119,13 +121,11 @@ public EurekaHttpResponse getApplications(String... regions) { } private EurekaHttpResponse getApplicationsInternal(String urlPath, String[] regions) { - String url = urlPath; - - if (regions != null && regions.length > 0) { - url = url + (urlPath.contains("?") ? "&" : "?") + "regions=" + StringUtil.join(regions); - } + Optional regionsParam = (regions != null && regions.length > 0) ? Optional.of(StringUtil.join(regions)) + : Optional.empty(); - ResponseEntity response = webClient.get().uri(url) + ResponseEntity response = webClient.get() + .uri(uriBuilder -> uriBuilder.path(urlPath).queryParamIfPresent("regions", regionsParam).build()) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE).retrieve() .onStatus(HttpStatusCode::isError, this::ignoreError).toEntity(Applications.class).block(); @@ -156,7 +156,8 @@ public EurekaHttpResponse getSecureVip(String secureVipAddress, St @Override public EurekaHttpResponse getApplication(String appName) { - ResponseEntity response = webClient.get().uri("apps/" + appName) + ResponseEntity response = webClient.get() + .uri(uriBuilder -> uriBuilder.path("apps/{appName}").build(appName)) .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE).retrieve() .onStatus(HttpStatusCode::isError, this::ignoreError).toEntity(Application.class).block(); @@ -170,16 +171,17 @@ public EurekaHttpResponse getApplication(String appName) { @Override public EurekaHttpResponse getInstance(String appName, String id) { - return getInstanceInternal("apps/" + appName + '/' + id); + return getInstanceInternal("apps", appName, id); } @Override public EurekaHttpResponse getInstance(String id) { - return getInstanceInternal("instances/" + id); + return getInstanceInternal("instances", id); } - private EurekaHttpResponse getInstanceInternal(String urlPath) { - ResponseEntity response = webClient.get().uri(urlPath) + private EurekaHttpResponse getInstanceInternal(String... pathSegments) { + ResponseEntity response = webClient.get() + .uri(uriBuilder -> uriBuilder.pathSegment(pathSegments).build()) .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE).retrieve() .onStatus(HttpStatusCode::isError, this::ignoreError).toEntity(InstanceInfo.class).block(); diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/AbstractEurekaHttpClientTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/AbstractEurekaHttpClientTests.java index 104331c15..27463880d 100644 --- a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/AbstractEurekaHttpClientTests.java +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/AbstractEurekaHttpClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-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. @@ -21,6 +21,8 @@ import com.netflix.discovery.shared.transport.EurekaHttpClient; import com.netflix.discovery.shared.transport.EurekaHttpResponse; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.http.HttpStatus; @@ -28,6 +30,7 @@ /** * @author Haytham Mohamed + * @author Václav Plic **/ abstract class AbstractEurekaHttpClientTests { @@ -47,9 +50,10 @@ void testCancel() { assertThat(eurekaHttpClient.cancel("test", "test").getStatusCode()).isEqualTo(HttpStatus.OK.value()); } - @Test - void testSendHeartBeat() { - EurekaHttpResponse response = eurekaHttpClient.sendHeartBeat("test", "test", info, null); + @ParameterizedTest + @ValueSource(strings = { "test", "test#1.[3.?]!" }) + void testSendHeartBeat(String instanceId) { + EurekaHttpResponse response = eurekaHttpClient.sendHeartBeat("test", instanceId, info, null); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK.value()); assertThat(response.getEntity()).isNotNull(); } @@ -66,10 +70,12 @@ void testSendHeartBeatFourOFourWithBody() { .isEqualTo(HttpStatus.NOT_FOUND.value()); } - @Test - void testStatusUpdate() { - assertThat(eurekaHttpClient.statusUpdate("test", "test", InstanceInfo.InstanceStatus.UP, info).getStatusCode()) - .isEqualTo(HttpStatus.OK.value()); + @ParameterizedTest + @ValueSource(strings = { "test", "test#1.[3.?]!" }) + void testStatusUpdate(String instanceId) { + assertThat( + eurekaHttpClient.statusUpdate("test", instanceId, InstanceInfo.InstanceStatus.UP, info).getStatusCode()) + .isEqualTo(HttpStatus.OK.value()); } @Test @@ -108,10 +114,11 @@ void testGetApplication() { eurekaHttpClient.getApplication("test"); } - @Test - void testGetInstance() { - eurekaHttpClient.getInstance("test"); - eurekaHttpClient.getInstance("test", "test"); + @ParameterizedTest + @ValueSource(strings = { "test", "test#1.[3.?]!" }) + void testGetInstance(String instanceId) { + eurekaHttpClient.getInstance(instanceId); + eurekaHttpClient.getInstance("test", instanceId); } }