Move ApiVersionStrategy up to AbstractHandlerMapping

Similar to CorProcessor, ApiVersionStrategy is now supported at the
AbstractHandlerMapping level.

See gh-35113
This commit is contained in:
rstoyanchev 2025-06-30 17:09:41 +01:00
parent 93a9c9b8bf
commit 86f50b20f2
6 changed files with 98 additions and 169 deletions

View File

@ -29,7 +29,6 @@ import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.observation.ServerRequestObservationContext;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.accept.ApiVersionStrategy;
import org.springframework.web.reactive.accept.DefaultApiVersionStrategy;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
@ -56,8 +55,6 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
private @Nullable ApiVersionStrategy versionStrategy;
/**
* Create an empty {@code RouterFunctionMapping}.
@ -96,15 +93,6 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
this.messageReaders = messageReaders;
}
/**
* Configure a strategy to manage API versioning.
* @param strategy the strategy to use
* @since 7.0
*/
public void setApiVersionStrategy(@Nullable ApiVersionStrategy strategy) {
this.versionStrategy = strategy;
}
@Override
public void afterPropertiesSet() throws Exception {
@ -119,7 +107,7 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
if (this.routerFunction != null) {
RouterFunctions.changeParser(this.routerFunction, getPathPatternParser());
if (this.versionStrategy instanceof DefaultApiVersionStrategy davs) {
if (getApiVersionStrategy() instanceof DefaultApiVersionStrategy davs) {
if (davs.detectSupportedVersions()) {
this.routerFunction.accept(new SupportedVersionVisitor(davs));
}
@ -169,24 +157,10 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
if (this.routerFunction == null) {
return Mono.empty();
}
if (this.versionStrategy != null) {
Comparable<?> version = exchange.getAttribute(API_VERSION_ATTRIBUTE);
if (version == null) {
version = this.versionStrategy.resolveParseAndValidateVersion(exchange);
if (version != null) {
exchange.getAttributes().put(API_VERSION_ATTRIBUTE, version);
}
}
}
ServerRequest request = ServerRequest.create(
exchange, this.messageReaders, this.versionStrategy);
ServerRequest request = ServerRequest.create(exchange, this.messageReaders, getApiVersionStrategy());
return this.routerFunction.route(request)
.doOnNext(handler -> setAttributes(exchange.getAttributes(), request, handler));
}

View File

@ -35,6 +35,7 @@ import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.cors.reactive.DefaultCorsProcessor;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.accept.ApiVersionStrategy;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
import org.springframework.web.util.pattern.PathPatternParser;
@ -64,6 +65,8 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport
private CorsProcessor corsProcessor = new DefaultCorsProcessor();
private @Nullable ApiVersionStrategy apiVersionStrategy;
private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered
private @Nullable String beanName;
@ -138,6 +141,23 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport
return this.corsProcessor;
}
/**
* Configure a strategy to manage API versioning.
* @param strategy the strategy to use
* @since 7.0
*/
public void setApiVersionStrategy(@Nullable ApiVersionStrategy strategy) {
this.apiVersionStrategy = strategy;
}
/**
* Return the configured {@link ApiVersionStrategy} strategy.
* @since 7.0
*/
public @Nullable ApiVersionStrategy getApiVersionStrategy() {
return this.apiVersionStrategy;
}
/**
* Specify the order value for this HandlerMapping bean.
* <p>The default value is {@code Ordered.LOWEST_PRECEDENCE}, meaning non-ordered.
@ -164,6 +184,7 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport
@Override
public Mono<Object> getHandler(ServerWebExchange exchange) {
initApiVersion(exchange);
return getHandlerInternal(exchange).map(handler -> {
if (logger.isDebugEnabled()) {
logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
@ -182,10 +203,28 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport
return NO_OP_HANDLER;
}
}
if (getApiVersionStrategy() != null) {
Comparable<?> version = exchange.getAttribute(API_VERSION_ATTRIBUTE);
if (version != null) {
getApiVersionStrategy().handleDeprecations(version, exchange);
}
}
return handler;
});
}
private void initApiVersion(ServerWebExchange exchange) {
if (this.apiVersionStrategy != null) {
Comparable<?> version = exchange.getAttribute(API_VERSION_ATTRIBUTE);
if (version == null) {
version = this.apiVersionStrategy.resolveParseAndValidateVersion(exchange);
if (version != null) {
exchange.getAttributes().put(API_VERSION_ATTRIBUTE, version);
}
}
}
}
/**
* Look up a handler for the given request, returning an empty {@code Mono}
* if no specific one is found. This method is called by {@link #getHandler}.

View File

@ -28,7 +28,6 @@ import java.util.function.Predicate;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
@ -48,7 +47,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.accept.ApiVersionStrategy;
import org.springframework.web.reactive.accept.DefaultApiVersionStrategy;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
@ -56,7 +54,6 @@ import org.springframework.web.reactive.result.condition.ConsumesRequestConditio
import org.springframework.web.reactive.result.condition.RequestCondition;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.service.annotation.HttpExchange;
/**
@ -82,8 +79,6 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
private RequestedContentTypeResolver contentTypeResolver = new RequestedContentTypeResolverBuilder().build();
private @Nullable ApiVersionStrategy apiVersionStrategy;
private @Nullable StringValueResolver embeddedValueResolver;
private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
@ -132,23 +127,6 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
return this.contentTypeResolver;
}
/**
* Configure a strategy to manage API versioning.
* @param strategy the strategy to use
* @since 7.0
*/
public void setApiVersionStrategy(@Nullable ApiVersionStrategy strategy) {
this.apiVersionStrategy = strategy;
}
/**
* Return the configured {@link ApiVersionStrategy} strategy.
* @since 7.0
*/
public @Nullable ApiVersionStrategy getApiVersionStrategy() {
return this.apiVersionStrategy;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
@ -174,20 +152,6 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
}
@Override
public Mono<HandlerMethod> getHandlerInternal(ServerWebExchange exchange) {
if (this.apiVersionStrategy != null) {
Comparable<?> version = exchange.getAttribute(API_VERSION_ATTRIBUTE);
if (version == null) {
version = this.apiVersionStrategy.resolveParseAndValidateVersion(exchange);
if (version != null) {
exchange.getAttributes().put(API_VERSION_ATTRIBUTE, version);
}
}
}
return super.getHandlerInternal(exchange);
}
/**
* Uses type-level and method-level {@link RequestMapping @RequestMapping}
* and {@link HttpExchange @HttpExchange} annotations to create the
@ -253,7 +217,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
info = createRequestMappingInfo((HttpExchange) exchangeDescriptors.get(0).annotation, customCondition);
}
if (info != null && this.apiVersionStrategy instanceof DefaultApiVersionStrategy davs) {
if (info != null && getApiVersionStrategy() instanceof DefaultApiVersionStrategy davs) {
String version = info.getVersionCondition().getVersion();
if (version != null) {
davs.addMappedVersion(version);
@ -397,17 +361,6 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
new RequestMethod[] {RequestMethod.valueOf(method)} : EMPTY_REQUEST_METHOD_ARRAY);
}
@Override
protected void handleMatch(RequestMappingInfo info, HandlerMethod handlerMethod, ServerWebExchange exchange) {
super.handleMatch(info, handlerMethod, exchange);
Comparable<?> version = exchange.getAttribute(API_VERSION_ATTRIBUTE);
if (version != null) {
Assert.state(this.apiVersionStrategy != null, "No ApiVersionStrategy");
this.apiVersionStrategy.handleDeprecations(version, exchange);
}
}
@Override
public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) {
super.registerMapping(mapping, handler, method);

View File

@ -31,7 +31,6 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.util.CollectionUtils;
import org.springframework.web.accept.ApiVersionStrategy;
import org.springframework.web.accept.DefaultApiVersionStrategy;
import org.springframework.web.filter.ServerHttpObservationFilter;
import org.springframework.web.servlet.function.HandlerFunction;
@ -64,8 +63,6 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
private List<HttpMessageConverter<?>> messageConverters = Collections.emptyList();
private @Nullable ApiVersionStrategy versionStrategy;
private boolean detectHandlerFunctionsInAncestorContexts = false;
@ -114,15 +111,6 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
this.messageConverters = messageConverters;
}
/**
* Configure a strategy to manage API versioning.
* @param strategy the strategy to use
* @since 7.0
*/
public void setApiVersionStrategy(@Nullable ApiVersionStrategy strategy) {
this.versionStrategy = strategy;
}
/**
* Set whether to detect handler functions in ancestor ApplicationContexts.
* <p>Default is "false": Only handler functions in the current ApplicationContext
@ -154,7 +142,7 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
}
RouterFunctions.changeParser(this.routerFunction, patternParser);
if (this.versionStrategy instanceof DefaultApiVersionStrategy davs) {
if (getApiVersionStrategy() instanceof DefaultApiVersionStrategy davs) {
if (davs.detectSupportedVersions()) {
this.routerFunction.accept(new SupportedVersionVisitor(davs));
}
@ -218,24 +206,10 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
@Override
protected @Nullable Object getHandlerInternal(HttpServletRequest servletRequest) throws Exception {
if (this.routerFunction == null) {
return null;
}
if (this.versionStrategy != null) {
Comparable<?> version = (Comparable<?>) servletRequest.getAttribute(API_VERSION_ATTRIBUTE);
if (version == null) {
version = this.versionStrategy.resolveParseAndValidateVersion(servletRequest);
if (version != null) {
servletRequest.setAttribute(API_VERSION_ATTRIBUTE, version);
}
}
}
ServerRequest request =
ServerRequest.create(servletRequest, this.messageConverters, this.versionStrategy);
ServerRequest request = ServerRequest.create(servletRequest, this.messageConverters, getApiVersionStrategy());
HandlerFunction<?> handlerFunction = this.routerFunction.route(request).orElse(null);
setAttributes(servletRequest, request, handlerFunction);
return handlerFunction;

View File

@ -39,6 +39,7 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.PathMatcher;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.accept.ApiVersionStrategy;
import org.springframework.web.context.request.WebRequestInterceptor;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncUtils;
@ -104,6 +105,8 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
private CorsProcessor corsProcessor = new DefaultCorsProcessor();
private @Nullable ApiVersionStrategy versionStrategy;
private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered
private @Nullable String beanName;
@ -397,6 +400,23 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
return this.corsProcessor;
}
/**
* Configure a strategy to manage API versioning.
* @param strategy the strategy to use
* @since 7.0
*/
public void setApiVersionStrategy(@Nullable ApiVersionStrategy strategy) {
this.versionStrategy = strategy;
}
/**
* Return the configured {@link ApiVersionStrategy} strategy.
* @since 7.0
*/
public @Nullable ApiVersionStrategy getApiVersionStrategy() {
return this.versionStrategy;
}
/**
* Specify the order value for this HandlerMapping bean.
* <p>The default value is {@code Ordered.LOWEST_PRECEDENCE}, meaning non-ordered.
@ -519,6 +539,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
*/
@Override
public final @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
initApiVersion(request);
Object handler = getHandlerInternal(request);
if (handler == null) {
handler = getDefaultHandler();
@ -563,6 +584,18 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
return executionChain;
}
private void initApiVersion(HttpServletRequest request) {
if (this.versionStrategy != null) {
Comparable<?> version = (Comparable<?>) request.getAttribute(API_VERSION_ATTRIBUTE);
if (version == null) {
version = this.versionStrategy.resolveParseAndValidateVersion(request);
if (version != null) {
request.setAttribute(API_VERSION_ATTRIBUTE, version);
}
}
}
}
/**
* Look up a handler for the given request, returning {@code null} if no
* specific one is found. This method is called by {@link #getHandler};
@ -634,6 +667,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
* @see #getAdaptedInterceptors()
*/
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain handlerExecutionChain ?
handlerExecutionChain : new HandlerExecutionChain(handler));
@ -647,6 +681,14 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
chain.addInterceptor(interceptor);
}
}
if (this.versionStrategy != null) {
Comparable<?> version = (Comparable<?>) request.getAttribute(API_VERSION_ATTRIBUTE);
if (version != null) {
chain.addInterceptor(new ApiVersionDeprecationHandlerInterceptor(this.versionStrategy, version));
}
}
return chain;
}
@ -755,4 +797,15 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
}
}
private record ApiVersionDeprecationHandlerInterceptor(
ApiVersionStrategy versionStrategy, Comparable<?> version) implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
this.versionStrategy.handleDeprecations(this.version, request, response);
return true;
}
}
}

