Client support for API versioning

Closes gh-34567
This commit is contained in:
rstoyanchev
2025-04-01 17:02:27 +01:00
parent 483abd96a4
commit 7bf628c827
22 changed files with 662 additions and 42 deletions

View File

@@ -52,6 +52,7 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ApiVersionInserter;
import org.springframework.web.reactive.function.BodyExtractor;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
@@ -93,6 +94,8 @@ final class DefaultWebClient implements WebClient {
private final @Nullable MultiValueMap<String, String> defaultCookies;
private final @Nullable ApiVersionInserter apiVersionInserter;
private final @Nullable Consumer<RequestHeadersSpec<?>> defaultRequest;
private final List<DefaultResponseSpec.StatusHandler> defaultStatusHandlers;
@@ -106,7 +109,9 @@ final class DefaultWebClient implements WebClient {
DefaultWebClient(ExchangeFunction exchangeFunction, @Nullable ExchangeFilterFunction filterFunctions,
UriBuilderFactory uriBuilderFactory, @Nullable HttpHeaders defaultHeaders,
@Nullable MultiValueMap<String, String> defaultCookies, @Nullable Consumer<RequestHeadersSpec<?>> defaultRequest,
@Nullable MultiValueMap<String, String> defaultCookies,
@Nullable ApiVersionInserter apiVersionInserter,
@Nullable Consumer<RequestHeadersSpec<?>> defaultRequest,
@Nullable Map<Predicate<HttpStatusCode>, Function<ClientResponse, Mono<? extends Throwable>>> statusHandlerMap,
ObservationRegistry observationRegistry, @Nullable ClientRequestObservationConvention observationConvention,
DefaultWebClientBuilder builder) {
@@ -116,6 +121,7 @@ final class DefaultWebClient implements WebClient {
this.uriBuilderFactory = uriBuilderFactory;
this.defaultHeaders = defaultHeaders;
this.defaultCookies = defaultCookies;
this.apiVersionInserter = apiVersionInserter;
this.defaultRequest = defaultRequest;
this.defaultStatusHandlers = initStatusHandlers(statusHandlerMap);
this.observationRegistry = observationRegistry;
@@ -205,6 +211,8 @@ final class DefaultWebClient implements WebClient {
private @Nullable MultiValueMap<String, String> cookies;
private @Nullable Object apiVersion;
private @Nullable BodyInserter<?, ? super ClientHttpRequest> inserter;
private final Map<String, Object> attributes = new LinkedHashMap<>(4);
@@ -323,6 +331,12 @@ final class DefaultWebClient implements WebClient {
return this;
}
@Override
public DefaultRequestBodyUriSpec apiVersion(Object version) {
this.apiVersion = version;
return this;
}
@Override
public RequestBodySpec attribute(String name, Object value) {
this.attributes.put(name, value);
@@ -474,7 +488,12 @@ final class DefaultWebClient implements WebClient {
}
private URI initUri() {
return (this.uri != null ? this.uri : uriBuilderFactory.expand(""));
URI uriToUse = (this.uri != null ? this.uri : uriBuilderFactory.expand(""));
if (this.apiVersion != null) {
Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured");
uriToUse = apiVersionInserter.insertVersion(this.apiVersion, uriToUse);
}
return uriToUse;
}
private void initHeaders(HttpHeaders out) {
@@ -484,6 +503,10 @@ final class DefaultWebClient implements WebClient {
if (this.headers != null && !this.headers.isEmpty()) {
out.putAll(this.headers);
}
if (this.apiVersion != null) {
Assert.state(apiVersionInserter != null, "No ApiVersionInserter configured");
apiVersionInserter.insertVersion(this.apiVersion, out);
}
}
private void initCookies(MultiValueMap<String, String> out) {

View File

@@ -42,6 +42,7 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ApiVersionInserter;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriBuilderFactory;
@@ -80,6 +81,8 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
private @Nullable MultiValueMap<String, String> defaultCookies;
private @Nullable ApiVersionInserter apiVersionInserter;
private @Nullable Consumer<WebClient.RequestHeadersSpec<?>> defaultRequest;
private @Nullable Map<Predicate<HttpStatusCode>, Function<ClientResponse, Mono<? extends Throwable>>> statusHandlers;
@@ -118,8 +121,9 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
this.defaultHeaders = null;
}
this.defaultCookies = (other.defaultCookies != null ?
new LinkedMultiValueMap<>(other.defaultCookies) : null);
this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null);
this.apiVersionInserter = other.apiVersionInserter;
this.defaultRequest = other.defaultRequest;
this.statusHandlers = (other.statusHandlers != null ? new LinkedHashMap<>(other.statusHandlers) : null);
this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null);
@@ -190,6 +194,13 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
return this.defaultCookies;
}
@Override
public WebClient.Builder apiVersionInserter(ApiVersionInserter apiVersionInserter) {
this.apiVersionInserter = apiVersionInserter;
return this;
}
@Override
public WebClient.Builder defaultRequest(Consumer<WebClient.RequestHeadersSpec<?>> defaultRequest) {
this.defaultRequest = this.defaultRequest != null ?
@@ -297,6 +308,7 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
return new DefaultWebClient(
exchange, filterFunctions,
initUriBuilderFactory(), defaultHeaders, defaultCookies,
this.apiVersionInserter,
this.defaultRequest,
this.statusHandlers,
this.observationRegistry, this.observationConvention,

View File

@@ -45,6 +45,8 @@ import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ApiVersionFormatter;
import org.springframework.web.client.ApiVersionInserter;
import org.springframework.web.reactive.function.BodyExtractor;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
@@ -250,6 +252,15 @@ public interface WebClient {
*/
Builder defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
/**
* Configure an {@link ApiVersionInserter} to abstract how an API version
* specified via {@link RequestHeadersSpec#apiVersion(Object)}
* is inserted into the request.
* @param apiVersionInserter the inserter to use
* @since 7.0
*/
Builder apiVersionInserter(ApiVersionInserter apiVersionInserter);
/**
* Provide a consumer to customize every request being built.
* @param defaultRequest the consumer to use for modifying requests
@@ -475,6 +486,17 @@ public interface WebClient {
*/
S headers(Consumer<HttpHeaders> headersConsumer);
/**
* Set an API version for the request. The version is inserted into the
* request by the {@link Builder#apiVersionInserter(ApiVersionInserter)
* configured} {@code ApiVersionInserter}.
* @param version the API version of the request; this can be a String or
* some Object that can be formatted the inserter, e.g. through an
* {@link ApiVersionFormatter}.
* @since 7.0
*/
S apiVersion(Object version);
/**
* Set the attribute with the given name to the given value.
* @param name the name of the attribute to add

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* 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.
@@ -127,6 +127,11 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter {
bodySpec.headers(headers -> headers.putAll(values.getHeaders()));
bodySpec.cookies(cookies -> cookies.putAll(values.getCookies()));
if (values.getApiVersion() != null) {
bodySpec.apiVersion(values.getApiVersion());
}
bodySpec.attributes(attributes -> attributes.putAll(values.getAttributes()));
if (values.getBodyValue() != null) {