Skip to content

Commit 707388b

Browse files
committed
Add API versioning auto-configuration and properties support
Update `RestClient`, `WebClient`, Spring MVC and Spring WebFlux auto-configuration to support API versioning. Closes gh-46519
1 parent eb31180 commit 707388b

File tree

32 files changed

+1640
-56
lines changed

32 files changed

+1640
-56
lines changed

documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,32 @@ For example, the following will use a JDK client configured with a specific java
277277

278278
include-code::MyClientHttpConfiguration[]
279279

280+
281+
282+
[[io.rest-client.apiversioning]]
283+
== API Versioning
284+
285+
Both `WebClient` and `RestClient` support making versioned remote HTTP calls so that APIs can be evolved over time.
286+
Commonly this involves sending an HTTP header, a query parameter or URL path segment that indicates the version of the API that should be used.
287+
288+
You can configure API versioning using methods on `WebClient.Builder` or `RestClient.Builder`.
289+
You can also the `spring.http.reactiveclient.webclient.apiversion` or `spring.http.client.restclient.apiversion` properties if you want to apply the same configuration to all builders.
290+
291+
For example, the following adds an `X-Version` HTTP header to all calls from the `RestClient` and uses the version `1.0.0` unless overridden for specific requests:
292+
293+
[configprops,yaml]
294+
----
295+
spring:
296+
http:
297+
client:
298+
restclient:
299+
apiversion:
300+
default: 1.0.0
301+
insert:
302+
header: X-Version
303+
----
304+
305+
You can also defined javadoc:org.springframework.web.client.ApiVersionInserter[] and javadoc:org.springframework.web.client.ApiVersionFormatter[] beans if you need more control of the way that version information should be inserted and formatted.
306+
307+
TIP: API versioning is also supported on the server-side.
308+
See the xref:web/servlet.adoc#web.servlet.spring-mvc.api-versioning[Spring MVC] and xref:web/reactive.adoc#web.reactive.webflux.api-versioning[Spring WebFlux] sections for details.

documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/reactive.adoc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,38 @@ When it does so, the orders shown in the following table will be used:
275275

276276

277277

278+
[[web.reactive.webflux.api-versioning]]
279+
=== API Versioning
280+
281+
Spring WebFlux supports API versioning which can be used to evolve an HTTP API over time.
282+
The same `@Controller` path can be mapped multiple times to support different versions of the API.
283+
284+
For more details see {url-spring-framework-docs}/web/webflux/controller/ann-requestmapping.html#webflux-ann-requestmapping-version[Spring Framework's reference documentation].
285+
286+
One mappings have been added, you additionally need to configure Spring WebFlux so that it is able to use any version information sent with a request.
287+
Typically, versions are sent as HTTP headers, query parameters or as part of the path.
288+
289+
To configure Spring WebFlux, you can either use a javadoc:org.springframework.web.reactive.config.WebFluxConfigurer[] bean and override the `configureApiVersioning(...)` method, or you can use properties.
290+
291+
For example, the following will use an `X-Version` HTTP header to obtain version information and default to `1.0.0` when no header is sent.
292+
293+
[configprops,yaml]
294+
----
295+
spring:
296+
webflux:
297+
apiversion:
298+
default: 1.0.0
299+
use:
300+
header: X-Version
301+
----
302+
303+
For more complete control, you can also define javadoc:org.springframework.web.reactive.accept.ApiVersionResolver[], javadoc:org.springframework.web.reactive.accept.ApiVersionParser[] and javadoc:org.springframework.web.reactive.accept.ApiVersionDeprecationHandler[] beans which will be injected into the auto-configured Spring MVC configuration.
304+
305+
TIP: API versioning is also supported on the client-side with both `WebClient` and `RestClient`.
306+
See xref:io/rest-client.adoc#io.rest-client.apiversioning[] for details.
307+
308+
309+
278310
[[web.reactive.reactive-server]]
279311
== Embedded Reactive Server Support
280312

documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,38 @@ include-code::MyCorsConfiguration[]
465465

466466

467467

