spring-cloud-gateway icon indicating copy to clipboard operation
spring-cloud-gateway copied to clipboard

Gateway MVC : Support for externalized or Dynamic routes

Open kdhrubo opened this issue 2 years ago • 13 comments

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

kdhrubo avatar Dec 12 '23 04:12 kdhrubo

It's not currently a feature

spencergibb avatar Dec 14 '23 21:12 spencergibb

Any chance it will be added in near future to make it comparable to reactive gateway?

kdhrubo avatar Dec 14 '23 21:12 kdhrubo

would like this feature to be added, that makes gateway mvc more usable.

code4curiosity avatar Jun 19 '24 08:06 code4curiosity

Looking forward to this feature.

kdhrubo avatar Jul 01 '24 02:07 kdhrubo

I am looking forward too!

rmasoudi avatar Aug 07 '24 04:08 rmasoudi

Looks like work is underway.

kdhrubo avatar Aug 16 '24 16:08 kdhrubo

Any update on that feature?

borowskimarcin avatar Mar 26 '25 13:03 borowskimarcin

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.

spencergibb avatar Mar 26 '25 14:03 spencergibb

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.

kdhrubo avatar Mar 26 '25 14:03 kdhrubo

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.

spencergibb avatar Mar 26 '25 14:03 spencergibb

Any tip on how this can be done, i can take an attempt and may be create a PR.

kdhrubo avatar Mar 26 '25 14:03 kdhrubo

I don't think this is a good one for external contributors as I need to do some design work around it.

spencergibb avatar Mar 26 '25 15:03 spencergibb

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.

borowskimarcin avatar Apr 01 '25 20:04 borowskimarcin