View File

@ -28,7 +28,6 @@ import java.util.function.Predicate;
import java.util.stream.Stream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.context.EmbeddedValueResolverAware;
@ -43,7 +42,6 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
import org.springframework.web.accept.ApiVersionStrategy;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.DefaultApiVersionStrategy;
import org.springframework.web.bind.annotation.CrossOrigin;
@ -53,8 +51,6 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.MatchableHandlerMapping;
import org.springframework.web.servlet.handler.RequestMatchResult;
import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
@ -89,8 +85,6 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
private @Nullable ApiVersionStrategy apiVersionStrategy;
private @Nullable StringValueResolver embeddedValueResolver;
private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
@ -137,23 +131,6 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
return this.contentNegotiationManager;
}
/**
* Configure a strategy to manage API versioning.
* @param strategy the strategy to use
* @since 7.0
*/
public void setApiVersionStrategy(@Nullable ApiVersionStrategy strategy) {
this.apiVersionStrategy = strategy;
}
/**
* Return the configured {@link ApiVersionStrategy} strategy.
* @since 7.0
*/
public @Nullable ApiVersionStrategy getApiVersionStrategy() {
return this.apiVersionStrategy;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
@ -201,20 +178,6 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
}
@Override
protected @Nullable HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
if (this.apiVersionStrategy != null) {
Comparable<?> version = (Comparable<?>) request.getAttribute(API_VERSION_ATTRIBUTE);
if (version == null) {
version = this.apiVersionStrategy.resolveParseAndValidateVersion(request);
if (version != null) {
request.setAttribute(API_VERSION_ATTRIBUTE, version);
}
}
}
return super.getHandlerInternal(request);
}
/**
* Uses type-level and method-level {@link RequestMapping @RequestMapping}
* and {@link HttpExchange @HttpExchange} annotations to create the
@ -287,7 +250,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
info = createRequestMappingInfo((HttpExchange) exchangeDescriptors.get(0).annotation, customCondition);
}
if (info != null && this.apiVersionStrategy instanceof DefaultApiVersionStrategy davs) {
if (info != null && getApiVersionStrategy() instanceof DefaultApiVersionStrategy davs) {
String version = info.getVersionCondition().getVersion();
if (version != null) {
davs.addMappedVersion(version);
@ -425,16 +388,6 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
new RequestMethod[] {RequestMethod.valueOf(method)} : EMPTY_REQUEST_METHOD_ARRAY);
}
@Override
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain executionChain = super.getHandlerExecutionChain(handler, request);
Comparable<?> version = (Comparable<?>) request.getAttribute(API_VERSION_ATTRIBUTE);
if (version != null) {
executionChain.addInterceptor(new DeprecationInterceptor(version));
}
return executionChain;
}
@Override
public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) {
super.registerMapping(mapping, handler, method);
@ -597,21 +550,4 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
}
private final class DeprecationInterceptor implements HandlerInterceptor {
private final Comparable<?> version;
private DeprecationInterceptor(Comparable<?> version) {
this.version = version;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
Assert.state(apiVersionStrategy != null, "No ApiVersionStrategy");
apiVersionStrategy.handleDeprecations(this.version, request, response);
return true;
}
}
}