468+
[[web.servlet.spring-mvc.api-versioning]]
469+
=== API Versioning
470+
471+
Spring MVC supports API versioning which can be used to evolve an HTTP API over time.
472+
The same `@Controller` path can be mapped multiple times to support different versions of the API.
473+
474+
For more details see {url-spring-framework-docs}/web/webmvc/mvc-controller/ann-requestmapping.html#mvc-ann-requestmapping-version[Spring Framework's reference documentation].
475+
476+
One mappings have been added, you additionally need to configure Spring MVC so that it is able to use any version information sent with a request.
477+
Typically, versions are sent as HTTP headers, query parameters or as part of the path.
478+
479+
To configure Spring MVC, you can either use a javadoc:org.springframework.web.servlet.config.annotation.WebMvcConfigurer[] bean and override the `configureApiVersioning(...)` method, or you can use properties.
480+
481+
For example, the following will use an `X-Version` HTTP header to obtain version information and default to `1.0.0` when no header is sent.
482+
483+
[configprops,yaml]
484+
----
485+
spring:
486+
mvc:
487+
apiversion:
488+
default: 1.0.0
489+
use:
490+
header: X-Version
491+
----
492+
493+
For more complete control, you can also define javadoc:org.springframework.web.accept.ApiVersionResolver[], javadoc:org.springframework.web.accept.ApiVersionParser[] and javadoc:org.springframework.web.accept.ApiVersionDeprecationHandler[] beans which will be injected into the auto-configured Spring MVC configuration.
494+
495+
TIP: API versioning is also supported with both `WebClient` and `RestClient`.
496+
See xref:io/rest-client.adoc#io.rest-client.apiversioning[] for details.
497+
498+
499+
468500
[[web.servlet.jersey]]
469501
== JAX-RS and Jersey
470502

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.http.client.autoconfigure;
18+
19+
import org.springframework.boot.context.properties.ConfigurationPropertiesSource;
20+
import org.springframework.boot.context.properties.bind.Name;
21+
22+
/**
23+
* API Version properties for reactive and blocking HTTP Clients.
24+
*
25+
* @author Phillip Webb
26+
* @since 4.0.0
27+
*/
28+
@ConfigurationPropertiesSource
29+
public class ApiversionProperties {
30+
31+
/**
32+
* Default version that should be used for each request.
33+
*/
34+
@Name("default")
35+
private String defaultVersion;
36+
37+
/**
38+
* How version details should be inserted into requests.
39+
*/
40+
private final Insert insert = new Insert();
41+
42+
public String getDefaultVersion() {
43+
return this.defaultVersion;
44+
}
45+
46+
public void setDefaultVersion(String defaultVersion) {
47+
this.defaultVersion = defaultVersion;
48+
}
49+
50+
public Insert getInsert() {
51+
return this.insert;
52+
}
53+
54+
@ConfigurationPropertiesSource
55+
public static class Insert {
56+
57+
/**
58+
* Insert the version into a header with the given name.
59+
*/
60+
private String header;
61+
62+
/**
63+
* Insert the version into a query parameter with the given name.
64+
*/
65+
private String queryParameter;
66+
67+
/**
68+
* Insert the version into a path segment at the given index.
69+
*/
70+
private Integer pathSegment;
71+
72+
public String getHeader() {
73+
return this.header;
74+
}
75+
76+
public void setHeader(String header) {
77+
this.header = header;
78+
}
79+
80+
public String getQueryParameter() {
81+
return this.queryParameter;
82+
}
83+
84+
public void setQueryParameter(String queryParameter) {
85+
this.queryParameter = queryParameter;
86+
}
87+
88+
public Integer getPathSegment() {
89+
return this.pathSegment;
90+
}
91+
92+
public void setPathSegment(Integer pathSegment) {
93+
this.pathSegment = pathSegment;
94+
}
95+
96+
}
97+
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.http.client.autoconfigure;
18+
19+
import java.net.URI;
20+
import java.util.ArrayList;
21+
import java.util.Arrays;
22+
import java.util.List;
23+
import java.util.stream.Stream;
24+
25+
import org.springframework.boot.context.properties.PropertyMapper;
26+
import org.springframework.boot.http.client.autoconfigure.ApiversionProperties.Insert;
27+
import org.springframework.http.HttpHeaders;
28+
import org.springframework.web.client.ApiVersionFormatter;
29+
import org.springframework.web.client.ApiVersionInserter;
30+
31+
/**
32+
* {@link ApiVersionInserter} to apply {@link ApiversionProperties}.
33+
*
34+
* @author Phillip Webb
35+
* @since 4.0.0
36+
*/
37+
public final class PropertiesApiVersionInserter implements ApiVersionInserter {
38+
39+
private final List<ApiVersionInserter> inserters;
40+
41+
private PropertiesApiVersionInserter(List<ApiVersionInserter> inserters) {
42+
this.inserters = inserters;
43+
}
44+
45+
@Override
46+
public URI insertVersion(Object version, URI uri) {
47+
for (ApiVersionInserter delegate : this.inserters) {
48+
uri = delegate.insertVersion(version, uri);
49+
}
50+
return uri;
51+
}
52+
53+
@Override
54+
public void insertVersion(Object version, HttpHeaders headers) {
55+
for (ApiVersionInserter delegate : this.inserters) {
56+
delegate.insertVersion(version, headers);
57+
}
58+
}
59+
60+
/**
61+
* Factory method that returns an {@link ApiVersionInserter} to apply the given
62+
* properties and delegate.
63+
* @param apiVersionInserter a delegate {@link ApiVersionInserter} that should also
64+
* apply (may be {@code null})
65+
* @param apiVersionFormatter the version formatter to use or {@code null}
66+
* @param properties the properties that should be applied
67+
* @return an {@link ApiVersionInserter} or {@code null} if no API version should be
68+
* inserted
69+
*/
70+
public static ApiVersionInserter get(ApiVersionInserter apiVersionInserter, ApiVersionFormatter apiVersionFormatter,
71+
ApiversionProperties... properties) {
72+
return get(apiVersionInserter, apiVersionFormatter, Arrays.stream(properties));
73+
}
74+
75+
/**
76+
* Factory method that returns an {@link ApiVersionInserter} to apply the given
77+
* properties and delegate.
78+
* @param apiVersionInserter a delegate {@link ApiVersionInserter} that should also
79+
* apply (may be {@code null})
80+
* @param apiVersionFormatter the version formatter to use or {@code null}
81+
* @param propertiesStream the properties that should be applied
82+
* @return an {@link ApiVersionInserter} or {@code null} if no API version should be
83+
* inserted
84+
*/
85+
public static ApiVersionInserter get(ApiVersionInserter apiVersionInserter, ApiVersionFormatter apiVersionFormatter,
86+
Stream<ApiversionProperties> propertiesStream) {
87+
List<ApiVersionInserter> inserters = new ArrayList<>();
88+
if (apiVersionInserter != null) {
89+
inserters.add(apiVersionInserter);
90+
}
91+
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
92+
propertiesStream.forEach((properties) -> {
93+
if (properties != null && properties.getInsert() != null) {
94+
Insert insert = properties.getInsert();
95+
Counter counter = new Counter();
96+
ApiVersionInserter.Builder builder = ApiVersionInserter.builder();
97+
map.from(apiVersionFormatter).to(builder::withVersionFormatter);
98+
map.from(insert::getHeader).whenHasText().as(counter::counted).to(builder::useHeader);
99+
map.from(insert::getQueryParameter).whenHasText().as(counter::counted).to(builder::useQueryParam);
100+
map.from(insert::getPathSegment).as(counter::counted).to(builder::usePathSegment);
101+
if (!counter.isEmpty()) {
102+
inserters.add(builder.build());
103+
}
104+
}
105+
});
106+
return (!inserters.isEmpty()) ? new PropertiesApiVersionInserter(inserters) : null;
107+
}
108+
109+
/**
110+
* Internal counter used to track if properties were applied.
111+
*/
112+
private static final class Counter {
113+
114+
private boolean empty = true;
115+
116+
<T> T counted(T value) {
117+
this.empty = false;
118+
return value;
119+
}
120+
121+
boolean isEmpty() {
122+
return this.empty;
123+
}
124+
125+
}
126+
127+
}

0 commit comments

Comments
 (0)