Skip to content

Commit 31cffc7

Browse files
authored
Initial support for API Versioning predicate (#3865)
Initial support for API Versioning predicate Supports, header, request param, path segment, and media type version resol.vers. Also adds support for optional ApiVersionResolver, ApiVersionDeprecationHandler, and ApiVersionParser beans Only configure APIVersionStrategy if version resolvers are configured.
1 parent 093f405 commit 31cffc7

File tree

9 files changed

+869
-2
lines changed

9 files changed

+869
-2
lines changed

spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.security.KeyStoreException;
2323
import java.security.NoSuchAlgorithmException;
2424
import java.security.cert.CertificateException;
25+
import java.util.ArrayList;
2526
import java.util.List;
2627
import java.util.Set;
2728
import java.util.function.Supplier;
@@ -31,6 +32,7 @@
3132
import io.github.bucket4j.distributed.proxy.AsyncProxyManager;
3233
import org.apache.commons.logging.Log;
3334
import org.apache.commons.logging.LogFactory;
35+
import org.jspecify.annotations.Nullable;
3436
import reactor.core.publisher.Flux;
3537
import reactor.netty.http.client.HttpClient;
3638
import reactor.netty.http.client.WebsocketClientSpec;
@@ -40,9 +42,11 @@
4042
import org.springframework.aot.hint.RuntimeHints;
4143
import org.springframework.aot.hint.RuntimeHintsRegistrar;
4244
import org.springframework.aot.hint.TypeReference;
45+
import org.springframework.beans.BeansException;
4346
import org.springframework.beans.factory.BeanFactory;
4447
import org.springframework.beans.factory.ObjectProvider;
4548
import org.springframework.beans.factory.annotation.Qualifier;
49+
import org.springframework.beans.factory.config.BeanPostProcessor;
4650
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
4751
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
4852
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
@@ -151,6 +155,7 @@
151155
import org.springframework.cloud.gateway.handler.predicate.ReadBodyRoutePredicateFactory;
152156
import org.springframework.cloud.gateway.handler.predicate.RemoteAddrRoutePredicateFactory;
153157
import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory;
158+
import org.springframework.cloud.gateway.handler.predicate.VersionRoutePredicateFactory;
154159
import org.springframework.cloud.gateway.handler.predicate.WeightRoutePredicateFactory;
155160
import org.springframework.cloud.gateway.handler.predicate.XForwardedRemoteAddrRoutePredicateFactory;
156161
import org.springframework.cloud.gateway.route.CachingRouteLocator;
@@ -165,6 +170,7 @@
165170
import org.springframework.cloud.gateway.route.RouteRefreshListener;
166171
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
167172
import org.springframework.cloud.gateway.support.ConfigurationService;
173+
import org.springframework.cloud.gateway.support.GatewayApiVersionStrategy;
168174
import org.springframework.cloud.gateway.support.StringToZonedDateTimeConverter;
169175
import org.springframework.cloud.gateway.support.config.KeyValueConverter;
170176
import org.springframework.context.ApplicationEventPublisher;
@@ -182,14 +188,25 @@
182188
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
183189
import org.springframework.security.web.server.SecurityWebFilterChain;
184190
import org.springframework.util.ClassUtils;
191+
import org.springframework.util.StringUtils;
185192
import org.springframework.validation.Validator;
193+
import org.springframework.web.accept.ApiVersionParser;
194+
import org.springframework.web.accept.SemanticApiVersionParser;
186195
import org.springframework.web.reactive.DispatcherHandler;
196+
import org.springframework.web.reactive.accept.ApiVersionDeprecationHandler;
197+
import org.springframework.web.reactive.accept.ApiVersionResolver;
198+
import org.springframework.web.reactive.accept.ApiVersionStrategy;
199+
import org.springframework.web.reactive.accept.MediaTypeParamApiVersionResolver;
200+
import org.springframework.web.reactive.accept.PathApiVersionResolver;
201+
import org.springframework.web.reactive.config.ApiVersionConfigurer;
202+
import org.springframework.web.reactive.config.WebFluxConfigurer;
187203
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
188204
import org.springframework.web.reactive.socket.client.WebSocketClient;
189205
import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy;
190206
import org.springframework.web.reactive.socket.server.WebSocketService;
191207
import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService;
192208
import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy;
209+
import org.springframework.web.server.ServerWebExchange;
193210

194211
/**
195212
* @author Spencer Gibb
@@ -299,18 +316,23 @@ public RoutePredicateHandlerMapping routePredicateHandlerMapping(FilteringWebHan
299316
return new RoutePredicateHandlerMapping(webHandler, routeLocator, globalCorsProperties, environment);
300317
}
301318

319+
// ConfigurationProperty beans
320+
302321
@Bean
303322
public GatewayProperties gatewayProperties() {
304323
return new GatewayProperties();
305324
}
306325

307-
// ConfigurationProperty beans
308-
309326
@Bean
310327
public SecureHeadersProperties secureHeadersProperties() {
311328
return new SecureHeadersProperties();
312329
}
313330

331+
@Bean
332+
public VersionProperties versionProperties() {
333+
return new VersionProperties();
334+
}
335+
314336
@Bean
315337
@Conditional(TrustedProxies.ForwardedTrustedProxiesCondition.class)
316338
public ForwardedHeadersFilter forwardedHeadersFilter(Environment env, ServerProperties serverProperties,
@@ -499,6 +521,13 @@ public RemoteAddrRoutePredicateFactory remoteAddrRoutePredicateFactory() {
499521
return new RemoteAddrRoutePredicateFactory();
500522
}
501523

524+
@Bean
525+
@ConditionalOnEnabledPredicate
526+
public VersionRoutePredicateFactory versionRoutePredicateFactory(
527+
@Qualifier("mvcApiVersionStrategy") @Nullable ApiVersionStrategy apiVersionStrategy) {
528+
return new VersionRoutePredicateFactory(apiVersionStrategy);
529+
}
530+
502531
@Bean
503532
@ConditionalOnEnabledPredicate
504533
public XForwardedRemoteAddrRoutePredicateFactory xForwardedRemoteAddrRoutePredicateFactory() {
@@ -746,6 +775,16 @@ public GzipMessageBodyResolver gzipMessageBodyResolver() {
746775
return new GzipMessageBodyResolver();
747776
}
748777

778+
@Bean
779+
public GatewayServerWebfluxBeanPostProcessor gatewayServerWebfluxBeanPostProcessor(
780+
VersionProperties versionProperties,
781+
ObjectProvider<ApiVersionDeprecationHandler> deprecationHandlerProvider,
782+
ObjectProvider<ApiVersionParser<?>> versionParserProvider,
783+
ObjectProvider<ApiVersionResolver> versionResolvers) {
784+
return new GatewayServerWebfluxBeanPostProcessor(versionProperties, deprecationHandlerProvider.getIfAvailable(),
785+
versionParserProvider.getIfAvailable(), versionResolvers.orderedStream().toList());
786+
}
787+
749788
@Bean
750789
static ConfigurableHintsRegistrationProcessor configurableHintsRegistrationProcessor() {
751790
return new ConfigurableHintsRegistrationProcessor();
@@ -924,6 +963,100 @@ public TokenRelayGatewayFilterFactory tokenRelayGatewayFilterFactory(
924963

925964
}
926965

966+
// FIXME: without adding a version resolver, things fail until I can replace
967+
// ApiVersionStrategy in a bean post processor
968+
@Configuration(proxyBeanMethods = false)
969+
protected static class ApiVersionConfiguration implements WebFluxConfigurer {
970+
971+
private final VersionProperties versionProperties;
972+
973+
protected ApiVersionConfiguration(VersionProperties versionProperties) {
974+
this.versionProperties = versionProperties;
975+
}
976+
977+
@Override
978+
public void configureApiVersioning(ApiVersionConfigurer configurer) {
979+
if (StringUtils.hasText(versionProperties.getHeaderName())
980+
|| (versionProperties.getMediaType() != null
981+
&& StringUtils.hasText(versionProperties.getMediaTypeParamName()))
982+
|| versionProperties.getPathSegment() != null
983+
|| StringUtils.hasText(versionProperties.getRequestParamName())) {
984+
// only add if version resolver configured
985+
configurer.useVersionResolver(new ApiVersionResolver() {
986+
@Override
987+
public @Nullable String resolveVersion(ServerWebExchange exchange) {
988+
return null;
989+
}
990+
});
991+
}
992+
}
993+
994+
}
995+
996+
protected static class GatewayServerWebfluxBeanPostProcessor implements BeanPostProcessor {
997+
998+
private final VersionProperties versionProperties;
999+
1000+
private final ApiVersionDeprecationHandler deprecationHandler;
1001+
1002+
private final ApiVersionParser<?> versionParser;
1003+
1004+
private final List<ApiVersionResolver> apiVersionResolvers;
1005+
1006+
public GatewayServerWebfluxBeanPostProcessor(VersionProperties versionProperties,
1007+
ApiVersionDeprecationHandler deprecationHandler, ApiVersionParser<?> versionParser,
1008+
List<ApiVersionResolver> apiVersionResolvers) {
1009+
this.versionProperties = versionProperties;
1010+
this.deprecationHandler = deprecationHandler;
1011+
this.versionParser = (versionParser != null) ? versionParser : new SemanticApiVersionParser();
1012+
this.apiVersionResolvers = apiVersionResolvers;
1013+
}
1014+
1015+
@Override
1016+
public @Nullable Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
1017+
1018+
// TODO: Use custom ApiVersionConfigurer when able to
1019+
if (bean instanceof ApiVersionStrategy && beanName.equals("mvcApiVersionStrategy")) {
1020+
List<ApiVersionResolver> versionResolvers = new ArrayList<>();
1021+
if (StringUtils.hasText(versionProperties.getHeaderName())) {
1022+
versionResolvers.add(
1023+
exchange -> exchange.getRequest().getHeaders().getFirst(versionProperties.getHeaderName()));
1024+
}
1025+
if (versionProperties.getMediaType() != null
1026+
&& StringUtils.hasText(versionProperties.getMediaTypeParamName())) {
1027+
versionResolvers.add(new MediaTypeParamApiVersionResolver(versionProperties.getMediaType(),
1028+
versionProperties.getMediaTypeParamName()));
1029+
}
1030+
if (versionProperties.getPathSegment() != null) {
1031+
versionResolvers.add(new PathApiVersionResolver(versionProperties.getPathSegment()));
1032+
}
1033+
if (StringUtils.hasText(versionProperties.getRequestParamName())) {
1034+
versionResolvers.add(exchange -> exchange.getRequest()
1035+
.getQueryParams()
1036+
.getFirst(versionProperties.getRequestParamName()));
1037+
}
1038+
1039+
if (apiVersionResolvers != null && !apiVersionResolvers.isEmpty()) {
1040+
versionResolvers.addAll(apiVersionResolvers);
1041+
}
1042+
1043+
if (versionResolvers.isEmpty()) {
1044+
return bean;
1045+
}
1046+
1047+
GatewayApiVersionStrategy strategy = new GatewayApiVersionStrategy(versionResolvers, versionParser,
1048+
versionProperties.isRequired(), versionProperties.getDefaultVersion(),
1049+
versionProperties.isDetectSupportedVersions(), deprecationHandler);
1050+
if (!versionProperties.getSupportedVersions().isEmpty()) {
1051+
strategy.addSupportedVersion(versionProperties.getSupportedVersions().toArray(new String[0]));
1052+
}
1053+
return strategy;
1054+
}
1055+
return bean;
1056+
}
1057+
1058+
}
1059+
9271060
}
9281061

9291062
class GatewayHints implements RuntimeHintsRegistrar {
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright 2013-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.config;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import org.springframework.boot.context.properties.ConfigurationProperties;
23+
import org.springframework.core.style.ToStringCreator;
24+
import org.springframework.http.MediaType;
25+
import org.springframework.validation.annotation.Validated;
26+
27+
/**
28+
* Configuration properties for Spring Framework API Version strategy.
29+
*/
30+
@ConfigurationProperties(GatewayProperties.PREFIX + ".version")
31+
@Validated
32+
public class VersionProperties {
33+
34+
/** The defaultVersion. */
35+
private String defaultVersion;
36+
37+
/**
38+
* Flag whether to use API versions that appear in mappings for supported version
39+
* validation (true), or use only explicitly configured versions (false). Defaults to
40+
* true.
41+
*/
42+
private boolean detectSupportedVersions = true;
43+
44+
/** The header name used to extract the API Version. */
45+
private String headerName;
46+
47+
/** The media type name used to extract the API Version. */
48+
private MediaType mediaType;
49+
50+
/** The media type parameter name used to extract the API Version. */
51+
private String mediaTypeParamName;
52+
53+
/** The index of a path segment used to extract the API Version. */
54+
private Integer pathSegment;
55+
56+
/** The request parameter name used to extract the API Version. */
57+
private String requestParamName;
58+
59+
private boolean required;
60+
61+
private List<String> supportedVersions = new ArrayList<>();
62+
63+
public String getDefaultVersion() {
64+
return defaultVersion;
65+
}
66+
67+
public void setDefaultVersion(String defaultVersion) {
68+
this.defaultVersion = defaultVersion;
69+
}
70+
71+
public boolean isDetectSupportedVersions() {
72+
return detectSupportedVersions;
73+
}
74+
75+
public void setDetectSupportedVersions(boolean detectSupportedVersions) {
76+
this.detectSupportedVersions = detectSupportedVersions;
77+
}
78+
79+
public String getHeaderName() {
80+
return headerName;
81+
}
82+
83+
public void setHeaderName(String headerName) {
84+
this.headerName = headerName;
85+
}
86+
87+
public MediaType getMediaType() {
88+
return mediaType;
89+
}
90+
91+
public void setMediaType(MediaType mediaType) {
92+
this.mediaType = mediaType;
93+
}
94+
95+
public String getMediaTypeParamName() {
96+
return mediaTypeParamName;
97+
}
98+
99+
public void setMediaTypeParamName(String mediaTypeParamName) {
100+
this.mediaTypeParamName = mediaTypeParamName;
101+
}
102+
103+
public Integer getPathSegment() {
104+
return pathSegment;
105+
}
106+
107+
public void setPathSegment(Integer pathSegment) {
108+
this.pathSegment = pathSegment;
109+
}
110+
111+
public String getRequestParamName() {
112+
return requestParamName;
113+
}
114+
115+
public void setRequestParamName(String requestParamName) {
116+
this.requestParamName = requestParamName;
117+
}
118+
119+
public boolean isRequired() {
120+
return required;
121+
}
122+
123+
public void setRequired(boolean required) {
124+
this.required = required;
125+
}
126+
127+
public List<String> getSupportedVersions() {
128+
return supportedVersions;
129+
}
130+
131+
public void setSupportedVersions(List<String> supportedVersions) {
132+
this.supportedVersions = supportedVersions;
133+
}
134+
135+
@Override
136+
public String toString() {
137+
// @formatter:off
138+
return new ToStringCreator(this)
139+
.append("defaultVersion", defaultVersion)
140+
.append("detectSupportedVersions", detectSupportedVersions)
141+
.append("headerName", headerName)
142+
.append("mediaType", mediaType)
143+
.append("mediaTypeParamName", mediaTypeParamName)
144+
.append("pathSegment", pathSegment)
145+
.append("requestParamName", requestParamName)
146+
.append("required", required)
147+
.append("supportedVersions", supportedVersions)
148+
.toString();
149+
// @formatter:on
150+
151+
}
152+
153+
}

0 commit comments

Comments
 (0)