Browse Source

Add defaultApiVersion to RestClient and WebClient

Closes gh-34857
pull/34864/head
rstoyanchev 11 months ago
parent
commit
22e7f24731
  1. 21
      spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java
  2. 12
      spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java
  3. 9
      spring-web/src/main/java/org/springframework/web/client/RestClient.java
  4. 11
      spring-web/src/test/java/org/springframework/web/client/RestClientVersionTests.java
  5. 19
      spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java
  6. 11
      spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java
  7. 9
      spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java
  8. 115
      spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientVersionTests.java

21
spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java

@ -108,6 +108,8 @@ final class DefaultRestClient implements RestClient {
private final @Nullable MultiValueMap<String, String> defaultCookies; private final @Nullable MultiValueMap<String, String> defaultCookies;
private final @Nullable Object defaultApiVersion;
private final @Nullable ApiVersionInserter apiVersionInserter; private final @Nullable ApiVersionInserter apiVersionInserter;
private final @Nullable Consumer<RequestHeadersSpec<?>> defaultRequest; private final @Nullable Consumer<RequestHeadersSpec<?>> defaultRequest;
@ -130,7 +132,7 @@ final class DefaultRestClient implements RestClient {
UriBuilderFactory uriBuilderFactory, UriBuilderFactory uriBuilderFactory,
@Nullable HttpHeaders defaultHeaders, @Nullable HttpHeaders defaultHeaders,
@Nullable MultiValueMap<String, String> defaultCookies, @Nullable MultiValueMap<String, String> defaultCookies,
@Nullable ApiVersionInserter apiVersionInserter, @Nullable Object defaultApiVersion, @Nullable ApiVersionInserter apiVersionInserter,
@Nullable Consumer<RequestHeadersSpec<?>> defaultRequest, @Nullable Consumer<RequestHeadersSpec<?>> defaultRequest,
@Nullable List<StatusHandler> statusHandlers, @Nullable List<StatusHandler> statusHandlers,
List<HttpMessageConverter<?>> messageConverters, List<HttpMessageConverter<?>> messageConverters,
@ -145,6 +147,7 @@ final class DefaultRestClient implements RestClient {
this.uriBuilderFactory = uriBuilderFactory; this.uriBuilderFactory = uriBuilderFactory;
this.defaultHeaders = defaultHeaders; this.defaultHeaders = defaultHeaders;
this.defaultCookies = defaultCookies; this.defaultCookies = defaultCookies;
this.defaultApiVersion = defaultApiVersion;
this.apiVersionInserter = apiVersionInserter; this.apiVersionInserter = apiVersionInserter;
this.defaultRequest = defaultRequest; this.defaultRequest = defaultRequest;
this.defaultStatusHandlers = (statusHandlers != null ? new ArrayList<>(statusHandlers) : new ArrayList<>()); this.defaultStatusHandlers = (statusHandlers != null ? new ArrayList<>(statusHandlers) : new ArrayList<>());
@ -609,13 +612,18 @@ final class DefaultRestClient implements RestClient {
private URI initUri() { private URI initUri() {
URI uriToUse = this.uri != null ? this.uri : DefaultRestClient.this.uriBuilderFactory.expand(""); URI uriToUse = this.uri != null ? this.uri : DefaultRestClient.this.uriBuilderFactory.expand("");
if (this.apiVersion != null) { Object version = getApiVersionOrDefault();
if (version != null) {
Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured"); Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured");
uriToUse = apiVersionInserter.insertVersion(this.apiVersion, uriToUse); uriToUse = apiVersionInserter.insertVersion(version, uriToUse);
} }
return uriToUse; return uriToUse;
} }
private @Nullable Object getApiVersionOrDefault() {
return (this.apiVersion != null ? this.apiVersion : DefaultRestClient.this.defaultApiVersion);
}
private @Nullable String serializeCookies() { private @Nullable String serializeCookies() {
MultiValueMap<String, String> map; MultiValueMap<String, String> map;
MultiValueMap<String, String> defaultCookies = DefaultRestClient.this.defaultCookies; MultiValueMap<String, String> defaultCookies = DefaultRestClient.this.defaultCookies;
@ -652,7 +660,8 @@ final class DefaultRestClient implements RestClient {
private @Nullable HttpHeaders initHeaders() { private @Nullable HttpHeaders initHeaders() {
HttpHeaders defaultHeaders = DefaultRestClient.this.defaultHeaders; HttpHeaders defaultHeaders = DefaultRestClient.this.defaultHeaders;
if (this.apiVersion == null) { Object version = getApiVersionOrDefault();
if (version == null) {
if (this.headers == null || this.headers.isEmpty()) { if (this.headers == null || this.headers.isEmpty()) {
return defaultHeaders; return defaultHeaders;
} }
@ -669,9 +678,9 @@ final class DefaultRestClient implements RestClient {
result.putAll(this.headers); result.putAll(this.headers);
} }
if (this.apiVersion != null) { if (version != null) {
Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured"); Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured");
apiVersionInserter.insertVersion(this.apiVersion, result); apiVersionInserter.insertVersion(version, result);
} }
return result; return result;

12
spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java

@ -116,7 +116,6 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
private static final boolean kotlinSerializationProtobufPresent; private static final boolean kotlinSerializationProtobufPresent;
static { static {
ClassLoader loader = DefaultRestClientBuilder.class.getClassLoader(); ClassLoader loader = DefaultRestClientBuilder.class.getClassLoader();
@ -150,6 +149,8 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
private @Nullable MultiValueMap<String, String> defaultCookies; private @Nullable MultiValueMap<String, String> defaultCookies;
private @Nullable Object defaultApiVersion;
private @Nullable ApiVersionInserter apiVersionInserter; private @Nullable ApiVersionInserter apiVersionInserter;
private @Nullable Consumer<RestClient.RequestHeadersSpec<?>> defaultRequest; private @Nullable Consumer<RestClient.RequestHeadersSpec<?>> defaultRequest;
@ -188,6 +189,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
this.defaultHeaders = null; this.defaultHeaders = null;
} }
this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null); this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null);
this.defaultApiVersion = other.defaultApiVersion;
this.apiVersionInserter = other.apiVersionInserter; this.apiVersionInserter = other.apiVersionInserter;
this.defaultRequest = other.defaultRequest; this.defaultRequest = other.defaultRequest;
this.statusHandlers = (other.statusHandlers != null ? new ArrayList<>(other.statusHandlers) : null); this.statusHandlers = (other.statusHandlers != null ? new ArrayList<>(other.statusHandlers) : null);
@ -324,6 +326,12 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
return this.defaultCookies; return this.defaultCookies;
} }
@Override
public RestClient.Builder defaultApiVersion(@Nullable Object version) {
this.defaultApiVersion = version;
return this;
}
@Override @Override
public RestClient.Builder apiVersionInserter(ApiVersionInserter apiVersionInserter) { public RestClient.Builder apiVersionInserter(ApiVersionInserter apiVersionInserter) {
this.apiVersionInserter = apiVersionInserter; this.apiVersionInserter = apiVersionInserter;
@ -521,7 +529,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
return new DefaultRestClient( return new DefaultRestClient(
requestFactory, this.interceptors, this.bufferingPredicate, this.initializers, requestFactory, this.interceptors, this.bufferingPredicate, this.initializers,
uriBuilderFactory, defaultHeaders, defaultCookies, uriBuilderFactory, defaultHeaders, defaultCookies, this.defaultApiVersion,
this.apiVersionInserter, this.defaultRequest, this.apiVersionInserter, this.defaultRequest,
this.statusHandlers, converters, this.statusHandlers, converters,
this.observationRegistry, this.observationConvention, this.observationRegistry, this.observationConvention,

9
spring-web/src/main/java/org/springframework/web/client/RestClient.java

@ -332,6 +332,15 @@ public interface RestClient {
*/ */
Builder defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer); Builder defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
/**
* Global option to specify an API version to be added to every request,
* if not explicitly set.
* @param version the version to use
* @return this builder
* @since 7.0
*/
Builder defaultApiVersion(Object version);
/** /**
* Configure an {@link ApiVersionInserter} to abstract how an API version * Configure an {@link ApiVersionInserter} to abstract how an API version
* specified via {@link RequestHeadersSpec#apiVersion(Object)} * specified via {@link RequestHeadersSpec#apiVersion(Object)}

11
spring-web/src/test/java/org/springframework/web/client/RestClientVersionTests.java

@ -86,7 +86,16 @@ public class RestClientVersionTests {
assertThatIllegalStateException() assertThatIllegalStateException()
.isThrownBy(() -> performRequest(DefaultApiVersionInserter.fromPathSegment(2))) .isThrownBy(() -> performRequest(DefaultApiVersionInserter.fromPathSegment(2)))
.withMessage("Cannot insert version into '/path' at path segment index 2"); .withMessage("Cannot insert version into '/path' at path segment index 2");
} }
@Test
void defaultVersion() {
ApiVersionInserter inserter = DefaultApiVersionInserter.fromHeader("X-API-Version").build();
RestClient restClient = restClientBuilder.defaultApiVersion(1.2).apiVersionInserter(inserter).build();
restClient.get().uri("/path").retrieve().body(String.class);
expectRequest(request -> assertThat(request.getHeader("X-API-Version")).isEqualTo("1.2"));
}
private void performRequest(DefaultApiVersionInserter.Builder builder) { private void performRequest(DefaultApiVersionInserter.Builder builder) {
ApiVersionInserter versionInserter = builder.build(); ApiVersionInserter versionInserter = builder.build();

19
spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java

@ -94,6 +94,8 @@ final class DefaultWebClient implements WebClient {
private final @Nullable MultiValueMap<String, String> defaultCookies; private final @Nullable MultiValueMap<String, String> defaultCookies;
private final @Nullable Object defaultApiVersion;
private final @Nullable ApiVersionInserter apiVersionInserter; private final @Nullable ApiVersionInserter apiVersionInserter;
private final @Nullable Consumer<RequestHeadersSpec<?>> defaultRequest; private final @Nullable Consumer<RequestHeadersSpec<?>> defaultRequest;
@ -110,7 +112,7 @@ final class DefaultWebClient implements WebClient {
DefaultWebClient(ExchangeFunction exchangeFunction, @Nullable ExchangeFilterFunction filterFunctions, DefaultWebClient(ExchangeFunction exchangeFunction, @Nullable ExchangeFilterFunction filterFunctions,
UriBuilderFactory uriBuilderFactory, @Nullable HttpHeaders defaultHeaders, UriBuilderFactory uriBuilderFactory, @Nullable HttpHeaders defaultHeaders,
@Nullable MultiValueMap<String, String> defaultCookies, @Nullable MultiValueMap<String, String> defaultCookies,
@Nullable ApiVersionInserter apiVersionInserter, @Nullable Object defaultApiVersion, @Nullable ApiVersionInserter apiVersionInserter,
@Nullable Consumer<RequestHeadersSpec<?>> defaultRequest, @Nullable Consumer<RequestHeadersSpec<?>> defaultRequest,
@Nullable Map<Predicate<HttpStatusCode>, Function<ClientResponse, Mono<? extends Throwable>>> statusHandlerMap, @Nullable Map<Predicate<HttpStatusCode>, Function<ClientResponse, Mono<? extends Throwable>>> statusHandlerMap,
ObservationRegistry observationRegistry, @Nullable ClientRequestObservationConvention observationConvention, ObservationRegistry observationRegistry, @Nullable ClientRequestObservationConvention observationConvention,
@ -121,6 +123,7 @@ final class DefaultWebClient implements WebClient {
this.uriBuilderFactory = uriBuilderFactory; this.uriBuilderFactory = uriBuilderFactory;
this.defaultHeaders = defaultHeaders; this.defaultHeaders = defaultHeaders;
this.defaultCookies = defaultCookies; this.defaultCookies = defaultCookies;
this.defaultApiVersion = defaultApiVersion;
this.apiVersionInserter = apiVersionInserter; this.apiVersionInserter = apiVersionInserter;
this.defaultRequest = defaultRequest; this.defaultRequest = defaultRequest;
this.defaultStatusHandlers = initStatusHandlers(statusHandlerMap); this.defaultStatusHandlers = initStatusHandlers(statusHandlerMap);
@ -491,13 +494,18 @@ final class DefaultWebClient implements WebClient {
private URI initUri() { private URI initUri() {
URI uriToUse = (this.uri != null ? this.uri : uriBuilderFactory.expand("")); URI uriToUse = (this.uri != null ? this.uri : uriBuilderFactory.expand(""));
if (this.apiVersion != null) { Object version = getApiVersionOrDefault();
if (version != null) {
Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured"); Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured");
uriToUse = apiVersionInserter.insertVersion(this.apiVersion, uriToUse); uriToUse = apiVersionInserter.insertVersion(version, uriToUse);
} }
return uriToUse; return uriToUse;
} }
private @Nullable Object getApiVersionOrDefault() {
return (this.apiVersion != null ? this.apiVersion : DefaultWebClient.this.defaultApiVersion);
}
private void initHeaders(HttpHeaders out) { private void initHeaders(HttpHeaders out) {
if (defaultHeaders != null && !defaultHeaders.isEmpty()) { if (defaultHeaders != null && !defaultHeaders.isEmpty()) {
out.putAll(defaultHeaders); out.putAll(defaultHeaders);
@ -505,9 +513,10 @@ final class DefaultWebClient implements WebClient {
if (this.headers != null && !this.headers.isEmpty()) { if (this.headers != null && !this.headers.isEmpty()) {
out.putAll(this.headers); out.putAll(this.headers);
} }
if (this.apiVersion != null) { Object version = getApiVersionOrDefault();
if (version != null) {
Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured"); Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured");
apiVersionInserter.insertVersion(this.apiVersion, out); apiVersionInserter.insertVersion(version, out);
} }
} }

11
spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java

@ -81,6 +81,8 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
private @Nullable MultiValueMap<String, String> defaultCookies; private @Nullable MultiValueMap<String, String> defaultCookies;
private @Nullable Object defaultApiVersion;
private @Nullable ApiVersionInserter apiVersionInserter; private @Nullable ApiVersionInserter apiVersionInserter;
private @Nullable Consumer<WebClient.RequestHeadersSpec<?>> defaultRequest; private @Nullable Consumer<WebClient.RequestHeadersSpec<?>> defaultRequest;
@ -122,6 +124,8 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
} }
this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null); this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null);
this.defaultApiVersion = other.defaultApiVersion;
this.apiVersionInserter = other.apiVersionInserter; this.apiVersionInserter = other.apiVersionInserter;
this.defaultRequest = other.defaultRequest; this.defaultRequest = other.defaultRequest;
@ -194,6 +198,11 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
return this.defaultCookies; return this.defaultCookies;
} }
@Override
public WebClient.Builder defaultApiVersion(Object version) {
this.defaultApiVersion = version;
return this;
}
@Override @Override
public WebClient.Builder apiVersionInserter(ApiVersionInserter apiVersionInserter) { public WebClient.Builder apiVersionInserter(ApiVersionInserter apiVersionInserter) {
@ -308,7 +317,7 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
return new DefaultWebClient( return new DefaultWebClient(
exchange, filterFunctions, exchange, filterFunctions,
initUriBuilderFactory(), defaultHeaders, defaultCookies, initUriBuilderFactory(), defaultHeaders, defaultCookies,
this.apiVersionInserter, this.defaultApiVersion, this.apiVersionInserter,
this.defaultRequest, this.defaultRequest,
this.statusHandlers, this.statusHandlers,
this.observationRegistry, this.observationConvention, this.observationRegistry, this.observationConvention,

9
spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java

@ -252,6 +252,15 @@ public interface WebClient {
*/ */
Builder defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer); Builder defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
/**
* Global option to specify an API version to add to every request,
* if not already set.
* @param version the version to use
* @return this builder
* @since 7.0
*/
Builder defaultApiVersion(Object version);
/** /**
* Configure an {@link ApiVersionInserter} to abstract how an API version * Configure an {@link ApiVersionInserter} to abstract how an API version
* specified via {@link RequestHeadersSpec#apiVersion(Object)} * specified via {@link RequestHeadersSpec#apiVersion(Object)}

115
spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientVersionTests.java

@ -0,0 +1,115 @@
/*
* Copyright 2002-2025 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 org.springframework.web.reactive.function.client;
import java.io.IOException;
import java.util.function.Consumer;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.web.client.ApiVersionInserter;
import org.springframework.web.client.DefaultApiVersionInserter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* {@link WebClient} tests for sending API versions.
* @author Rossen Stoyanchev
*/
public class WebClientVersionTests {
private final MockWebServer server = new MockWebServer();
private final WebClient.Builder webClientBuilder =
WebClient.builder().baseUrl(this.server.url("/").toString());
@BeforeEach
void setUp() {
MockResponse response = new MockResponse();
response.setHeader("Content-Type", "text/plain").setBody("body");
this.server.enqueue(response);
}
@AfterEach
void shutdown() throws IOException {
this.server.shutdown();
}
@Test
void header() {
performRequest(DefaultApiVersionInserter.fromHeader("X-API-Version"));
expectRequest(request -> assertThat(request.getHeader("X-API-Version")).isEqualTo("1.2"));
}
@Test
void queryParam() {
performRequest(DefaultApiVersionInserter.fromQueryParam("api-version"));
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/path?api-version=1.2"));
}
@Test
void pathSegmentIndexLessThanSize() {
performRequest(DefaultApiVersionInserter.fromPathSegment(0).withVersionFormatter(v -> "v" + v));
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/v1.2/path"));
}
@Test
void pathSegmentIndexEqualToSize() {
performRequest(DefaultApiVersionInserter.fromPathSegment(1).withVersionFormatter(v -> "v" + v));
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/path/v1.2"));
}
@Test
void pathSegmentIndexGreaterThanSize() {
assertThatIllegalStateException()
.isThrownBy(() -> performRequest(DefaultApiVersionInserter.fromPathSegment(2)))
.withMessage("Cannot insert version into '/path' at path segment index 2");
}
@Test
void defaultVersion() {
ApiVersionInserter inserter = DefaultApiVersionInserter.fromHeader("X-API-Version").build();
WebClient webClient = webClientBuilder.defaultApiVersion(1.2).apiVersionInserter(inserter).build();
webClient.get().uri("/path").retrieve().bodyToMono(String.class).block();
expectRequest(request -> assertThat(request.getHeader("X-API-Version")).isEqualTo("1.2"));
}
private void performRequest(DefaultApiVersionInserter.Builder builder) {
ApiVersionInserter versionInserter = builder.build();
WebClient webClient = webClientBuilder.apiVersionInserter(versionInserter).build();
webClient.get().uri("/path").apiVersion(1.2).retrieve().bodyToMono(String.class).block();
}
private void expectRequest(Consumer<RecordedRequest> consumer) {
try {
consumer.accept(this.server.takeRequest());
}
catch (InterruptedException ex) {
throw new IllegalStateException(ex);
}
}
}
Loading…
Cancel
Save