mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-29 05:29:08 +00:00
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
This commit is contained in:
+29
@@ -277,3 +277,32 @@ For example, the following will use a JDK client configured with a specific java
|
||||
|
||||
include-code::MyClientHttpConfiguration[]
|
||||
|
||||
|
||||
|
||||
[[io.rest-client.apiversioning]]
|
||||
== API Versioning
|
||||
|
||||
Both `WebClient` and `RestClient` support making versioned remote HTTP calls so that APIs can be evolved over time.
|
||||
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.
|
||||
|
||||
You can configure API versioning using methods on `WebClient.Builder` or `RestClient.Builder`.
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
spring:
|
||||
http:
|
||||
client:
|
||||
restclient:
|
||||
apiversion:
|
||||
default: 1.0.0
|
||||
insert:
|
||||
header: X-Version
|
||||
----
|
||||
|
||||
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.
|
||||
|
||||
TIP: API versioning is also supported on the server-side.
|
||||
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.
|
||||
|
||||
+32
@@ -275,6 +275,38 @@ When it does so, the orders shown in the following table will be used:
|
||||
|
||||
|
||||
|
||||
[[web.reactive.webflux.api-versioning]]
|
||||
=== API Versioning
|
||||
|
||||
Spring WebFlux supports API versioning which can be used to evolve an HTTP API over time.
|
||||
The same `@Controller` path can be mapped multiple times to support different versions of the API.
|
||||
|
||||
For more details see {url-spring-framework-docs}/web/webflux/controller/ann-requestmapping.html#webflux-ann-requestmapping-version[Spring Framework's reference documentation].
|
||||
|
||||
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.
|
||||
Typically, versions are sent as HTTP headers, query parameters or as part of the path.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
spring:
|
||||
webflux:
|
||||
apiversion:
|
||||
default: 1.0.0
|
||||
use:
|
||||
header: X-Version
|
||||
----
|
||||
|
||||
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.
|
||||
|
||||
TIP: API versioning is also supported on the client-side with both `WebClient` and `RestClient`.
|
||||
See xref:io/rest-client.adoc#io.rest-client.apiversioning[] for details.
|
||||
|
||||
|
||||
|
||||
[[web.reactive.reactive-server]]
|
||||
== Embedded Reactive Server Support
|
||||
|
||||
|
||||
+32
@@ -465,6 +465,38 @@ include-code::MyCorsConfiguration[]
|
||||
|
||||
|
||||
|
||||
[[web.servlet.spring-mvc.api-versioning]]
|
||||
=== API Versioning
|
||||
|
||||
Spring MVC supports API versioning which can be used to evolve an HTTP API over time.
|
||||
The same `@Controller` path can be mapped multiple times to support different versions of the API.
|
||||
|
||||
For more details see {url-spring-framework-docs}/web/webmvc/mvc-controller/ann-requestmapping.html#mvc-ann-requestmapping-version[Spring Framework's reference documentation].
|
||||
|
||||
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.
|
||||
Typically, versions are sent as HTTP headers, query parameters or as part of the path.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
spring:
|
||||
mvc:
|
||||
apiversion:
|
||||
default: 1.0.0
|
||||
use:
|
||||
header: X-Version
|
||||
----
|
||||
|
||||
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.
|
||||
|
||||
TIP: API versioning is also supported with both `WebClient` and `RestClient`.
|
||||
See xref:io/rest-client.adoc#io.rest-client.apiversioning[] for details.
|
||||
|
||||
|
||||
|
||||
[[web.servlet.jersey]]
|
||||
== JAX-RS and Jersey
|
||||
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.boot.http.client.autoconfigure;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesSource;
|
||||
import org.springframework.boot.context.properties.bind.Name;
|
||||
|
||||
/**
|
||||
* API Version properties for reactive and blocking HTTP Clients.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@ConfigurationPropertiesSource
|
||||
public class ApiversionProperties {
|
||||
|
||||
/**
|
||||
* Default version that should be used for each request.
|
||||
*/
|
||||
@Name("default")
|
||||
private String defaultVersion;
|
||||
|
||||
/**
|
||||
* How version details should be inserted into requests.
|
||||
*/
|
||||
private final Insert insert = new Insert();
|
||||
|
||||
public String getDefaultVersion() {
|
||||
return this.defaultVersion;
|
||||
}
|
||||
|
||||
public void setDefaultVersion(String defaultVersion) {
|
||||
this.defaultVersion = defaultVersion;
|
||||
}
|
||||
|
||||
public Insert getInsert() {
|
||||
return this.insert;
|
||||
}
|
||||
|
||||
@ConfigurationPropertiesSource
|
||||
public static class Insert {
|
||||
|
||||
/**
|
||||
* Insert the version into a header with the given name.
|
||||
*/
|
||||
private String header;
|
||||
|
||||
/**
|
||||
* Insert the version into a query parameter with the given name.
|
||||
*/
|
||||
private String queryParameter;
|
||||
|
||||
/**
|
||||
* Insert the version into a path segment at the given index.
|
||||
*/
|
||||
private Integer pathSegment;
|
||||
|
||||
public String getHeader() {
|
||||
return this.header;
|
||||
}
|
||||
|
||||
public void setHeader(String header) {
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
public String getQueryParameter() {
|
||||
return this.queryParameter;
|
||||
}
|
||||
|
||||
public void setQueryParameter(String queryParameter) {
|
||||
this.queryParameter = queryParameter;
|
||||
}
|
||||
|
||||
public Integer getPathSegment() {
|
||||
return this.pathSegment;
|
||||
}
|
||||
|
||||
public void setPathSegment(Integer pathSegment) {
|
||||
this.pathSegment = pathSegment;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.boot.http.client.autoconfigure;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.http.client.autoconfigure.ApiversionProperties.Insert;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
|
||||
/**
|
||||
* {@link ApiVersionInserter} to apply {@link ApiversionProperties}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public final class PropertiesApiVersionInserter implements ApiVersionInserter {
|
||||
|
||||
private final List<ApiVersionInserter> inserters;
|
||||
|
||||
private PropertiesApiVersionInserter(List<ApiVersionInserter> inserters) {
|
||||
this.inserters = inserters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI insertVersion(Object version, URI uri) {
|
||||
for (ApiVersionInserter delegate : this.inserters) {
|
||||
uri = delegate.insertVersion(version, uri);
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertVersion(Object version, HttpHeaders headers) {
|
||||
for (ApiVersionInserter delegate : this.inserters) {
|
||||
delegate.insertVersion(version, headers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method that returns an {@link ApiVersionInserter} to apply the given
|
||||
* properties and delegate.
|
||||
* @param apiVersionInserter a delegate {@link ApiVersionInserter} that should also
|
||||
* apply (may be {@code null})
|
||||
* @param apiVersionFormatter the version formatter to use or {@code null}
|
||||
* @param properties the properties that should be applied
|
||||
* @return an {@link ApiVersionInserter} or {@code null} if no API version should be
|
||||
* inserted
|
||||
*/
|
||||
public static ApiVersionInserter get(ApiVersionInserter apiVersionInserter, ApiVersionFormatter apiVersionFormatter,
|
||||
ApiversionProperties... properties) {
|
||||
return get(apiVersionInserter, apiVersionFormatter, Arrays.stream(properties));
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method that returns an {@link ApiVersionInserter} to apply the given
|
||||
* properties and delegate.
|
||||
* @param apiVersionInserter a delegate {@link ApiVersionInserter} that should also
|
||||
* apply (may be {@code null})
|
||||
* @param apiVersionFormatter the version formatter to use or {@code null}
|
||||
* @param propertiesStream the properties that should be applied
|
||||
* @return an {@link ApiVersionInserter} or {@code null} if no API version should be
|
||||
* inserted
|
||||
*/
|
||||
public static ApiVersionInserter get(ApiVersionInserter apiVersionInserter, ApiVersionFormatter apiVersionFormatter,
|
||||
Stream<ApiversionProperties> propertiesStream) {
|
||||
List<ApiVersionInserter> inserters = new ArrayList<>();
|
||||
if (apiVersionInserter != null) {
|
||||
inserters.add(apiVersionInserter);
|
||||
}
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
propertiesStream.forEach((properties) -> {
|
||||
if (properties != null && properties.getInsert() != null) {
|
||||
Insert insert = properties.getInsert();
|
||||
Counter counter = new Counter();
|
||||
ApiVersionInserter.Builder builder = ApiVersionInserter.builder();
|
||||
map.from(apiVersionFormatter).to(builder::withVersionFormatter);
|
||||
map.from(insert::getHeader).whenHasText().as(counter::counted).to(builder::useHeader);
|
||||
map.from(insert::getQueryParameter).whenHasText().as(counter::counted).to(builder::useQueryParam);
|
||||
map.from(insert::getPathSegment).as(counter::counted).to(builder::usePathSegment);
|
||||
if (!counter.isEmpty()) {
|
||||
inserters.add(builder.build());
|
||||
}
|
||||
}
|
||||
});
|
||||
return (!inserters.isEmpty()) ? new PropertiesApiVersionInserter(inserters) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal counter used to track if properties were applied.
|
||||
*/
|
||||
private static final class Counter {
|
||||
|
||||
private boolean empty = true;
|
||||
|
||||
<T> T counted(T value) {
|
||||
this.empty = false;
|
||||
return value;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return this.empty;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.boot.http.client.autoconfigure;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link PropertiesApiVersionInserter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PropertiesApiVersionInserterTests {
|
||||
|
||||
@Test
|
||||
void getWhenEmptyPropertiesArrayAndNoDeleteReturnsNull() {
|
||||
assertThat(PropertiesApiVersionInserter.get(null, null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenNoPropertiesAndNoDelegateReturnsNull() {
|
||||
assertThat(PropertiesApiVersionInserter.get(null, null, new ApiversionProperties(), new ApiversionProperties()))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenNoPropertiesAndDelegateUsesDelegate() throws Exception {
|
||||
ApiVersionInserter inserter = PropertiesApiVersionInserter.get(ApiVersionInserter.useQueryParam("v"), null);
|
||||
URI uri = new URI("https://example.com");
|
||||
assertThat(inserter.insertVersion("123", uri)).hasToString("https://example.com?v=123");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getReturnsInserterThatAppliesProperties() throws Exception {
|
||||
ApiversionProperties properties1 = new ApiversionProperties();
|
||||
properties1.getInsert().setHeader("x-test");
|
||||
properties1.getInsert().setQueryParameter("v1");
|
||||
ApiversionProperties properties2 = new ApiversionProperties();
|
||||
properties2.getInsert().setQueryParameter("v2");
|
||||
properties2.getInsert().setPathSegment(1);
|
||||
ApiVersionInserter inserter = PropertiesApiVersionInserter.get(null, null, properties1, properties2);
|
||||
URI uri = new URI("https://example.com/foo/bar");
|
||||
assertThat(inserter.insertVersion("123", uri)).hasToString("https://example.com/foo/123/bar?v1=123&v2=123");
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
inserter.insertVersion("123", headers);
|
||||
assertThat(headers.get("x-test")).containsExactly("123");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenHasDelegateReturnsInserterThatAppliesPropertiesAndDelegate() throws Exception {
|
||||
ApiVersionInserter delegate = ApiVersionInserter.useQueryParam("d");
|
||||
ApiversionProperties properties = new ApiversionProperties();
|
||||
properties.getInsert().setQueryParameter("v");
|
||||
ApiVersionInserter inserter = PropertiesApiVersionInserter.get(delegate, null, properties);
|
||||
assertThat(inserter.insertVersion("123", new URI("https://example.com")))
|
||||
.hasToString("https://example.com?d=123&v=123");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenHasFormatterAppliesToProperties() throws Exception {
|
||||
ApiversionProperties properties1 = new ApiversionProperties();
|
||||
properties1.getInsert().setQueryParameter("v");
|
||||
ApiVersionFormatter formatter = (version) -> String.valueOf(version).toUpperCase(Locale.ROOT);
|
||||
ApiVersionInserter inserter = PropertiesApiVersionInserter.get(null, formatter, properties1);
|
||||
URI uri = new URI("https://example.com");
|
||||
assertThat(inserter.insertVersion("latest", uri)).hasToString("https://example.com?v=LATEST");
|
||||
}
|
||||
|
||||
}
|
||||
+17
-3
@@ -14,23 +14,27 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.restclient.autoconfigure.service;
|
||||
package org.springframework.boot.restclient.autoconfigure;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.boot.http.client.autoconfigure.AbstractHttpRequestFactoryProperties;
|
||||
import org.springframework.boot.http.client.autoconfigure.ApiversionProperties;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* {@link AbstractHttpRequestFactoryProperties} for HTTP Service clients.
|
||||
* {@link AbstractHttpRequestFactoryProperties} for properties to configure technologies
|
||||
* built on {@link RestClient}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public abstract class AbstractHttpClientServiceProperties extends AbstractHttpRequestFactoryProperties {
|
||||
public abstract class AbstractRestClientProperties extends AbstractHttpRequestFactoryProperties {
|
||||
|
||||
/**
|
||||
* Base url to set in the underlying HTTP client group. By default, set to
|
||||
@@ -44,6 +48,12 @@ public abstract class AbstractHttpClientServiceProperties extends AbstractHttpRe
|
||||
*/
|
||||
private Map<String, List<String>> defaultHeader = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* API version properties.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private final ApiversionProperties apiversion = new ApiversionProperties();
|
||||
|
||||
public String getBaseUrl() {
|
||||
return this.baseUrl;
|
||||
}
|
||||
@@ -60,4 +70,8 @@ public abstract class AbstractHttpClientServiceProperties extends AbstractHttpRe
|
||||
this.defaultHeader = defaultHeaders;
|
||||
}
|
||||
|
||||
public ApiversionProperties getApiversion() {
|
||||
return this.apiversion;
|
||||
}
|
||||
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.boot.restclient.autoconfigure;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.http.client.autoconfigure.ApiversionProperties;
|
||||
import org.springframework.boot.http.client.autoconfigure.PropertiesApiVersionInserter;
|
||||
import org.springframework.boot.restclient.RestClientCustomizer;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* {@link RestClientCustomizer} to apply {@link AbstractRestClientProperties}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class PropertiesRestClientCustomizer implements RestClientCustomizer {
|
||||
|
||||
private final AbstractRestClientProperties[] orderedProperties;
|
||||
|
||||
private ApiVersionInserter apiVersionInserter;
|
||||
|
||||
public PropertiesRestClientCustomizer(ApiVersionInserter apiVersionInserter,
|
||||
ApiVersionFormatter apiVersionFormatter, AbstractRestClientProperties... orderedProperties) {
|
||||
this.orderedProperties = orderedProperties;
|
||||
this.apiVersionInserter = PropertiesApiVersionInserter.get(apiVersionInserter, apiVersionFormatter,
|
||||
Arrays.stream(orderedProperties).map(this::getApiVersion));
|
||||
}
|
||||
|
||||
private ApiversionProperties getApiVersion(AbstractRestClientProperties properties) {
|
||||
return (properties != null) ? properties.getApiversion() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(RestClient.Builder builder) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(this.apiVersionInserter).to(builder::apiVersionInserter);
|
||||
for (int i = this.orderedProperties.length - 1; i >= 0; i--) {
|
||||
AbstractRestClientProperties properties = this.orderedProperties[i];
|
||||
if (properties != null) {
|
||||
map.from(properties::getBaseUrl).whenHasText().to(builder::baseUrl);
|
||||
map.from(properties::getDefaultHeader).as(this::putAllHeaders).to(builder::defaultHeaders);
|
||||
map.from(properties.getApiversion())
|
||||
.as(ApiversionProperties::getDefaultVersion)
|
||||
.to(builder::defaultApiVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Consumer<HttpHeaders> putAllHeaders(Map<String, List<String>> defaultHeaders) {
|
||||
return (httpHeaders) -> httpHeaders.putAll(defaultHeaders);
|
||||
}
|
||||
|
||||
}
|
||||
+11
-2
@@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
|
||||
@@ -36,6 +37,8 @@ import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.RestClient.Builder;
|
||||
|
||||
@@ -48,12 +51,14 @@ import org.springframework.web.client.RestClient.Builder;
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Moritz Halbritter
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(
|
||||
after = { HttpClientAutoConfiguration.class, TaskExecutionAutoConfiguration.class, SslAutoConfiguration.class })
|
||||
@ConditionalOnClass(RestClient.class)
|
||||
@Conditional(NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition.class)
|
||||
@EnableConfigurationProperties(RestClientProperties.class)
|
||||
public final class RestClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -73,11 +78,15 @@ public final class RestClientAutoConfiguration {
|
||||
RestClientBuilderConfigurer restClientBuilderConfigurer(
|
||||
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientHttpRequestFactoryBuilder,
|
||||
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings,
|
||||
ObjectProvider<RestClientCustomizer> customizerProvider) {
|
||||
ObjectProvider<RestClientCustomizer> customizerProvider,
|
||||
ObjectProvider<ApiVersionInserter> apiVersionInserter,
|
||||
ObjectProvider<ApiVersionFormatter> apiVersionFormatter, RestClientProperties restClientProperties) {
|
||||
PropertiesRestClientCustomizer propertiesCustomizer = new PropertiesRestClientCustomizer(
|
||||
apiVersionInserter.getIfAvailable(), apiVersionFormatter.getIfAvailable(), restClientProperties);
|
||||
return new RestClientBuilderConfigurer(
|
||||
clientHttpRequestFactoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect),
|
||||
clientHttpRequestFactorySettings.getIfAvailable(ClientHttpRequestFactorySettings::defaults),
|
||||
customizerProvider.orderedStream().toList());
|
||||
propertiesCustomizer, customizerProvider.orderedStream().toList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
+10
-3
@@ -43,15 +43,19 @@ public class RestClientBuilderConfigurer {
|
||||
|
||||
private final List<RestClientCustomizer> customizers;
|
||||
|
||||
private final PropertiesRestClientCustomizer propertiesCustomizer;
|
||||
|
||||
public RestClientBuilderConfigurer() {
|
||||
this(ClientHttpRequestFactoryBuilder.detect(), ClientHttpRequestFactorySettings.defaults(),
|
||||
this(ClientHttpRequestFactoryBuilder.detect(), ClientHttpRequestFactorySettings.defaults(), null,
|
||||
Collections.emptyList());
|
||||
}
|
||||
|
||||
RestClientBuilderConfigurer(ClientHttpRequestFactoryBuilder<?> requestFactoryBuilder,
|
||||
ClientHttpRequestFactorySettings requestFactorySettings, List<RestClientCustomizer> customizers) {
|
||||
ClientHttpRequestFactorySettings requestFactorySettings,
|
||||
PropertiesRestClientCustomizer propertiesCustomizer, List<RestClientCustomizer> customizers) {
|
||||
this.requestFactoryBuilder = requestFactoryBuilder;
|
||||
this.requestFactorySettings = requestFactorySettings;
|
||||
this.propertiesCustomizer = propertiesCustomizer;
|
||||
this.customizers = customizers;
|
||||
}
|
||||
|
||||
@@ -67,7 +71,10 @@ public class RestClientBuilderConfigurer {
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void applyCustomizers(Builder builder) {
|
||||
private void applyCustomizers(RestClient.Builder builder) {
|
||||
if (this.propertiesCustomizer != null) {
|
||||
this.propertiesCustomizer.customize(builder);
|
||||
}
|
||||
for (RestClientCustomizer customizer : this.customizers) {
|
||||
customizer.customize(builder);
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.boot.restclient.autoconfigure;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* Properties for {@link RestClient}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.http.client.restclient")
|
||||
public class RestClientProperties extends AbstractRestClientProperties {
|
||||
|
||||
}
|
||||
+3
-2
@@ -20,6 +20,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.restclient.autoconfigure.AbstractRestClientProperties;
|
||||
|
||||
/**
|
||||
* Properties for HTTP Service clients.
|
||||
@@ -30,7 +31,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.http.client.service")
|
||||
public class HttpClientServiceProperties extends AbstractHttpClientServiceProperties {
|
||||
public class HttpClientServiceProperties extends AbstractRestClientProperties {
|
||||
|
||||
/**
|
||||
* Group settings.
|
||||
@@ -48,7 +49,7 @@ public class HttpClientServiceProperties extends AbstractHttpClientServiceProper
|
||||
/**
|
||||
* Properties for a single HTTP Service client group.
|
||||
*/
|
||||
public static class Group extends AbstractHttpClientServiceProperties {
|
||||
public static class Group extends AbstractRestClientProperties {
|
||||
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -31,6 +31,8 @@ import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfigura
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
import org.springframework.web.client.support.RestClientAdapter;
|
||||
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
|
||||
import org.springframework.web.service.registry.ImportHttpServices;
|
||||
@@ -68,10 +70,12 @@ public final class HttpServiceClientAutoConfiguration implements BeanClassLoader
|
||||
ObjectProvider<SslBundles> sslBundles, ObjectProvider<HttpClientProperties> httpClientProperties,
|
||||
HttpClientServiceProperties serviceProperties,
|
||||
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientFactoryBuilder,
|
||||
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings) {
|
||||
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings,
|
||||
ObjectProvider<ApiVersionInserter> apiVersionInserter,
|
||||
ObjectProvider<ApiVersionFormatter> apiVersionFormatter) {
|
||||
return new RestClientPropertiesHttpServiceGroupConfigurer(this.beanClassLoader, sslBundles,
|
||||
httpClientProperties.getIfAvailable(), serviceProperties, clientFactoryBuilder,
|
||||
clientHttpRequestFactorySettings);
|
||||
clientHttpRequestFactorySettings, apiVersionInserter, apiVersionFormatter);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
+17
-16
@@ -16,20 +16,17 @@
|
||||
|
||||
package org.springframework.boot.restclient.autoconfigure.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.http.client.autoconfigure.ClientHttpRequestFactories;
|
||||
import org.springframework.boot.http.client.autoconfigure.HttpClientProperties;
|
||||
import org.springframework.boot.restclient.autoconfigure.PropertiesRestClientCustomizer;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
|
||||
import org.springframework.web.service.registry.HttpServiceGroup;
|
||||
@@ -55,16 +52,24 @@ class RestClientPropertiesHttpServiceGroupConfigurer implements RestClientHttpSe
|
||||
|
||||
private final ObjectProvider<ClientHttpRequestFactorySettings> requestFactorySettings;
|
||||
|
||||
private final ApiVersionInserter apiVersionInserter;
|
||||
|
||||
private final ApiVersionFormatter apiVersionFormatter;
|
||||
|
||||
RestClientPropertiesHttpServiceGroupConfigurer(ClassLoader classLoader, ObjectProvider<SslBundles> sslBundles,
|
||||
HttpClientProperties clientProperties, HttpClientServiceProperties serviceProperties,
|
||||
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> requestFactoryBuilder,
|
||||
ObjectProvider<ClientHttpRequestFactorySettings> requestFactorySettings) {
|
||||
ObjectProvider<ClientHttpRequestFactorySettings> requestFactorySettings,
|
||||
ObjectProvider<ApiVersionInserter> apiVersionInserter,
|
||||
ObjectProvider<ApiVersionFormatter> apiVersionFormatter) {
|
||||
this.classLoader = classLoader;
|
||||
this.sslBundles = sslBundles;
|
||||
this.clientProperties = clientProperties;
|
||||
this.serviceProperties = serviceProperties;
|
||||
this.requestFactoryBuilder = requestFactoryBuilder;
|
||||
this.requestFactorySettings = requestFactorySettings;
|
||||
this.apiVersionInserter = apiVersionInserter.getIfAvailable();
|
||||
this.apiVersionFormatter = apiVersionFormatter.getIfAvailable();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,17 +85,13 @@ class RestClientPropertiesHttpServiceGroupConfigurer implements RestClientHttpSe
|
||||
private void configureClient(HttpServiceGroup group, RestClient.Builder builder) {
|
||||
HttpClientServiceProperties.Group groupProperties = this.serviceProperties.getGroup().get(group.name());
|
||||
builder.requestFactory(getRequestFactory(groupProperties));
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(this.serviceProperties::getBaseUrl).whenHasText().to(builder::baseUrl);
|
||||
map.from(this.serviceProperties::getDefaultHeader).as(this::putAllHeaders).to(builder::defaultHeaders);
|
||||
if (groupProperties != null) {
|
||||
map.from(groupProperties::getBaseUrl).whenHasText().to(builder::baseUrl);
|
||||
map.from(groupProperties::getDefaultHeader).as(this::putAllHeaders).to(builder::defaultHeaders);
|
||||
}
|
||||
getPropertiesRestClientCustomizer(groupProperties).customize(builder);
|
||||
}
|
||||
|
||||
private Consumer<HttpHeaders> putAllHeaders(Map<String, List<String>> defaultHeaders) {
|
||||
return (httpHeaders) -> httpHeaders.putAll(defaultHeaders);
|
||||
private PropertiesRestClientCustomizer getPropertiesRestClientCustomizer(
|
||||
HttpClientServiceProperties.Group groupProperties) {
|
||||
return new PropertiesRestClientCustomizer(this.apiVersionInserter, this.apiVersionFormatter, groupProperties,
|
||||
this.serviceProperties);
|
||||
}
|
||||
|
||||
private ClientHttpRequestFactory getRequestFactory(HttpClientServiceProperties.Group groupProperties) {
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.boot.restclient.autoconfigure;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link PropertiesRestClientCustomizer}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PropertiesRestClientCustomizerTests {
|
||||
|
||||
@Test
|
||||
void customizeAppliesPropertiesInOrder() throws Exception {
|
||||
ApiVersionInserter delegateApiVersionInserter = ApiVersionInserter.useQueryParam("v");
|
||||
ApiVersionFormatter apiVersionFormatter = (version) -> String.valueOf(version).toUpperCase(Locale.ROOT);
|
||||
TestRestClientProperties properties1 = new TestRestClientProperties();
|
||||
properties1.setBaseUrl("https://example.com/b1");
|
||||
properties1.getDefaultHeader().put("x-h1", List.of("v1"));
|
||||
properties1.getApiversion().setDefaultVersion("dv1");
|
||||
properties1.getApiversion().getInsert().setQueryParameter("p1");
|
||||
TestRestClientProperties properties2 = new TestRestClientProperties();
|
||||
properties2.setBaseUrl("https://example.com/b2");
|
||||
properties1.getDefaultHeader().put("x-h2", List.of("v2"));
|
||||
properties2.getApiversion().setDefaultVersion("dv2");
|
||||
PropertiesRestClientCustomizer customizer = new PropertiesRestClientCustomizer(delegateApiVersionInserter,
|
||||
apiVersionFormatter, properties1, properties2);
|
||||
RestClient.Builder builder = RestClient.builder();
|
||||
customizer.customize(builder);
|
||||
RestClient client = builder.build();
|
||||
assertThat(client).extracting("defaultApiVersion").isEqualTo("dv1");
|
||||
UriBuilderFactory uriBuilderFactory = (UriBuilderFactory) ReflectionTestUtils.getField(client,
|
||||
"uriBuilderFactory");
|
||||
assertThat(uriBuilderFactory.builder().build()).hasToString("https://example.com/b1");
|
||||
HttpHeaders defaultHeaders = (HttpHeaders) ReflectionTestUtils.getField(client, "defaultHeaders");
|
||||
assertThat(defaultHeaders.get("x-h1")).containsExactly("v1");
|
||||
assertThat(defaultHeaders.get("x-h2")).containsExactly("v2");
|
||||
ApiVersionInserter apiVersionInserter = (ApiVersionInserter) ReflectionTestUtils.getField(client,
|
||||
"apiVersionInserter");
|
||||
assertThat(apiVersionInserter.insertVersion("v123", new URI("https://example.com")))
|
||||
.hasToString("https://example.com?v=v123&p1=V123");
|
||||
}
|
||||
|
||||
static class TestRestClientProperties extends AbstractRestClientProperties {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -16,8 +16,10 @@
|
||||
|
||||
package org.springframework.boot.restclient.autoconfigure;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledForJreRange;
|
||||
@@ -43,6 +45,8 @@ import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.RestClient.Builder;
|
||||
|
||||
@@ -316,6 +320,48 @@ class RestClientAutoConfigurationTests {
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(RestClient.Builder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasApiVersionProperties() {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
|
||||
.withPropertyValues("spring.http.client.restclient.apiversion.default=123",
|
||||
"spring.http.client.restclient.apiversion.insert.query-parameter=version")
|
||||
.run((context) -> {
|
||||
RestClient restClient = context.getBean(RestClient.Builder.class).build();
|
||||
assertThat(restClient).extracting("defaultApiVersion").isEqualTo("123");
|
||||
ApiVersionInserter apiVersionInserter = (ApiVersionInserter) ReflectionTestUtils.getField(restClient,
|
||||
"apiVersionInserter");
|
||||
assertThat(apiVersionInserter.insertVersion("123", new URI("https://example.com")))
|
||||
.hasToString("https://example.com?version=123");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasCustomApiVersionInserter() {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
|
||||
.withUserConfiguration(ApiVersionInserterConfig.class)
|
||||
.run((context) -> {
|
||||
RestClient restClient = context.getBean(RestClient.Builder.class).build();
|
||||
ApiVersionInserter apiVersionInserter = (ApiVersionInserter) ReflectionTestUtils.getField(restClient,
|
||||
"apiVersionInserter");
|
||||
assertThat(apiVersionInserter.insertVersion("123", new URI("https://example.com")))
|
||||
.hasToString("https://example.com?version=123");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasCustomApiVersionFormatter() {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
|
||||
.withPropertyValues("spring.http.client.restclient.apiversion.insert.query-parameter=version")
|
||||
.withUserConfiguration(ApiVersionFormatterConfig.class)
|
||||
.run((context) -> {
|
||||
RestClient restClient = context.getBean(RestClient.Builder.class).build();
|
||||
ApiVersionInserter apiVersionInserter = (ApiVersionInserter) ReflectionTestUtils.getField(restClient,
|
||||
"apiVersionInserter");
|
||||
assertThat(apiVersionInserter.insertVersion("best", new URI("https://example.com")))
|
||||
.hasToString("https://example.com?version=BEST");
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class RestClientCustomizerConfig {
|
||||
|
||||
@@ -354,4 +400,24 @@ class RestClientAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ApiVersionInserterConfig {
|
||||
|
||||
@Bean
|
||||
ApiVersionInserter apiVersionInserter() {
|
||||
return ApiVersionInserter.useQueryParam("version");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ApiVersionFormatterConfig {
|
||||
|
||||
@Bean
|
||||
ApiVersionFormatter apiVersionFormatter() {
|
||||
return (version) -> String.valueOf(version).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-2
@@ -55,9 +55,8 @@ class RestClientBuilderConfigurerTests {
|
||||
RestClientCustomizer customizer = mock(RestClientCustomizer.class);
|
||||
RestClientCustomizer customizer1 = mock(RestClientCustomizer.class);
|
||||
RestClientBuilderConfigurer configurer = new RestClientBuilderConfigurer(this.clientHttpRequestFactoryBuilder,
|
||||
settings, List.of(customizer, customizer1));
|
||||
settings, null, List.of(customizer, customizer1));
|
||||
given(this.clientHttpRequestFactoryBuilder.build(settings)).willReturn(this.clientHttpRequestFactory);
|
||||
|
||||
RestClient.Builder builder = RestClient.builder();
|
||||
configurer.configure(builder);
|
||||
assertThat(builder.build()).hasFieldOrPropertyWithValue("clientRequestFactory", this.clientHttpRequestFactory);
|
||||
|
||||
+17
-3
@@ -14,23 +14,27 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.webclient.autoconfigure.service;
|
||||
package org.springframework.boot.webclient.autoconfigure;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.boot.http.client.autoconfigure.ApiversionProperties;
|
||||
import org.springframework.boot.http.client.autoconfigure.reactive.AbstractClientHttpConnectorProperties;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* {@link AbstractClientHttpConnectorProperties} for reactive HTTP Service clients.
|
||||
* {@link AbstractClientHttpConnectorProperties} for properties to configure technologies
|
||||
* built on {@link WebClient}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public abstract class AbstractHttpReactiveClientServiceProperties extends AbstractClientHttpConnectorProperties {
|
||||
public abstract class AbstractWebClientProperties extends AbstractClientHttpConnectorProperties {
|
||||
|
||||
/**
|
||||
* Base url to set in the underlying HTTP client group. By default, set to
|
||||
@@ -44,6 +48,12 @@ public abstract class AbstractHttpReactiveClientServiceProperties extends Abstra
|
||||
*/
|
||||
private Map<String, List<String>> defaultHeader = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* API version properties.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private final ApiversionProperties apiversion = new ApiversionProperties();
|
||||
|
||||
public String getBaseUrl() {
|
||||
return this.baseUrl;
|
||||
}
|
||||
@@ -60,4 +70,8 @@ public abstract class AbstractHttpReactiveClientServiceProperties extends Abstra
|
||||
this.defaultHeader = defaultHeaders;
|
||||
}
|
||||
|
||||
public ApiversionProperties getApiversion() {
|
||||
return this.apiversion;
|
||||
}
|
||||
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.boot.webclient.autoconfigure;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.http.client.autoconfigure.ApiversionProperties;
|
||||
import org.springframework.boot.http.client.autoconfigure.PropertiesApiVersionInserter;
|
||||
import org.springframework.boot.webclient.WebClientCustomizer;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* {@link WebClientCustomizer} to apply {@link AbstractWebClientProperties}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class PropertiesWebClientCustomizer implements WebClientCustomizer {
|
||||
|
||||
private final AbstractWebClientProperties[] orderedProperties;
|
||||
|
||||
private ApiVersionInserter apiVersionInserter;
|
||||
|
||||
public PropertiesWebClientCustomizer(ApiVersionInserter apiVersionInserter, ApiVersionFormatter apiVersionFormatter,
|
||||
AbstractWebClientProperties... orderedProperties) {
|
||||
this.orderedProperties = orderedProperties;
|
||||
this.apiVersionInserter = PropertiesApiVersionInserter.get(apiVersionInserter, apiVersionFormatter,
|
||||
Arrays.stream(orderedProperties).map(this::getApiVersion));
|
||||
}
|
||||
|
||||
private ApiversionProperties getApiVersion(AbstractWebClientProperties properties) {
|
||||
return (properties != null) ? properties.getApiversion() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(WebClient.Builder builder) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(this.apiVersionInserter).to(builder::apiVersionInserter);
|
||||
for (int i = this.orderedProperties.length - 1; i >= 0; i--) {
|
||||
AbstractWebClientProperties properties = this.orderedProperties[i];
|
||||
if (properties != null) {
|
||||
map.from(properties::getBaseUrl).whenHasText().to(builder::baseUrl);
|
||||
map.from(properties::getDefaultHeader).as(this::putAllHeaders).to(builder::defaultHeaders);
|
||||
map.from(properties.getApiversion())
|
||||
.as(ApiversionProperties::getDefaultVersion)
|
||||
.to(builder::defaultApiVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Consumer<HttpHeaders> putAllHeaders(Map<String, List<String>> defaultHeaders) {
|
||||
return (httpHeaders) -> httpHeaders.putAll(defaultHeaders);
|
||||
}
|
||||
|
||||
}
|
||||
+10
-1
@@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.http.client.autoconfigure.reactive.ClientHttpConnectorAutoConfiguration;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
|
||||
@@ -36,6 +37,8 @@ import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
@@ -52,13 +55,19 @@ import org.springframework.web.reactive.function.client.WebClient;
|
||||
*/
|
||||
@AutoConfiguration(after = { ClientHttpConnectorAutoConfiguration.class, CodecsAutoConfiguration.class })
|
||||
@ConditionalOnClass(WebClient.class)
|
||||
@EnableConfigurationProperties(WebClientProperties.class)
|
||||
public final class WebClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@ConditionalOnMissingBean
|
||||
WebClient.Builder webClientBuilder(ObjectProvider<WebClientCustomizer> customizerProvider) {
|
||||
WebClient.Builder webClientBuilder(ObjectProvider<WebClientCustomizer> customizerProvider,
|
||||
ObjectProvider<ApiVersionInserter> apiVersionInserter,
|
||||
ObjectProvider<ApiVersionFormatter> apiVersionFormatter, WebClientProperties webClientProperties) {
|
||||
WebClient.Builder builder = WebClient.builder();
|
||||
PropertiesWebClientCustomizer propertiesCustomizer = new PropertiesWebClientCustomizer(
|
||||
apiVersionInserter.getIfAvailable(), apiVersionFormatter.getIfAvailable(), webClientProperties);
|
||||
propertiesCustomizer.customize(builder);
|
||||
customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
return builder;
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.boot.webclient.autoconfigure;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* Properties for {@link WebClient}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.http.reactiveclient.webclient")
|
||||
public class WebClientProperties extends AbstractWebClientProperties {
|
||||
|
||||
}
|
||||
+3
-2
@@ -20,6 +20,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.webclient.autoconfigure.AbstractWebClientProperties;
|
||||
|
||||
/**
|
||||
* Properties for Reactive HTTP Service clients.
|
||||
@@ -30,7 +31,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.http.reactiveclient.service")
|
||||
public class ReactiveHttpClientServiceProperties extends AbstractHttpReactiveClientServiceProperties {
|
||||
public class ReactiveHttpClientServiceProperties extends AbstractWebClientProperties {
|
||||
|
||||
/**
|
||||
* Group settings.
|
||||
@@ -48,7 +49,7 @@ public class ReactiveHttpClientServiceProperties extends AbstractHttpReactiveCli
|
||||
/**
|
||||
* Properties for a single HTTP Service client group.
|
||||
*/
|
||||
public static class Group extends AbstractHttpReactiveClientServiceProperties {
|
||||
public static class Group extends AbstractWebClientProperties {
|
||||
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -30,6 +30,8 @@ import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.webclient.WebClientCustomizer;
|
||||
import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
|
||||
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
|
||||
import org.springframework.web.service.registry.ImportHttpServices;
|
||||
@@ -66,9 +68,12 @@ public final class ReactiveHttpServiceClientAutoConfiguration implements BeanCla
|
||||
ObjectProvider<SslBundles> sslBundles, HttpReactiveClientProperties httpReactiveClientProperties,
|
||||
ReactiveHttpClientServiceProperties serviceProperties,
|
||||
ObjectProvider<ClientHttpConnectorBuilder<?>> clientConnectorBuilder,
|
||||
ObjectProvider<ClientHttpConnectorSettings> clientConnectorSettings) {
|
||||
ObjectProvider<ClientHttpConnectorSettings> clientConnectorSettings,
|
||||
ObjectProvider<ApiVersionInserter> apiVersionInserter,
|
||||
ObjectProvider<ApiVersionFormatter> apiVersionFormatter) {
|
||||
return new WebClientPropertiesHttpServiceGroupConfigurer(this.beanClassLoader, sslBundles,
|
||||
httpReactiveClientProperties, serviceProperties, clientConnectorBuilder, clientConnectorSettings);
|
||||
httpReactiveClientProperties, serviceProperties, clientConnectorBuilder, clientConnectorSettings,
|
||||
apiVersionInserter, apiVersionFormatter);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
+17
-16
@@ -16,20 +16,17 @@
|
||||
|
||||
package org.springframework.boot.webclient.autoconfigure.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.http.client.autoconfigure.reactive.ClientHttpConnectors;
|
||||
import org.springframework.boot.http.client.autoconfigure.reactive.HttpReactiveClientProperties;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.webclient.autoconfigure.PropertiesWebClientCustomizer;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
@@ -57,16 +54,24 @@ class WebClientPropertiesHttpServiceGroupConfigurer implements WebClientHttpServ
|
||||
|
||||
private final ObjectProvider<ClientHttpConnectorSettings> clientConnectorSettings;
|
||||
|
||||
private final ApiVersionInserter apiVersionInserter;
|
||||
|
||||
private final ApiVersionFormatter apiVersionFormatter;
|
||||
|
||||
WebClientPropertiesHttpServiceGroupConfigurer(ClassLoader classLoader, ObjectProvider<SslBundles> sslBundles,
|
||||
HttpReactiveClientProperties clientProperties, ReactiveHttpClientServiceProperties serviceProperties,
|
||||
ObjectProvider<ClientHttpConnectorBuilder<?>> clientConnectorBuilder,
|
||||
ObjectProvider<ClientHttpConnectorSettings> clientConnectorSettings) {
|
||||
ObjectProvider<ClientHttpConnectorSettings> clientConnectorSettings,
|
||||
ObjectProvider<ApiVersionInserter> apiVersionInserter,
|
||||
ObjectProvider<ApiVersionFormatter> apiVersionFormatter) {
|
||||
this.classLoader = classLoader;
|
||||
this.sslBundles = sslBundles;
|
||||
this.clientProperties = clientProperties;
|
||||
this.serviceProperties = serviceProperties;
|
||||
this.clientConnectorBuilder = clientConnectorBuilder;
|
||||
this.clientConnectorSettings = clientConnectorSettings;
|
||||
this.apiVersionInserter = apiVersionInserter.getIfAvailable();
|
||||
this.apiVersionFormatter = apiVersionFormatter.getIfAvailable();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,17 +87,13 @@ class WebClientPropertiesHttpServiceGroupConfigurer implements WebClientHttpServ
|
||||
private void configureClient(HttpServiceGroup group, WebClient.Builder builder) {
|
||||
ReactiveHttpClientServiceProperties.Group groupProperties = this.serviceProperties.getGroup().get(group.name());
|
||||
builder.clientConnector(getClientConnector(groupProperties));
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(this.serviceProperties::getBaseUrl).whenHasText().to(builder::baseUrl);
|
||||
map.from(this.serviceProperties::getDefaultHeader).as(this::putAllHeaders).to(builder::defaultHeaders);
|
||||
if (groupProperties != null) {
|
||||
map.from(groupProperties::getBaseUrl).whenHasText().to(builder::baseUrl);
|
||||
map.from(groupProperties::getDefaultHeader).as(this::putAllHeaders).to(builder::defaultHeaders);
|
||||
}
|
||||
getPropertiesWebClientCustomizer(groupProperties).customize(builder);
|
||||
}
|
||||
|
||||
private Consumer<HttpHeaders> putAllHeaders(Map<String, List<String>> defaultHeaders) {
|
||||
return (httpHeaders) -> httpHeaders.putAll(defaultHeaders);
|
||||
private PropertiesWebClientCustomizer getPropertiesWebClientCustomizer(
|
||||
ReactiveHttpClientServiceProperties.Group groupProperties) {
|
||||
return new PropertiesWebClientCustomizer(this.apiVersionInserter, this.apiVersionFormatter, groupProperties,
|
||||
this.serviceProperties);
|
||||
}
|
||||
|
||||
private ClientHttpConnector getClientConnector(ReactiveHttpClientServiceProperties.Group groupProperties) {
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.boot.webclient.autoconfigure;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link PropertiesWebClientCustomizer}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PropertiesWebClientCustomizerTests {
|
||||
|
||||
@Test
|
||||
void customizeAppliesPropertiesInOrder() throws Exception {
|
||||
ApiVersionInserter delegateApiVersionInserter = ApiVersionInserter.useQueryParam("v");
|
||||
ApiVersionFormatter apiVersionFormatter = (version) -> String.valueOf(version).toUpperCase(Locale.ROOT);
|
||||
TestWebClientProperties properties1 = new TestWebClientProperties();
|
||||
properties1.setBaseUrl("https://example.com/b1");
|
||||
properties1.getDefaultHeader().put("x-h1", List.of("v1"));
|
||||
properties1.getApiversion().setDefaultVersion("dv1");
|
||||
properties1.getApiversion().getInsert().setQueryParameter("p1");
|
||||
TestWebClientProperties properties2 = new TestWebClientProperties();
|
||||
properties2.setBaseUrl("https://example.com/b2");
|
||||
properties1.getDefaultHeader().put("x-h2", List.of("v2"));
|
||||
properties2.getApiversion().setDefaultVersion("dv2");
|
||||
PropertiesWebClientCustomizer customizer = new PropertiesWebClientCustomizer(delegateApiVersionInserter,
|
||||
apiVersionFormatter, properties1, properties2);
|
||||
WebClient.Builder builder = WebClient.builder();
|
||||
customizer.customize(builder);
|
||||
WebClient client = builder.build();
|
||||
assertThat(client).extracting("defaultApiVersion").isEqualTo("dv1");
|
||||
UriBuilderFactory uriBuilderFactory = (UriBuilderFactory) ReflectionTestUtils.getField(client,
|
||||
"uriBuilderFactory");
|
||||
assertThat(uriBuilderFactory.builder().build()).hasToString("https://example.com/b1");
|
||||
HttpHeaders defaultHeaders = (HttpHeaders) ReflectionTestUtils.getField(client, "defaultHeaders");
|
||||
assertThat(defaultHeaders.get("x-h1")).containsExactly("v1");
|
||||
assertThat(defaultHeaders.get("x-h2")).containsExactly("v2");
|
||||
ApiVersionInserter apiVersionInserter = (ApiVersionInserter) ReflectionTestUtils.getField(client,
|
||||
"apiVersionInserter");
|
||||
assertThat(apiVersionInserter.insertVersion("v123", new URI("https://example.com")))
|
||||
.hasToString("https://example.com?v=v123&p1=V123");
|
||||
}
|
||||
|
||||
static class TestWebClientProperties extends AbstractWebClientProperties {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+67
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.boot.webclient.autoconfigure;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
@@ -26,6 +29,9 @@ import org.springframework.boot.webclient.WebClientCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.codec.CodecConfigurer;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.client.ApiVersionFormatter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -37,6 +43,7 @@ import static org.mockito.Mockito.mock;
|
||||
* Tests for {@link WebClientAutoConfiguration}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class WebClientAutoConfigurationTests {
|
||||
|
||||
@@ -102,6 +109,46 @@ class WebClientAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasApiVersionProperties() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.http.reactiveclient.webclient.apiversion.default=123",
|
||||
"spring.http.reactiveclient.webclient.apiversion.insert.query-parameter=version")
|
||||
.run((context) -> {
|
||||
WebClient webClient = context.getBean(WebClient.Builder.class).build();
|
||||
assertThat(webClient).extracting("defaultApiVersion").isEqualTo("123");
|
||||
ApiVersionInserter apiVersionInserter = (ApiVersionInserter) ReflectionTestUtils.getField(webClient,
|
||||
"apiVersionInserter");
|
||||
assertThat(apiVersionInserter.insertVersion("123", new URI("https://example.com")))
|
||||
.hasToString("https://example.com?version=123");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasCustomApiVersionInserter() {
|
||||
this.contextRunner.withUserConfiguration(ApiVersionInserterConfig.class).run((context) -> {
|
||||
WebClient webClient = context.getBean(WebClient.Builder.class).build();
|
||||
ApiVersionInserter apiVersionInserter = (ApiVersionInserter) ReflectionTestUtils.getField(webClient,
|
||||
"apiVersionInserter");
|
||||
assertThat(apiVersionInserter.insertVersion("123", new URI("https://example.com")))
|
||||
.hasToString("https://example.com?version=123");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasCustomApiVersionFormatter() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.http.reactiveclient.webclient.apiversion.insert.query-parameter=version")
|
||||
.withUserConfiguration(ApiVersionFormatterConfig.class)
|
||||
.run((context) -> {
|
||||
WebClient webClient = context.getBean(WebClient.Builder.class).build();
|
||||
ApiVersionInserter apiVersionInserter = (ApiVersionInserter) ReflectionTestUtils.getField(webClient,
|
||||
"apiVersionInserter");
|
||||
assertThat(apiVersionInserter.insertVersion("best", new URI("https://example.com")))
|
||||
.hasToString("https://example.com?version=BEST");
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CodecConfiguration {
|
||||
|
||||
@@ -136,4 +183,24 @@ class WebClientAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ApiVersionInserterConfig {
|
||||
|
||||
@Bean
|
||||
ApiVersionInserter apiVersionInserter() {
|
||||
return ApiVersionInserter.useQueryParam("version");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ApiVersionFormatterConfig {
|
||||
|
||||
@Bean
|
||||
ApiVersionFormatter apiVersionFormatter() {
|
||||
return (version) -> String.valueOf(version).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+50
-1
@@ -21,6 +21,7 @@ import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
@@ -42,12 +43,15 @@ import org.springframework.boot.autoconfigure.web.WebResourcesRuntimeHints;
|
||||
import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters;
|
||||
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.convert.ApplicationConversionService;
|
||||
import org.springframework.boot.http.codec.CodecCustomizer;
|
||||
import org.springframework.boot.http.codec.autoconfigure.CodecsAutoConfiguration;
|
||||
import org.springframework.boot.thread.Threading;
|
||||
import org.springframework.boot.validation.autoconfigure.ValidatorAdapter;
|
||||
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
|
||||
import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Apiversion;
|
||||
import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Apiversion.Use;
|
||||
import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Format;
|
||||
import org.springframework.boot.webflux.filter.OrderedHiddenHttpMethodFilter;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -64,7 +68,12 @@ import org.springframework.format.support.FormattingConversionService;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.accept.ApiVersionParser;
|
||||
import org.springframework.web.filter.reactive.HiddenHttpMethodFilter;
|
||||
import org.springframework.web.reactive.accept.ApiVersionDeprecationHandler;
|
||||
import org.springframework.web.reactive.accept.ApiVersionResolver;
|
||||
import org.springframework.web.reactive.accept.ApiVersionStrategy;
|
||||
import org.springframework.web.reactive.config.ApiVersionConfigurer;
|
||||
import org.springframework.web.reactive.config.BlockingExecutionConfigurer;
|
||||
import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
@@ -169,11 +178,19 @@ public final class WebFluxAutoConfiguration {
|
||||
|
||||
private final ObjectProvider<ViewResolver> viewResolvers;
|
||||
|
||||
private final ObjectProvider<ApiVersionResolver> apiVersionResolvers;
|
||||
|
||||
private final ObjectProvider<ApiVersionParser<?>> apiVersionParser;
|
||||
|
||||
private final ObjectProvider<ApiVersionDeprecationHandler> apiVersionDeprecationHandler;
|
||||
|
||||
WebFluxConfig(Environment environment, WebProperties webProperties, WebFluxProperties webFluxProperties,
|
||||
ListableBeanFactory beanFactory, ObjectProvider<HandlerMethodArgumentResolver> resolvers,
|
||||
ObjectProvider<CodecCustomizer> codecCustomizers,
|
||||
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizers,
|
||||
ObjectProvider<ViewResolver> viewResolvers) {
|
||||
ObjectProvider<ViewResolver> viewResolvers, ObjectProvider<ApiVersionResolver> apiVersionResolvers,
|
||||
ObjectProvider<ApiVersionParser<?>> apiVersionParser,
|
||||
ObjectProvider<ApiVersionDeprecationHandler> apiVersionDeprecationHandler) {
|
||||
this.environment = environment;
|
||||
this.resourceProperties = webProperties.getResources();
|
||||
this.webFluxProperties = webFluxProperties;
|
||||
@@ -182,6 +199,9 @@ public final class WebFluxAutoConfiguration {
|
||||
this.codecCustomizers = codecCustomizers;
|
||||
this.resourceHandlerRegistrationCustomizers = resourceHandlerRegistrationCustomizers;
|
||||
this.viewResolvers = viewResolvers;
|
||||
this.apiVersionResolvers = apiVersionResolvers;
|
||||
this.apiVersionParser = apiVersionParser;
|
||||
this.apiVersionDeprecationHandler = apiVersionDeprecationHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -252,6 +272,29 @@ public final class WebFluxAutoConfiguration {
|
||||
ApplicationConversionService.addBeans(registry, this.beanFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureApiVersioning(ApiVersionConfigurer configurer) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
Apiversion properties = this.webFluxProperties.getApiversion();
|
||||
map.from(properties::isRequired).to(configurer::setVersionRequired);
|
||||
map.from(properties::getDefaultVersion).to(configurer::setDefaultVersion);
|
||||
map.from(properties::getSupported).to((supported) -> supported.forEach(configurer::addSupportedVersions));
|
||||
map.from(properties::isDetectSupported).to(configurer::detectSupportedVersions);
|
||||
configureApiVersioningUse(configurer, properties.getUse());
|
||||
this.apiVersionResolvers.orderedStream().forEach(configurer::useVersionResolver);
|
||||
this.apiVersionParser.ifAvailable(configurer::setVersionParser);
|
||||
this.apiVersionDeprecationHandler.ifAvailable(configurer::setDeprecationHandler);
|
||||
}
|
||||
|
||||
private void configureApiVersioningUse(ApiVersionConfigurer configurer, Use use) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(use::getHeader).whenHasText().to(configurer::useRequestHeader);
|
||||
map.from(use::getRequestParameter).whenHasText().to(configurer::useRequestParam);
|
||||
map.from(use::getPathSegment).to(configurer::usePathSegment);
|
||||
use.getMediaTypeParameter()
|
||||
.forEach((mediaType, parameterName) -> configurer.useMediaTypeParameter(mediaType, parameterName));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,6 +390,12 @@ public final class WebFluxAutoConfiguration {
|
||||
return webSessionManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ConditionalOnMissingBean(name = "mvcApiVersionStrategy")
|
||||
public @Nullable ApiVersionStrategy mvcApiVersionStrategy() {
|
||||
return super.mvcApiVersionStrategy();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
||||
+134
@@ -16,7 +16,13 @@
|
||||
|
||||
package org.springframework.boot.webflux.autoconfigure;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.Name;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -38,6 +44,8 @@ public class WebFluxProperties {
|
||||
|
||||
private final Problemdetails problemdetails = new Problemdetails();
|
||||
|
||||
private final Apiversion apiversion = new Apiversion();
|
||||
|
||||
/**
|
||||
* Path pattern used for static resources.
|
||||
*/
|
||||
@@ -80,6 +88,10 @@ public class WebFluxProperties {
|
||||
return this.problemdetails;
|
||||
}
|
||||
|
||||
public Apiversion getApiversion() {
|
||||
return this.apiversion;
|
||||
}
|
||||
|
||||
public String getStaticPathPattern() {
|
||||
return this.staticPathPattern;
|
||||
}
|
||||
@@ -159,4 +171,126 @@ public class WebFluxProperties {
|
||||
|
||||
}
|
||||
|
||||
public static class Apiversion {
|
||||
|
||||
/**
|
||||
* Whether the API version is required with each request.
|
||||
*/
|
||||
private boolean required = false;
|
||||
|
||||
/**
|
||||
* Default version that should be used for each request.
|
||||
*/
|
||||
@Name("default")
|
||||
private String defaultVersion;
|
||||
|
||||
/**
|
||||
* Supported versions.
|
||||
*/
|
||||
private List<String> supported;
|
||||
|
||||
/**
|
||||
* Whether supported versions should be detected from controllers.
|
||||
*/
|
||||
private boolean detectSupported = true;
|
||||
|
||||
/**
|
||||
* How version details should be inserted into requests.
|
||||
*/
|
||||
private final Use use = new Use();
|
||||
|
||||
public boolean isRequired() {
|
||||
return this.required;
|
||||
}
|
||||
|
||||
public void setRequired(boolean required) {
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
public String getDefaultVersion() {
|
||||
return this.defaultVersion;
|
||||
}
|
||||
|
||||
public void setDefaultVersion(String defaultVersion) {
|
||||
this.defaultVersion = defaultVersion;
|
||||
}
|
||||
|
||||
public List<String> getSupported() {
|
||||
return this.supported;
|
||||
}
|
||||
|
||||
public void setSupported(List<String> supported) {
|
||||
this.supported = supported;
|
||||
}
|
||||
|
||||
public Use getUse() {
|
||||
return this.use;
|
||||
}
|
||||
|
||||
public boolean isDetectSupported() {
|
||||
return this.detectSupported;
|
||||
}
|
||||
|
||||
public void setDetectSupported(boolean detectSupported) {
|
||||
this.detectSupported = detectSupported;
|
||||
}
|
||||
|
||||
public static class Use {
|
||||
|
||||
/**
|
||||
* Use the HTTP header with the given name to obtain the version.
|
||||
*/
|
||||
private String header;
|
||||
|
||||
/**
|
||||
* Use the query parameter with the given name to obtain the version.
|
||||
*/
|
||||
private String requestParameter;
|
||||
|
||||
/**
|
||||
* Use the path segment at the given index to obtain the version.
|
||||
*/
|
||||
private Integer pathSegment;
|
||||
|
||||
/**
|
||||
* Use the media type parameter with the given name to obtain the version.
|
||||
*/
|
||||
private Map<MediaType, String> mediaTypeParameter = new LinkedHashMap<>();
|
||||
|
||||
public String getHeader() {
|
||||
return this.header;
|
||||
}
|
||||
|
||||
public void setHeader(String header) {
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
public String getRequestParameter() {
|
||||
return this.requestParameter;
|
||||
}
|
||||
|
||||
public void setRequestParameter(String queryParameter) {
|
||||
this.requestParameter = queryParameter;
|
||||
}
|
||||
|
||||
public Integer getPathSegment() {
|
||||
return this.pathSegment;
|
||||
}
|
||||
|
||||
public void setPathSegment(Integer pathSegment) {
|
||||
this.pathSegment = pathSegment;
|
||||
}
|
||||
|
||||
public Map<MediaType, String> getMediaTypeParameter() {
|
||||
return this.mediaTypeParameter;
|
||||
}
|
||||
|
||||
public void setMediaTypeParameter(Map<MediaType, String> mediaTypeParameter) {
|
||||
this.mediaTypeParameter = mediaTypeParameter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+127
@@ -85,11 +85,18 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
import org.springframework.web.accept.ApiVersionParser;
|
||||
import org.springframework.web.accept.InvalidApiVersionException;
|
||||
import org.springframework.web.accept.MissingApiVersionException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.filter.reactive.HiddenHttpMethodFilter;
|
||||
import org.springframework.web.method.ControllerAdviceBean;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.accept.ApiVersionDeprecationHandler;
|
||||
import org.springframework.web.reactive.accept.ApiVersionResolver;
|
||||
import org.springframework.web.reactive.accept.DefaultApiVersionStrategy;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||
import org.springframework.web.reactive.accept.StandardApiVersionDeprecationHandler;
|
||||
import org.springframework.web.reactive.config.BlockingExecutionConfigurer;
|
||||
import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration;
|
||||
import org.springframework.web.reactive.config.ResourceHandlerRegistration;
|
||||
@@ -122,6 +129,7 @@ import org.springframework.web.server.session.WebSessionStore;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -792,6 +800,105 @@ class WebFluxAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionPropertiesAreApplied() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.webflux.apiversion.use.header=version",
|
||||
"spring.webflux.apiversion.required=true", "spring.webflux.apiversion.supported=123,456",
|
||||
"spring.webflux.apiversion.detect-supported=false")
|
||||
.run((context) -> {
|
||||
DefaultApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy",
|
||||
DefaultApiVersionStrategy.class);
|
||||
MockServerWebExchange request = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("https://example.com"));
|
||||
assertThatExceptionOfType(MissingApiVersionException.class)
|
||||
.isThrownBy(() -> versionStrategy.validateVersion(null, request));
|
||||
assertThatExceptionOfType(InvalidApiVersionException.class)
|
||||
.isThrownBy(() -> versionStrategy.validateVersion(versionStrategy.parseVersion("789"),
|
||||
MockServerWebExchange.from(MockServerHttpRequest.get("https://example.com"))));
|
||||
assertThat(versionStrategy.detectSupportedVersions()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionDefaultVersionPropertyIsApplied() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.webflux.apiversion.use.header=version",
|
||||
"spring.webflux.apiversion.default=1.0.0")
|
||||
.run((context) -> {
|
||||
DefaultApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy",
|
||||
DefaultApiVersionStrategy.class);
|
||||
MockServerWebExchange request = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("https://example.com"));
|
||||
versionStrategy.addSupportedVersion("1.0.0");
|
||||
Comparable<?> version = versionStrategy.parseVersion("1.0.0");
|
||||
assertThat(versionStrategy.getDefaultVersion()).isEqualTo(version);
|
||||
versionStrategy.validateVersion(version, request);
|
||||
versionStrategy.validateVersion(null, request);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionUseHeaderPropertyIsApplied() {
|
||||
this.contextRunner.withPropertyValues("spring.webflux.apiversion.use.header=hv").run((context) -> {
|
||||
DefaultApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy",
|
||||
DefaultApiVersionStrategy.class);
|
||||
MockServerWebExchange request = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("https://example.com").header("hv", "123"));
|
||||
assertThat(versionStrategy.resolveVersion(request)).isEqualTo("123");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionUseRequestParameterPropertyIsApplied() {
|
||||
this.contextRunner.withPropertyValues("spring.webflux.apiversion.use.request-parameter=rpv").run((context) -> {
|
||||
DefaultApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy",
|
||||
DefaultApiVersionStrategy.class);
|
||||
MockServerWebExchange request = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("https://example.com?rpv=123"));
|
||||
assertThat(versionStrategy.resolveVersion(request)).isEqualTo("123");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionUsePathSegmentPropertyIsApplied() {
|
||||
this.contextRunner.withPropertyValues("spring.webflux.apiversion.use.path-segment=1").run((context) -> {
|
||||
DefaultApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy",
|
||||
DefaultApiVersionStrategy.class);
|
||||
MockServerWebExchange request = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("https://example.com/test/123"));
|
||||
assertThat(versionStrategy.resolveVersion(request)).isEqualTo("123");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionUseMediaTypeParameterPropertyIsApplied() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.webflux.apiversion.use.media-type-parameter[application/json]=mtpv")
|
||||
.run((context) -> {
|
||||
DefaultApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy",
|
||||
DefaultApiVersionStrategy.class);
|
||||
MockServerWebExchange request = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("https://example.com")
|
||||
.header("content-type", "application/json;mtpv=123"));
|
||||
assertThat(versionStrategy.resolveVersion(request)).isEqualTo("123");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionBeansAreInjected() {
|
||||
this.contextRunner.withUserConfiguration(ApiVersionConfiguration.class).run((context) -> {
|
||||
DefaultApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy",
|
||||
DefaultApiVersionStrategy.class);
|
||||
assertThat(versionStrategy).extracting("versionResolvers")
|
||||
.asInstanceOf(InstanceOfAssertFactories.LIST)
|
||||
.containsExactly(context.getBean(ApiVersionResolver.class));
|
||||
assertThat(versionStrategy).extracting("deprecationHandler")
|
||||
.isEqualTo(context.getBean(ApiVersionDeprecationHandler.class));
|
||||
assertThat(versionStrategy).extracting("versionParser").isEqualTo(context.getBean(ApiVersionParser.class));
|
||||
});
|
||||
}
|
||||
|
||||
private ContextConsumer<ReactiveWebApplicationContext> assertExchangeWithSession(
|
||||
Consumer<MockServerWebExchange> exchange) {
|
||||
return (context) -> {
|
||||
@@ -1187,4 +1294,24 @@ class WebFluxAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ApiVersionConfiguration {
|
||||
|
||||
@Bean
|
||||
ApiVersionResolver apiVersionResolver() {
|
||||
return (request) -> "latest";
|
||||
}
|
||||
|
||||
@Bean
|
||||
ApiVersionDeprecationHandler apiVersionDeprecationHandler(ApiVersionParser<?> apiVersionParser) {
|
||||
return new StandardApiVersionDeprecationHandler(apiVersionParser);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ApiVersionParser<String> apiVersionParser() {
|
||||
return (version) -> String.valueOf(version);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+50
-1
@@ -50,6 +50,7 @@ import org.springframework.boot.autoconfigure.web.WebResourcesRuntimeHints;
|
||||
import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters;
|
||||
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.convert.ApplicationConversionService;
|
||||
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
|
||||
import org.springframework.boot.servlet.filter.OrderedFormContentFilter;
|
||||
@@ -57,6 +58,8 @@ import org.springframework.boot.servlet.filter.OrderedHiddenHttpMethodFilter;
|
||||
import org.springframework.boot.servlet.filter.OrderedRequestContextFilter;
|
||||
import org.springframework.boot.validation.autoconfigure.ValidatorAdapter;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Apiversion;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Apiversion.Use;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Format;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
@@ -79,6 +82,10 @@ import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.validation.DefaultMessageCodesResolver;
|
||||
import org.springframework.validation.MessageCodesResolver;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.accept.ApiVersionDeprecationHandler;
|
||||
import org.springframework.web.accept.ApiVersionParser;
|
||||
import org.springframework.web.accept.ApiVersionResolver;
|
||||
import org.springframework.web.accept.ApiVersionStrategy;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
||||
import org.springframework.web.context.ServletContextAware;
|
||||
@@ -94,6 +101,7 @@ import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.RequestToViewNameTranslator;
|
||||
import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;
|
||||
@@ -197,11 +205,20 @@ public final class WebMvcAutoConfiguration {
|
||||
|
||||
private ServletContext servletContext;
|
||||
|
||||
private final ObjectProvider<ApiVersionResolver> apiVersionResolvers;
|
||||
|
||||
private final ObjectProvider<ApiVersionParser<?>> apiVersionParser;
|
||||
|
||||
private final ObjectProvider<ApiVersionDeprecationHandler> apiVersionDeprecationHandler;
|
||||
|
||||
WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties,
|
||||
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
|
||||
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
|
||||
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
|
||||
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
|
||||
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations,
|
||||
ObjectProvider<ApiVersionResolver> apiVersionResolvers,
|
||||
ObjectProvider<ApiVersionParser<?>> apiVersionParser,
|
||||
ObjectProvider<ApiVersionDeprecationHandler> apiVersionDeprecationHandler) {
|
||||
this.resourceProperties = webProperties.getResources();
|
||||
this.mvcProperties = mvcProperties;
|
||||
this.beanFactory = beanFactory;
|
||||
@@ -209,6 +226,9 @@ public final class WebMvcAutoConfiguration {
|
||||
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
|
||||
this.dispatcherServletPath = dispatcherServletPath;
|
||||
this.servletRegistrations = servletRegistrations;
|
||||
this.apiVersionResolvers = apiVersionResolvers;
|
||||
this.apiVersionParser = apiVersionParser;
|
||||
this.apiVersionDeprecationHandler = apiVersionDeprecationHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -366,6 +386,29 @@ public final class WebMvcAutoConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureApiVersioning(ApiVersionConfigurer configurer) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
Apiversion properties = this.mvcProperties.getApiversion();
|
||||
map.from(properties::isRequired).to(configurer::setVersionRequired);
|
||||
map.from(properties::getDefaultVersion).to(configurer::setDefaultVersion);
|
||||
map.from(properties::getSupported).to((supported) -> supported.forEach(configurer::addSupportedVersions));
|
||||
map.from(properties::isDetectSupported).to(configurer::detectSupportedVersions);
|
||||
configureApiVersioningUse(configurer, properties.getUse());
|
||||
this.apiVersionResolvers.orderedStream().forEach(configurer::useVersionResolver);
|
||||
this.apiVersionParser.ifAvailable(configurer::setVersionParser);
|
||||
this.apiVersionDeprecationHandler.ifAvailable(configurer::setDeprecationHandler);
|
||||
}
|
||||
|
||||
private void configureApiVersioningUse(ApiVersionConfigurer configurer, Use use) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(use::getHeader).whenHasText().to(configurer::useRequestHeader);
|
||||
map.from(use::getRequestParameter).whenHasText().to(configurer::useRequestParam);
|
||||
map.from(use::getPathSegment).to(configurer::usePathSegment);
|
||||
use.getMediaTypeParameter()
|
||||
.forEach((mediaType, parameterName) -> configurer.useMediaTypeParameter(mediaType, parameterName));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean({ RequestContextListener.class, RequestContextFilter.class })
|
||||
@ConditionalOnMissingFilterBean
|
||||
@@ -575,6 +618,12 @@ public final class WebMvcAutoConfiguration {
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ConditionalOnMissingBean(name = "mvcApiVersionStrategy")
|
||||
public ApiVersionStrategy mvcApiVersionStrategy() {
|
||||
return super.mvcApiVersionStrategy();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
||||
+135
@@ -23,6 +23,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.Name;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.validation.DefaultMessageCodesResolver;
|
||||
@@ -97,6 +98,8 @@ public class WebMvcProperties {
|
||||
|
||||
private final Problemdetails problemdetails = new Problemdetails();
|
||||
|
||||
private final Apiversion apiversion = new Apiversion();
|
||||
|
||||
public DefaultMessageCodesResolver.Format getMessageCodesResolverFormat() {
|
||||
return this.messageCodesResolverFormat;
|
||||
}
|
||||
@@ -189,6 +192,10 @@ public class WebMvcProperties {
|
||||
return this.problemdetails;
|
||||
}
|
||||
|
||||
public Apiversion getApiversion() {
|
||||
return this.apiversion;
|
||||
}
|
||||
|
||||
public static class Async {
|
||||
|
||||
/**
|
||||
@@ -442,6 +449,9 @@ public class WebMvcProperties {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Problem Details.
|
||||
*/
|
||||
public static class Problemdetails {
|
||||
|
||||
/**
|
||||
@@ -459,4 +469,129 @@ public class WebMvcProperties {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* API Version.
|
||||
*/
|
||||
public static class Apiversion {
|
||||
|
||||
/**
|
||||
* Whether the API version is required with each request.
|
||||
*/
|
||||
private boolean required = false;
|
||||
|
||||
/**
|
||||
* Default version that should be used for each request.
|
||||
*/
|
||||
@Name("default")
|
||||
private String defaultVersion;
|
||||
|
||||
/**
|
||||
* Supported versions.
|
||||
*/
|
||||
private List<String> supported;
|
||||
|
||||
/**
|
||||
* Whether supported versions should be detected from controllers.
|
||||
*/
|
||||
private boolean detectSupported = true;
|
||||
|
||||
/**
|
||||
* How version details should be inserted into requests.
|
||||
*/
|
||||
private final Use use = new Use();
|
||||
|
||||
public boolean isRequired() {
|
||||
return this.required;
|
||||
}
|
||||
|
||||
public void setRequired(boolean required) {
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
public String getDefaultVersion() {
|
||||
return this.defaultVersion;
|
||||
}
|
||||
|
||||
public void setDefaultVersion(String defaultVersion) {
|
||||
this.defaultVersion = defaultVersion;
|
||||
}
|
||||
|
||||
public List<String> getSupported() {
|
||||
return this.supported;
|
||||
}
|
||||
|
||||
public void setSupported(List<String> supported) {
|
||||
this.supported = supported;
|
||||
}
|
||||
|
||||
public Use getUse() {
|
||||
return this.use;
|
||||
}
|
||||
|
||||
public boolean isDetectSupported() {
|
||||
return this.detectSupported;
|
||||
}
|
||||
|
||||
public void setDetectSupported(boolean detectSupported) {
|
||||
this.detectSupported = detectSupported;
|
||||
}
|
||||
|
||||
public static class Use {
|
||||
|
||||
/**
|
||||
* Use the HTTP header with the given name to obtain the version.
|
||||
*/
|
||||
private String header;
|
||||
|
||||
/**
|
||||
* Use the query parameter with the given name to obtain the version.
|
||||
*/
|
||||
private String requestParameter;
|
||||
|
||||
/**
|
||||
* Use the path segment at the given index to obtain the version.
|
||||
*/
|
||||
private Integer pathSegment;
|
||||
|
||||
/**
|
||||
* Use the media type parameter with the given name to obtain the version.
|
||||
*/
|
||||
private Map<MediaType, String> mediaTypeParameter = new LinkedHashMap<>();
|
||||
|
||||
public String getHeader() {
|
||||
return this.header;
|
||||
}
|
||||
|
||||
public void setHeader(String header) {
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
public String getRequestParameter() {
|
||||
return this.requestParameter;
|
||||
}
|
||||
|
||||
public void setRequestParameter(String queryParameter) {
|
||||
this.requestParameter = queryParameter;
|
||||
}
|
||||
|
||||
public Integer getPathSegment() {
|
||||
return this.pathSegment;
|
||||
}
|
||||
|
||||
public void setPathSegment(Integer pathSegment) {
|
||||
this.pathSegment = pathSegment;
|
||||
}
|
||||
|
||||
public Map<MediaType, String> getMediaTypeParameter() {
|
||||
return this.mediaTypeParameter;
|
||||
}
|
||||
|
||||
public void setMediaTypeParameter(Map<MediaType, String> mediaTypeParameter) {
|
||||
this.mediaTypeParameter = mediaTypeParameter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+117
@@ -83,14 +83,23 @@ import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.RequestPath;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
import org.springframework.web.accept.ApiVersionDeprecationHandler;
|
||||
import org.springframework.web.accept.ApiVersionParser;
|
||||
import org.springframework.web.accept.ApiVersionResolver;
|
||||
import org.springframework.web.accept.ApiVersionStrategy;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
import org.springframework.web.accept.DefaultApiVersionStrategy;
|
||||
import org.springframework.web.accept.FixedContentNegotiationStrategy;
|
||||
import org.springframework.web.accept.InvalidApiVersionException;
|
||||
import org.springframework.web.accept.MissingApiVersionException;
|
||||
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
|
||||
import org.springframework.web.accept.StandardApiVersionDeprecationHandler;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
@@ -141,9 +150,11 @@ import org.springframework.web.servlet.support.SessionFlashMapManager;
|
||||
import org.springframework.web.servlet.view.AbstractView;
|
||||
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
|
||||
import org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
import org.springframework.web.util.UrlPathHelper;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -1007,6 +1018,92 @@ class WebMvcAutoConfigurationTests {
|
||||
OrderedControllerAdviceBeansConfiguration.LowestOrderedControllerAdvice.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionPropertiesAreApplied() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.mvc.apiversion.use.header=version", "spring.mvc.apiversion.required=true",
|
||||
"spring.mvc.apiversion.supported=123,456", "spring.mvc.apiversion.detect-supported=false")
|
||||
.run((context) -> {
|
||||
DefaultApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy",
|
||||
DefaultApiVersionStrategy.class);
|
||||
assertThatExceptionOfType(MissingApiVersionException.class)
|
||||
.isThrownBy(() -> versionStrategy.validateVersion(null, new MockHttpServletRequest()));
|
||||
assertThatExceptionOfType(InvalidApiVersionException.class).isThrownBy(() -> versionStrategy
|
||||
.validateVersion(versionStrategy.parseVersion("789"), new MockHttpServletRequest()));
|
||||
assertThat(versionStrategy.detectSupportedVersions()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionDefaultVersionPropertyIsApplied() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.mvc.apiversion.use.header=version", "spring.mvc.apiversion.default=1.0.0")
|
||||
.run((context) -> {
|
||||
DefaultApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy",
|
||||
DefaultApiVersionStrategy.class);
|
||||
versionStrategy.addSupportedVersion("1.0.0");
|
||||
Comparable<?> version = versionStrategy.parseVersion("1.0.0");
|
||||
assertThat(versionStrategy.getDefaultVersion()).isEqualTo(version);
|
||||
versionStrategy.validateVersion(version, new MockHttpServletRequest());
|
||||
versionStrategy.validateVersion(null, new MockHttpServletRequest());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionUseHeaderPropertyIsApplied() {
|
||||
this.contextRunner.withPropertyValues("spring.mvc.apiversion.use.header=hv").run((context) -> {
|
||||
ApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy", ApiVersionStrategy.class);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("hv", "123");
|
||||
assertThat(versionStrategy.resolveVersion(request)).isEqualTo("123");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionUseRequestParameterPropertyIsApplied() {
|
||||
this.contextRunner.withPropertyValues("spring.mvc.apiversion.use.request-parameter=rpv").run((context) -> {
|
||||
ApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy", ApiVersionStrategy.class);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addParameter("rpv", "123");
|
||||
assertThat(versionStrategy.resolveVersion(request)).isEqualTo("123");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionUsePathSegmentPropertyIsApplied() {
|
||||
this.contextRunner.withPropertyValues("spring.mvc.apiversion.use.path-segment=1").run((context) -> {
|
||||
ApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy", ApiVersionStrategy.class);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "https://example.com/test/123");
|
||||
ServletRequestPathUtils.setParsedRequestPath(RequestPath.parse("/test/123", "/"), request);
|
||||
assertThat(versionStrategy.resolveVersion(request)).isEqualTo("123");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionUseMediaTypeParameterPropertyIsApplied() {
|
||||
this.contextRunner.withPropertyValues("spring.mvc.apiversion.use.media-type-parameter[application/json]=mtpv")
|
||||
.run((context) -> {
|
||||
ApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy", ApiVersionStrategy.class);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(HttpHeaders.CONTENT_TYPE, "application/json;mtpv=123");
|
||||
assertThat(versionStrategy.resolveVersion(request)).isEqualTo("123");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiVersionBeansAreInjected() {
|
||||
this.contextRunner.withUserConfiguration(ApiVersionConfiguration.class).run((context) -> {
|
||||
DefaultApiVersionStrategy versionStrategy = context.getBean("mvcApiVersionStrategy",
|
||||
DefaultApiVersionStrategy.class);
|
||||
assertThat(versionStrategy).extracting("versionResolvers")
|
||||
.asInstanceOf(InstanceOfAssertFactories.LIST)
|
||||
.containsExactly(context.getBean(ApiVersionResolver.class));
|
||||
assertThat(versionStrategy).extracting("deprecationHandler")
|
||||
.isEqualTo(context.getBean(ApiVersionDeprecationHandler.class));
|
||||
assertThat(versionStrategy).extracting("versionParser").isEqualTo(context.getBean(ApiVersionParser.class));
|
||||
});
|
||||
}
|
||||
|
||||
private void assertResourceHttpRequestHandler(AssertableWebApplicationContext context,
|
||||
Consumer<ResourceHttpRequestHandler> handlerConsumer) {
|
||||
Map<String, Object> handlerMap = getHandlerMap(context.getBean("resourceHandlerMapping", HandlerMapping.class));
|
||||
@@ -1568,4 +1665,24 @@ class WebMvcAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ApiVersionConfiguration {
|
||||
|
||||
@Bean
|
||||
ApiVersionResolver apiVersionResolver() {
|
||||
return (request) -> "latest";
|
||||
}
|
||||
|
||||
@Bean
|
||||
ApiVersionDeprecationHandler apiVersionDeprecationHandler(ApiVersionParser<?> apiVersionParser) {
|
||||
return new StandardApiVersionDeprecationHandler(apiVersionParser);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ApiVersionParser<String> apiVersionParser() {
|
||||
return (version) -> String.valueOf(version);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user