spring-cloud-gateway
spring-cloud-gateway copied to clipboard
Gateway MVC : Support for externalized or Dynamic routes
Describe the bug 4.1.0
Spring Cloud Gateway (Reactive) does support configuring routes in yml as well as dynamic route refresh from external sources. The gateway MVC does not support route refresh capability at runtime. If so kindly share how it can be done.
Sample NA
It's not currently a feature
Any chance it will be added in near future to make it comparable to reactive gateway?
would like this feature to be added, that makes gateway mvc more usable.
Looking forward to this feature.
I am looking forward too!
Looks like work is underway.
Any update on that feature?
Using external config files currently works. For routes from something like DiscoveryClient, when this issue is assigned a project, then we are starting work on it.
thanks @spencergibb using external config files - do you mean spring config files?
My original intent was from non spring files or say external data source like a database to load the configuration. Any test or example reference will be helpful.
Yes external properties (application.properites or yaml). The db or discovery client is what this issue is for, so I don't have any test or example.
Any tip on how this can be done, i can take an attempt and may be create a PR.
I don't think this is a good one for external contributors as I need to do some design work around it.
Since my routes are dynamically defined and can change at runtime, I implemented a custom router function mapping that loads a custom router function. It’s not perfect, but it works for now:
public class CustomRouterFunctionMapping extends RouterFunctionMapping {
private static final Logger log = LoggerFactory.getLogger(CustomRouterFunctionMapping.class);
private final AtomicReference<RouterFunction<?>> customRouterFunction = new AtomicReference<>();
private final CustomRouterFunctionRetriever customRouterFunctionRetriever;
private final List<HttpMessageConverter<?>> messageConverters;
public CustomRouterFunctionMapping(CustomRouterFunctionRetriever customRouterFunctionRetriever, List<HttpMessageConverter<?>> messageConverters) {
this.customRouterFunctionRetriever = customRouterFunctionRetriever;
this.messageConverters = messageConverters;
}
public void refresh() {
log.info("Refreshing the custom router function");
RouterFunction<?> controlPlaneRouterFunction = customRouterFunctionRetriever.retrieve();
RouterFunction<?> defaultSpringRoutingFunction = super.getRouterFunction();
RouterFunction<?> finalRouterFunction = getFinalRouterFunction(defaultSpringRoutingFunction, controlPlaneRouterFunction);
customRouterFunction.set(finalRouterFunction);
log.info("Refreshing of the custom router function finished: {}", finalRouterFunction);
}
private RouterFunction<?> getFinalRouterFunction(RouterFunction<?> defaultRouterFunction, RouterFunction<?> controlPlaneRouterFunction) {
if (controlPlaneRouterFunction != null) {
return defaultRouterFunction != null ? controlPlaneRouterFunction.andOther(defaultRouterFunction) : controlPlaneRouterFunction;
}
return defaultRouterFunction;
}
@Override
public RouterFunction<?> getRouterFunction() {
return this.customRouterFunction.get();
}
/**
* Based on the {@link org.springframework.web.servlet.function.support.RouterFunctionMapping#getHandlerInternal(jakarta.servlet.http.HttpServletRequest)}
*/
@Override
@Nullable
protected Object getHandlerInternal(HttpServletRequest servletRequest) throws Exception {
RouterFunction<?> currentRoutingFunction = this.customRouterFunction.get();
if (currentRoutingFunction != null) {
ServerRequest request = ServerRequest.create(servletRequest, this.messageConverters);
HandlerFunction<?> handlerFunction = currentRoutingFunction.route(request).orElse(null);
setAttributes(servletRequest, request, handlerFunction);
return handlerFunction;
} else {
return null;
}
}
/**
* Based on the {@link org.springframework.web.servlet.function.support.RouterFunctionMapping#setAttributes(HttpServletRequest, ServerRequest, HandlerFunction)}
*/
private void setAttributes(HttpServletRequest servletRequest, ServerRequest request,
@Nullable HandlerFunction<?> handlerFunction) {
PathPattern matchingPattern =
(PathPattern) servletRequest.getAttribute(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE);
if (matchingPattern != null) {
servletRequest.removeAttribute(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE);
servletRequest.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, matchingPattern.getPatternString());
ServerHttpObservationFilter.findObservationContext(request.servletRequest())
.ifPresent(context -> context.setPathPattern(matchingPattern.getPatternString()));
}
servletRequest.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, handlerFunction);
servletRequest.setAttribute(RouterFunctions.REQUEST_ATTRIBUTE, request);
}
}
@Configuration
public class RoutingConfiguration extends WebMvcConfigurationSupport {
/**
* This configuration is based on the {@link WebMvcConfigurationSupport#routerFunctionMapping(FormattingConversionService, ResourceUrlProvider)}
* It should be checked for the potential breaking changes with every Spring Boot upgrade.
*/
@Bean
@Primary
public CustomRouterFunctionMapping customRouterFunctionMapping(
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider,
CustomRouterFunctionRetriever customRouterFunctionRetriever
) {
CustomRouterFunctionMapping mapping = new CustomRouterFunctionMapping(customRouterFunctionRetriever, getMessageConverters());
// We want the custom router function mapping to be the first in the chain.
mapping.setOrder(HIGHEST_PRECEDENCE);
mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
mapping.setCorsConfigurations(getCorsConfigurations());
PathPatternParser patternParser = getPathMatchConfigurer().getPatternParser();
if (patternParser != null) {
mapping.setPatternParser(patternParser);
}
return mapping;
}
}
If you have any suggestions or better alternatives, please feel free to share.