From 6117ec4a613077cfc15f590a97ba2e3d12f23444 Mon Sep 17 00:00:00 2001 From: fru1tworld Date: Thu, 9 Apr 2026 08:44:59 +0900 Subject: [PATCH] Add URI template support to ProxyExchange for observability Signed-off-by: fru1tworld --- .../cloud/gateway/mvc/ProxyExchange.java | 29 +++++++++++++++++++ .../mvc/ProductionConfigurationTests.java | 20 +++++++++++++ 2 files changed, 49 insertions(+) diff --git a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/ProxyExchange.java b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/ProxyExchange.java index afeb12404f..c4de59b98b 100644 --- a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/ProxyExchange.java +++ b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/ProxyExchange.java @@ -32,6 +32,8 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.Vector; import java.util.function.Function; @@ -50,6 +52,7 @@ import org.springframework.core.Conventions; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.RequestEntity; import org.springframework.http.RequestEntity.BodyBuilder; @@ -147,6 +150,10 @@ public class ProxyExchange { private URI uri; + private String uriTemplate; + + private Map uriVariables; + private RestTemplate rest; private Object body; @@ -247,6 +254,8 @@ public ProxyExchange excluded(String... names) { */ public ProxyExchange uri(URI uri) { this.uri = uri; + this.uriTemplate = null; + this.uriVariables = null; return this; } @@ -264,6 +273,21 @@ public ProxyExchange uri(String uri) { } } + /** + * Sets the uri for the backend call using a URI template with variables. When a + * template is provided, the downstream {@link RestTemplate} call preserves the + * template pattern for observability (e.g. Micrometer URI tags). + * @param uriTemplate the URI template (e.g. {@code "http://service/foos/{id}"}) + * @param uriVariables the variables to expand in the template + * @return this for convenience + */ + public ProxyExchange uri(String uriTemplate, Map uriVariables) { + this.uriTemplate = uriTemplate; + this.uriVariables = uriVariables; + this.uri = rest.getUriTemplateHandler().expand(uriTemplate, uriVariables); + return this; + } + public String path() { return (String) this.webRequest.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, WebRequest.SCOPE_REQUEST); @@ -357,6 +381,11 @@ private ResponseEntity exchange(RequestEntity requestEntity) { if (type instanceof TypeVariable || type instanceof WildcardType) { type = Object.class; } + if (this.uriTemplate != null && this.uriVariables != null) { + return rest.exchange(this.uriTemplate, Objects.requireNonNull(requestEntity.getMethod()), + new HttpEntity<>(requestEntity.getBody(), requestEntity.getHeaders()), + ParameterizedTypeReference.forType(type), this.uriVariables); + } return rest.exchange(requestEntity, ParameterizedTypeReference.forType(type)); } diff --git a/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java b/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java index f3668521ae..17eae7fd67 100644 --- a/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java +++ b/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java @@ -23,6 +23,7 @@ import java.util.Map; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.micrometer.core.instrument.MeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -72,6 +73,9 @@ public class ProductionConfigurationTests { @Autowired private TestApplication application; + @Autowired + private MeterRegistry meterRegistry; + @LocalServerPort private int port; @@ -86,6 +90,17 @@ public void get() { assertThat(rest.getForObject("/proxy/0", Foo.class).getName()).isEqualTo("bye"); } + @Test + public void getWithUriTemplate() { + assertThat(rest.getForObject("/proxy/template/0", Foo.class).getName()).isEqualTo("bye"); + } + + @Test + public void getWithUriTemplatePreservesUriTagForObservability() { + rest.getForObject("/proxy/template/0", Foo.class); + assertThat(meterRegistry.find("http.client.requests").tag("uri", "/foos/{id}").timer()).isNotNull(); + } + @Test public void path() { assertThat(rest.getForObject("/proxy/path/1", Foo.class).getName()).isEqualTo("foo"); @@ -363,6 +378,11 @@ public ResponseEntity proxyFoos(@PathVariable Integer id, ProxyExchange pr return proxy.uri(home.toString() + "/foos/" + id).get(); } + @GetMapping("/proxy/template/{id}") + public ResponseEntity proxyWithTemplate(@PathVariable Integer id, ProxyExchange proxy) { + return proxy.uri(home.toString() + "/foos/{id}", Map.of("id", String.valueOf(id))).get(); + } + @GetMapping("/proxy/path/**") public ResponseEntity proxyPath(ProxyExchange proxy, UriComponentsBuilder uri) { String path = proxy.path("/proxy/path/");