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 { @@ -108,6 +108,8 @@ final class DefaultRestClient implements RestClient {
private final @Nullable MultiValueMap<String, String> defaultCookies;
private final @Nullable Object defaultApiVersion;
private final @Nullable ApiVersionInserter apiVersionInserter;
private final @Nullable Consumer<RequestHeadersSpec<?>> defaultRequest;
@ -130,7 +132,7 @@ final class DefaultRestClient implements RestClient { @@ -130,7 +132,7 @@ final class DefaultRestClient implements RestClient {
UriBuilderFactory uriBuilderFactory,
@Nullable HttpHeaders defaultHeaders,
@Nullable MultiValueMap<String, String> defaultCookies,
@Nullable ApiVersionInserter apiVersionInserter,
@Nullable Object defaultApiVersion, @Nullable ApiVersionInserter apiVersionInserter,
@Nullable Consumer<RequestHeadersSpec<?>> defaultRequest,
@Nullable List<StatusHandler> statusHandlers,
List<HttpMessageConverter<?>> messageConverters,
@ -145,6 +147,7 @@ final class DefaultRestClient implements RestClient { @@ -145,6 +147,7 @@ final class DefaultRestClient implements RestClient {
this.uriBuilderFactory = uriBuilderFactory;
this.defaultHeaders = defaultHeaders;
this.defaultCookies = defaultCookies;
this.defaultApiVersion = defaultApiVersion;
this.apiVersionInserter = apiVersionInserter;
this.defaultRequest = defaultRequest;
this.defaultStatusHandlers = (statusHandlers != null ? new ArrayList<>(statusHandlers) : new ArrayList<>());
@ -609,13 +612,18 @@ final class DefaultRestClient implements RestClient { @@ -609,13 +612,18 @@ final class DefaultRestClient implements RestClient {
private URI initUri() {
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");
uriToUse = apiVersionInserter.insertVersion(this.apiVersion, uriToUse);
uriToUse = apiVersionInserter.insertVersion(version, uriToUse);
}
return uriToUse;
}
private @Nullable Object getApiVersionOrDefault() {
return (this.apiVersion != null ? this.apiVersion : DefaultRestClient.this.defaultApiVersion);
}
private @Nullable String serializeCookies() {
MultiValueMap<String, String> map;
MultiValueMap<String, String> defaultCookies = DefaultRestClient.this.defaultCookies;
@ -652,7 +660,8 @@ final class DefaultRestClient implements RestClient { @@ -652,7 +660,8 @@ final class DefaultRestClient implements RestClient {
private @Nullable HttpHeaders initHeaders() {
HttpHeaders defaultHeaders = DefaultRestClient.this.defaultHeaders;
if (this.apiVersion == null) {
Object version = getApiVersionOrDefault();
if (version == null) {
if (this.headers == null || this.headers.isEmpty()) {
return defaultHeaders;
}
@ -669,9 +678,9 @@ final class DefaultRestClient implements RestClient { @@ -669,9 +678,9 @@ final class DefaultRestClient implements RestClient {
result.putAll(this.headers);
}
if (this.apiVersion != null) {
if (version != null) {
Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured");
apiVersionInserter.insertVersion(this.apiVersion, result);
apiVersionInserter.insertVersion(version, 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 { @@ -116,7 +116,6 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
private static final boolean kotlinSerializationProtobufPresent;
static {
ClassLoader loader = DefaultRestClientBuilder.class.getClassLoader();
@ -150,6 +149,8 @@ final class DefaultRestClientBuilder implements RestClient.Builder { @@ -150,6 +149,8 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
private @Nullable MultiValueMap<String, String> defaultCookies;
private @Nullable Object defaultApiVersion;
private @Nullable ApiVersionInserter apiVersionInserter;
private @Nullable Consumer<RestClient.RequestHeadersSpec<?>> defaultRequest;
@ -188,6 +189,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder { @@ -188,6 +189,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
this.defaultHeaders = null;
}
this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null);
this.defaultApiVersion = other.defaultApiVersion;
this.apiVersionInserter = other.apiVersionInserter;
this.defaultRequest = other.defaultRequest;
this.statusHandlers = (other.statusHandlers != null ? new ArrayList<>(other.statusHandlers) : null);
@ -324,6 +326,12 @@ final class DefaultRestClientBuilder implements RestClient.Builder { @@ -324,6 +326,12 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
return this.defaultCookies;
}
@Override
public RestClient.Builder defaultApiVersion(@Nullable Object version) {
this.defaultApiVersion = version;
return this;
}
@Override
public RestClient.Builder apiVersionInserter(ApiVersionInserter apiVersionInserter) {
this.apiVersionInserter = apiVersionInserter;
@ -521,7 +529,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder { @@ -521,7 +529,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
return new DefaultRestClient(
requestFactory, this.interceptors, this.bufferingPredicate, this.initializers,
uriBuilderFactory, defaultHeaders, defaultCookies,
uriBuilderFactory, defaultHeaders, defaultCookies, this.defaultApiVersion,
this.apiVersionInserter, this.defaultRequest,
this.statusHandlers, converters,
this.observationRegistry, this.observationConvention,

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

@ -332,6 +332,15 @@ public interface RestClient { @@ -332,6 +332,15 @@ public interface RestClient {
*/
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
* 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 { @@ -86,7 +86,16 @@ public class RestClientVersionTests {
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();
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) {
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 { @@ -94,6 +94,8 @@ final class DefaultWebClient implements WebClient {
private final @Nullable MultiValueMap<String, String> defaultCookies;
private final @Nullable Object defaultApiVersion;
private final @Nullable ApiVersionInserter apiVersionInserter;
private final @Nullable Consumer<RequestHeadersSpec<?>> defaultRequest;
@ -110,7 +112,7 @@ final class DefaultWebClient implements WebClient { @@ -110,7 +112,7 @@ final class DefaultWebClient implements WebClient {
DefaultWebClient(ExchangeFunction exchangeFunction, @Nullable ExchangeFilterFunction filterFunctions,
UriBuilderFactory uriBuilderFactory, @Nullable HttpHeaders defaultHeaders,
@Nullable MultiValueMap<String, String> defaultCookies,
@Nullable ApiVersionInserter apiVersionInserter,
@Nullable Object defaultApiVersion, @Nullable ApiVersionInserter apiVersionInserter,
@Nullable Consumer<RequestHeadersSpec<?>> defaultRequest,
@Nullable Map<Predicate<HttpStatusCode>, Function<ClientResponse, Mono<? extends Throwable>>> statusHandlerMap,
ObservationRegistry observationRegistry, @Nullable ClientRequestObservationConvention observationConvention,
@ -121,6 +123,7 @@ final class DefaultWebClient implements WebClient { @@ -121,6 +123,7 @@ final class DefaultWebClient implements WebClient {
this.uriBuilderFactory = uriBuilderFactory;
this.defaultHeaders = defaultHeaders;
this.defaultCookies = defaultCookies;
this.defaultApiVersion = defaultApiVersion;
this.apiVersionInserter = apiVersionInserter;
this.defaultRequest = defaultRequest;
this.defaultStatusHandlers = initStatusHandlers(statusHandlerMap);
@ -491,13 +494,18 @@ final class DefaultWebClient implements WebClient { @@ -491,13 +494,18 @@ final class DefaultWebClient implements WebClient {
private URI initUri() {
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");
uriToUse = apiVersionInserter.insertVersion(this.apiVersion, uriToUse);
uriToUse = apiVersionInserter.insertVersion(version, uriToUse);
}
return uriToUse;
}
private @Nullable Object getApiVersionOrDefault() {
return (this.apiVersion != null ? this.apiVersion : DefaultWebClient.this.defaultApiVersion);
}
private void initHeaders(HttpHeaders out) {
if (defaultHeaders != null && !defaultHeaders.isEmpty()) {
out.putAll(defaultHeaders);
@ -505,9 +513,10 @@ final class DefaultWebClient implements WebClient { @@ -505,9 +513,10 @@ final class DefaultWebClient implements WebClient {
if (this.headers != null && !this.headers.isEmpty()) {
out.putAll(this.headers);
}
if (this.apiVersion != null) {
Object version = getApiVersionOrDefault();
if (version != null) {
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 { @@ -81,6 +81,8 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
private @Nullable MultiValueMap<String, String> defaultCookies;
private @Nullable Object defaultApiVersion;
private @Nullable ApiVersionInserter apiVersionInserter;
private @Nullable Consumer<WebClient.RequestHeadersSpec<?>> defaultRequest;
@ -122,6 +124,8 @@ final class DefaultWebClientBuilder implements WebClient.Builder { @@ -122,6 +124,8 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
}
this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null);
this.defaultApiVersion = other.defaultApiVersion;
this.apiVersionInserter = other.apiVersionInserter;
this.defaultRequest = other.defaultRequest;
@ -194,6 +198,11 @@ final class DefaultWebClientBuilder implements WebClient.Builder { @@ -194,6 +198,11 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
return this.defaultCookies;
}
@Override
public WebClient.Builder defaultApiVersion(Object version) {
this.defaultApiVersion = version;
return this;
}
@Override
public WebClient.Builder apiVersionInserter(ApiVersionInserter apiVersionInserter) {
@ -308,7 +317,7 @@ final class DefaultWebClientBuilder implements WebClient.Builder { @@ -308,7 +317,7 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
return new DefaultWebClient(
exchange, filterFunctions,
initUriBuilderFactory(), defaultHeaders, defaultCookies,
this.apiVersionInserter,
this.defaultApiVersion, this.apiVersionInserter,
this.defaultRequest,
this.statusHandlers,
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 { @@ -252,6 +252,15 @@ public interface WebClient {
*/
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
* specified via {@link RequestHeadersSpec#apiVersion(Object)}

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

@ -0,0 +1,115 @@ @@ -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