From fbb72e2164fb5c82ee2ccf2cc8ba5be8c5bd6e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Wi=C3=9Fkirchen?= Date: Tue, 19 Mar 2024 16:36:17 +0100 Subject: [PATCH] Enhance AOT-Support (Issue#3171) (#3193) replace registerBeanDefinition callback with a factoryMethod split out a RouterFunctionHolderFactory from the Registrar class and provide its bean in the AutoConfiguration set refresh scope on the RouterFunctionHolder bean only if RefreshScope bean in context and add warning add runtimeHintsRegistrar to allow externalized route configuration --- .../GatewayServerMvcAutoConfiguration.java | 18 +- .../GatewayMvcAotRuntimeHintsRegistrar.java | 82 +++++ ...yMvcPropertiesBeanDefinitionRegistrar.java | 297 +---------------- .../config/RouterFunctionHolderFactory.java | 315 ++++++++++++++++++ 4 files changed, 432 insertions(+), 280 deletions(-) create mode 100644 spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java create mode 100644 spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/RouterFunctionHolderFactory.java diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java index 87a7ee81..062be411 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2024 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. @@ -28,8 +28,10 @@ import org.springframework.boot.web.client.ClientHttpRequestFactories; import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; import org.springframework.boot.web.client.RestClientCustomizer; import org.springframework.cloud.gateway.server.mvc.common.ArgumentSupplierBeanPostProcessor; +import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcAotRuntimeHintsRegistrar; import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties; import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcPropertiesBeanDefinitionRegistrar; +import org.springframework.cloud.gateway.server.mvc.config.RouterFunctionHolderFactory; import org.springframework.cloud.gateway.server.mvc.filter.FormFilter; import org.springframework.cloud.gateway.server.mvc.filter.ForwardedRequestHeadersFilter; import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter.RequestHttpHeadersFilter; @@ -48,14 +50,23 @@ import org.springframework.cloud.gateway.server.mvc.predicate.PredicateDiscovere import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.core.env.Environment; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.util.StringUtils; import org.springframework.web.client.RestClient; +/** + * AutoConfiguration for Spring Cloud Gateway MVC server. + * + * @author Spencer Gibb + * @author Jürgen Wißkirchen + */ @AutoConfiguration(after = { RestTemplateAutoConfiguration.class, RestClientAutoConfiguration.class }) @ConditionalOnProperty(name = "spring.cloud.gateway.mvc.enabled", matchIfMissing = true) @Import(GatewayMvcPropertiesBeanDefinitionRegistrar.class) +@ImportRuntimeHints(GatewayMvcAotRuntimeHintsRegistrar.class) public class GatewayServerMvcAutoConfiguration { @Bean @@ -64,6 +75,11 @@ public class GatewayServerMvcAutoConfiguration { return new ArgumentSupplierBeanPostProcessor(publisher); } + @Bean + public RouterFunctionHolderFactory routerFunctionHolderFactory(Environment env) { + return new RouterFunctionHolderFactory(env); + } + @Bean public RestClientCustomizer gatewayRestClientCustomizer(ClientHttpRequestFactory requestFactory) { return restClientBuilder -> restClientBuilder.requestFactory(requestFactory); diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java new file mode 100644 index 00000000..9312a8b0 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013-2024 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.cloud.gateway.server.mvc.config; + +import java.util.Arrays; +import java.util.Set; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions; +import org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions; +import org.springframework.cloud.gateway.server.mvc.filter.BodyFilterFunctions; +import org.springframework.cloud.gateway.server.mvc.filter.Bucket4jFilterFunctions; +import org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions; +import org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions; +import org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions; +import org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerHandlerSupplier; +import org.springframework.cloud.gateway.server.mvc.filter.TokenRelayFilterFunctions; +import org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions; +import org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions; +import org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates; +import org.springframework.lang.NonNull; +import org.springframework.util.ClassUtils; + +/** + * AOT runtime hints registrar on the gateway server mvc. + * + * @author Jürgen Wißkirchen + */ +public class GatewayMvcAotRuntimeHintsRegistrar implements RuntimeHintsRegistrar { + + private static final Set> FUNCTION_PROVIDERS = Set.of(HandlerFunctions.class, + LoadBalancerHandlerSupplier.class, FilterFunctions.class, BeforeFilterFunctions.class, + AfterFilterFunctions.class, TokenRelayFilterFunctions.class, BodyFilterFunctions.class, + CircuitBreakerFilterFunctions.class, GatewayRouterFunctions.class, LoadBalancerFilterFunctions.class, + GatewayRequestPredicates.class, Bucket4jFilterFunctions.class); + + private static final Set> PROPERTIES = Set.of(FilterProperties.class, PredicateProperties.class, + RouteProperties.class); + + @Override + public void registerHints(@NonNull RuntimeHints hints, ClassLoader classLoader) { + final ReflectionHints reflectionHints = hints.reflection(); + FUNCTION_PROVIDERS.forEach(clazz -> addHintsForClass(reflectionHints, clazz, classLoader)); + + PROPERTIES.forEach(clazz -> reflectionHints.registerType(clazz, MemberCategory.PUBLIC_FIELDS, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + } + + /** + * Add hints for the given class. Since we need to register mostly static methods, the + * annotation way with @Reflective does not work here. + * @param reflectionHints the reflection hints + * @param clazz the class to add hints for + * @param classLoader the class loader + */ + private void addHintsForClass(ReflectionHints reflectionHints, Class clazz, ClassLoader classLoader) { + if (!ClassUtils.isPresent(clazz.getName(), classLoader)) { + return; // safety net + } + Arrays.stream(clazz.getMethods()) + .forEach(method -> reflectionHints.registerMethod(method, ExecutableMode.INVOKE)); + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcPropertiesBeanDefinitionRegistrar.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcPropertiesBeanDefinitionRegistrar.java index 80a52065..3b9f1261 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcPropertiesBeanDefinitionRegistrar.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcPropertiesBeanDefinitionRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2024 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. @@ -16,49 +16,17 @@ package org.springframework.cloud.gateway.server.mvc.config; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.boot.context.properties.bind.handler.IgnoreTopLevelConverterNotFoundBindHandler; -import org.springframework.boot.context.properties.source.ConfigurationPropertySource; -import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; -import org.springframework.cloud.gateway.server.mvc.common.Configurable; -import org.springframework.cloud.gateway.server.mvc.common.MvcUtils; -import org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions; -import org.springframework.cloud.gateway.server.mvc.filter.FilterDiscoverer; -import org.springframework.cloud.gateway.server.mvc.handler.HandlerDiscoverer; -import org.springframework.cloud.gateway.server.mvc.invoke.InvocationContext; -import org.springframework.cloud.gateway.server.mvc.invoke.OperationArgumentResolver; -import org.springframework.cloud.gateway.server.mvc.invoke.OperationParameter; -import org.springframework.cloud.gateway.server.mvc.invoke.OperationParameters; -import org.springframework.cloud.gateway.server.mvc.invoke.ParameterValueMapper; -import org.springframework.cloud.gateway.server.mvc.invoke.convert.ConversionServiceParameterValueMapper; -import org.springframework.cloud.gateway.server.mvc.invoke.reflect.OperationMethod; -import org.springframework.cloud.gateway.server.mvc.invoke.reflect.ReflectiveOperationInvoker; -import org.springframework.cloud.gateway.server.mvc.predicate.PredicateDiscoverer; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.env.Environment; -import org.springframework.core.log.LogMessage; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; import org.springframework.web.servlet.function.HandlerFilterFunction; import org.springframework.web.servlet.function.HandlerFunction; import org.springframework.web.servlet.function.RequestPredicate; @@ -67,59 +35,35 @@ import org.springframework.web.servlet.function.RouterFunctions; import org.springframework.web.servlet.function.ServerRequest; import org.springframework.web.servlet.function.ServerResponse; -import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; - +/** + * BeanDefinitionRegistrar that registers a RouterFunctionHolder and a + * DelegatingRouterFunction. + * + * @author Spencer Gibb + * @author Pavel Tregl + * @author Jürgen Wißkirchen + */ public class GatewayMvcPropertiesBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { - private static final RequestPredicate neverPredicate = new RequestPredicate() { - @Override - public boolean test(ServerRequest request) { - return false; - } - - @Override - public String toString() { - return "Never"; - } - }; - - private static final RouterFunction NEVER_ROUTE = RouterFunctions.route(neverPredicate, - request -> ServerResponse.notFound().build()); - - protected final Log log = LogFactory.getLog(getClass()); - - private final TrueNullOperationArgumentResolver trueNullOperationArgumentResolver = new TrueNullOperationArgumentResolver(); - - private final Environment env; - - private final FilterDiscoverer filterDiscoverer = new FilterDiscoverer(); - - private final HandlerDiscoverer handlerDiscoverer = new HandlerDiscoverer(); - - private final PredicateDiscoverer predicateDiscoverer = new PredicateDiscoverer(); - - private final ParameterValueMapper parameterValueMapper = new ConversionServiceParameterValueMapper(); - - public GatewayMvcPropertiesBeanDefinitionRegistrar(Environment env) { - this.env = env; - } - @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { // registers a RouterFunctionHolder that specifically isn't a RouterFunction since // RouterFunctionMapping gets a list of RouterFunction and if you put // RouterFunction in refresh scope, RouterFunctionMapping will end up with two. - // Uses this::routerFunctionHolderSupplier so when the bean is refreshed, that - // method is called again. + // Registers RouterFunctionHolderFactory::routerFunctionHolderSupplier so when the + // bean is refreshed, that method is called again. AbstractBeanDefinition routerFnProviderBeanDefinition = BeanDefinitionBuilder - .genericBeanDefinition(RouterFunctionHolder.class, this::routerFunctionHolderSupplier) + .rootBeanDefinition(RouterFunctionHolder.class) + .setFactoryMethodOnBean("routerFunctionHolderSupplier", "routerFunctionHolderFactory") .getBeanDefinition(); - // TODO: opt out of refresh scope? - // Puts the RouterFunctionHolder in refresh scope BeanDefinitionHolder holder = new BeanDefinitionHolder(routerFnProviderBeanDefinition, "gatewayRouterFunctionHolder"); BeanDefinitionHolder proxy = ScopedProxyUtils.createScopedProxy(holder, registry, true); - routerFnProviderBeanDefinition.setScope("refresh"); + + // Puts the RouterFunctionHolder in refresh scope, if not disabled. + if (registry.containsBeanDefinition("refreshScope")) { + routerFnProviderBeanDefinition.setScope("refresh"); + } if (registry.containsBeanDefinition(proxy.getBeanName())) { registry.removeBeanDefinition(proxy.getBeanName()); } @@ -133,211 +77,6 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrar implements ImportBeanDe registry.registerBeanDefinition("gatewayCompositeRouterFunction", routerFunctionBeanDefinition); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - private RouterFunctionHolder routerFunctionHolderSupplier() { - GatewayMvcProperties properties = Binder.get(env).bindOrCreate(GatewayMvcProperties.PREFIX, - GatewayMvcProperties.class); - log.trace(LogMessage.format("RouterFunctionHolder initializing with %d map routes and %d list routes", - properties.getRoutesMap().size(), properties.getRoutes().size())); - - Map routerFunctions = new LinkedHashMap<>(); - properties.getRoutes().forEach(routeProperties -> { - routerFunctions.put(routeProperties.getId(), getRouterFunction(routeProperties, routeProperties.getId())); - }); - properties.getRoutesMap().forEach((routeId, routeProperties) -> { - String computedRouteId = routeId; - if (StringUtils.hasText(routeProperties.getId())) { - computedRouteId = routeProperties.getId(); - } - routerFunctions.put(computedRouteId, getRouterFunction(routeProperties, computedRouteId)); - }); - RouterFunction routerFunction; - if (routerFunctions.isEmpty()) { - // no properties routes, so a RouterFunction that will never match - routerFunction = NEVER_ROUTE; - } - else { - routerFunction = routerFunctions.values().stream().reduce(RouterFunction::andOther).orElse(null); - // puts the map of configured RouterFunctions in an attribute. Makes testing - // easy. - routerFunction = routerFunction.withAttribute("gatewayRouterFunctions", routerFunctions); - } - log.trace(LogMessage.format("RouterFunctionHolder initialized %s", routerFunction.toString())); - return new RouterFunctionHolder(routerFunction); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private RouterFunction getRouterFunction(RouteProperties routeProperties, String routeId) { - log.trace(LogMessage.format("Creating route for : %s", routeProperties)); - - RouterFunctions.Builder builder = route(routeId); - - // MVC.fn users won't need this anonymous filter as url will be set directly. - // Put this function first, so if a filter from a handler changes the url - // it is after this one. - builder.filter((request, next) -> { - MvcUtils.setRequestUrl(request, routeProperties.getUri()); - return next.handle(request); - }); - builder.before(BeforeFilterFunctions.routeId(routeId)); - - MultiValueMap handlerOperations = handlerDiscoverer.getOperations(); - // TODO: cache? - // translate handlerFunction - String scheme = routeProperties.getUri().getScheme(); - Map handlerArgs = new HashMap<>(); - Optional handlerOperationMethod = findOperation(handlerOperations, - scheme.toLowerCase(), handlerArgs); - if (handlerOperationMethod.isEmpty()) { - // single RouteProperties param - handlerArgs.clear(); - String routePropsKey = StringUtils.uncapitalize(RouteProperties.class.getSimpleName()); - handlerArgs.put(routePropsKey, routeProperties); - handlerOperationMethod = findOperation(handlerOperations, scheme.toLowerCase(), handlerArgs); - if (handlerOperationMethod.isEmpty()) { - throw new IllegalStateException("Unable to find HandlerFunction for scheme: " + scheme); - } - } - NormalizedOperationMethod normalizedOpMethod = handlerOperationMethod.get(); - Object response = invokeOperation(normalizedOpMethod, normalizedOpMethod.getNormalizedArgs()); - HandlerFunction handlerFunction = null; - if (response instanceof HandlerFunction) { - handlerFunction = (HandlerFunction) response; - } - else if (response instanceof HandlerDiscoverer.Result result) { - handlerFunction = result.getHandlerFunction(); - result.getFilters().forEach(builder::filter); - } - if (handlerFunction == null) { - throw new IllegalStateException( - "Unable to find HandlerFunction for scheme: " + scheme + " and response " + response); - } - - // translate predicates - MultiValueMap predicateOperations = predicateDiscoverer.getOperations(); - final AtomicReference predicate = new AtomicReference<>(); - - routeProperties.getPredicates().forEach(predicateProperties -> { - Map args = new LinkedHashMap<>(predicateProperties.getArgs()); - translate(predicateOperations, predicateProperties.getName(), args, RequestPredicate.class, - requestPredicate -> { - log.trace(LogMessage.format("Adding predicate to route %s - %s", routeId, predicateProperties)); - if (predicate.get() == null) { - predicate.set(requestPredicate); - } - else { - RequestPredicate combined = predicate.get().and(requestPredicate); - predicate.set(combined); - } - log.trace(LogMessage.format("Combined predicate for route %s - %s", routeId, predicate.get())); - }); - }); - - // combine predicate and handlerFunction - builder.route(predicate.get(), handlerFunction); - predicate.set(null); - - // translate filters - MultiValueMap filterOperations = filterDiscoverer.getOperations(); - routeProperties.getFilters().forEach(filterProperties -> { - Map args = new LinkedHashMap<>(filterProperties.getArgs()); - translate(filterOperations, filterProperties.getName(), args, HandlerFilterFunction.class, builder::filter); - }); - - builder.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, routeId); - - return builder.build(); - } - - private void translate(MultiValueMap operations, String operationName, - Map operationArgs, Class returnType, Consumer operationHandler) { - String normalizedName = StringUtils.uncapitalize(operationName); - Optional operationMethod = findOperation(operations, normalizedName, operationArgs); - if (operationMethod.isPresent()) { - NormalizedOperationMethod opMethod = operationMethod.get(); - T handlerFilterFunction = invokeOperation(opMethod, opMethod.getNormalizedArgs()); - if (handlerFilterFunction != null) { - operationHandler.accept(handlerFilterFunction); - } - } - else { - throw new IllegalArgumentException(String.format("Unable to find operation %s for %s with args %s", - returnType, normalizedName, operationArgs)); - } - } - - private Optional findOperation(MultiValueMap operations, - String operationName, Map operationArgs) { - return operations.getOrDefault(operationName, Collections.emptyList()).stream() - .map(operationMethod -> new NormalizedOperationMethod(operationMethod, operationArgs)) - .filter(opeMethod -> matchOperation(opeMethod, operationArgs)).findFirst(); - } - - private static boolean matchOperation(NormalizedOperationMethod operationMethod, Map args) { - Map normalizedArgs = operationMethod.getNormalizedArgs(); - OperationParameters parameters = operationMethod.getParameters(); - if (operationMethod.isConfigurable()) { - // this is a special case - return true; - } - if (parameters.getParameterCount() != normalizedArgs.size()) { - return false; - } - for (int i = 0; i < parameters.getParameterCount(); i++) { - if (!normalizedArgs.containsKey(parameters.get(i).getName())) { - return false; - } - } - // args contains all parameter names - return true; - } - - private T invokeOperation(OperationMethod operationMethod, Map operationArgs) { - Map args = new HashMap<>(); - if (operationMethod.isConfigurable()) { - OperationParameter operationParameter = operationMethod.getParameters().get(0); - Object config = bindConfigurable(operationMethod, args, operationParameter); - args.put(operationParameter.getName(), config); - } - else { - args.putAll(operationArgs); - } - ReflectiveOperationInvoker operationInvoker = new ReflectiveOperationInvoker(operationMethod, - this.parameterValueMapper); - InvocationContext context = new InvocationContext(args, trueNullOperationArgumentResolver); - return operationInvoker.invoke(context); - } - - private static Object bindConfigurable(OperationMethod operationMethod, Map args, - OperationParameter operationParameter) { - Class configurableType = operationParameter.getType(); - Configurable configurable = operationMethod.getMethod().getAnnotation(Configurable.class); - if (configurable != null && !configurable.value().equals(Void.class)) { - configurableType = configurable.value(); - } - Bindable bindable = Bindable.of(configurableType); - List propertySources = Collections - .singletonList(new MapConfigurationPropertySource(args)); - // TODO: potentially deal with conversion service - Binder binder = new Binder(propertySources, null, DefaultConversionService.getSharedInstance()); - Object config = binder.bindOrCreate("", bindable, new IgnoreTopLevelConverterNotFoundBindHandler()); - return config; - } - - static class TrueNullOperationArgumentResolver implements OperationArgumentResolver { - - @Override - public boolean canResolve(Class type) { - return true; - } - - @Override - public T resolve(Class type) { - return null; - } - - } - /** * Simply holds the composite gateway RouterFunction. This class can be refresh scope * without fear of having multiple RouterFunction mappings. diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/RouterFunctionHolderFactory.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/RouterFunctionHolderFactory.java new file mode 100644 index 00000000..2da4fbbe --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/RouterFunctionHolderFactory.java @@ -0,0 +1,315 @@ +/* + * Copyright 2013-2024 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.cloud.gateway.server.mvc.config; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.bind.handler.IgnoreTopLevelConverterNotFoundBindHandler; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.cloud.gateway.server.mvc.common.Configurable; +import org.springframework.cloud.gateway.server.mvc.common.MvcUtils; +import org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions; +import org.springframework.cloud.gateway.server.mvc.filter.FilterDiscoverer; +import org.springframework.cloud.gateway.server.mvc.handler.HandlerDiscoverer; +import org.springframework.cloud.gateway.server.mvc.invoke.InvocationContext; +import org.springframework.cloud.gateway.server.mvc.invoke.OperationArgumentResolver; +import org.springframework.cloud.gateway.server.mvc.invoke.OperationParameter; +import org.springframework.cloud.gateway.server.mvc.invoke.OperationParameters; +import org.springframework.cloud.gateway.server.mvc.invoke.ParameterValueMapper; +import org.springframework.cloud.gateway.server.mvc.invoke.convert.ConversionServiceParameterValueMapper; +import org.springframework.cloud.gateway.server.mvc.invoke.reflect.OperationMethod; +import org.springframework.cloud.gateway.server.mvc.invoke.reflect.ReflectiveOperationInvoker; +import org.springframework.cloud.gateway.server.mvc.predicate.PredicateDiscoverer; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.env.Environment; +import org.springframework.core.log.LogMessage; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.function.HandlerFilterFunction; +import org.springframework.web.servlet.function.HandlerFunction; +import org.springframework.web.servlet.function.RequestPredicate; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.ServerResponse; + +import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; + +/** + * Factory bean for the creation of a RouterFunctionHolder, that may have refresh scope. + * + * @author Spencer Gibb + * @author Jürgen Wißkirchen + */ +public class RouterFunctionHolderFactory { + + private static final RequestPredicate neverPredicate = new RequestPredicate() { + @Override + public boolean test(ServerRequest request) { + return false; + } + + @Override + public String toString() { + return "Never"; + } + }; + + private static final RouterFunction NEVER_ROUTE = RouterFunctions.route(neverPredicate, + request -> ServerResponse.notFound().build()); + + private final Log log = LogFactory.getLog(getClass()); + + private final TrueNullOperationArgumentResolver trueNullOperationArgumentResolver = new TrueNullOperationArgumentResolver(); + + private final Environment env; + + private final FilterDiscoverer filterDiscoverer = new FilterDiscoverer(); + + private final HandlerDiscoverer handlerDiscoverer = new HandlerDiscoverer(); + + private final PredicateDiscoverer predicateDiscoverer = new PredicateDiscoverer(); + + private final ParameterValueMapper parameterValueMapper = new ConversionServiceParameterValueMapper(); + + public RouterFunctionHolderFactory(Environment env) { + this.env = env; + } + + /** + * supplier for RouterFunctionHolder, which is registered as factory method on the + * bean definition. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private GatewayMvcPropertiesBeanDefinitionRegistrar.RouterFunctionHolder routerFunctionHolderSupplier() { + GatewayMvcProperties properties = Binder.get(env).bindOrCreate(GatewayMvcProperties.PREFIX, + GatewayMvcProperties.class); + log.trace(LogMessage.format("RouterFunctionHolder initializing with %d map routes and %d list routes", + properties.getRoutesMap().size(), properties.getRoutes().size())); + + Map routerFunctions = new LinkedHashMap<>(); + properties.getRoutes().forEach(routeProperties -> { + routerFunctions.put(routeProperties.getId(), getRouterFunction(routeProperties, routeProperties.getId())); + }); + properties.getRoutesMap().forEach((routeId, routeProperties) -> { + String computedRouteId = routeId; + if (StringUtils.hasText(routeProperties.getId())) { + computedRouteId = routeProperties.getId(); + } + routerFunctions.put(computedRouteId, getRouterFunction(routeProperties, computedRouteId)); + }); + RouterFunction routerFunction; + if (routerFunctions.isEmpty()) { + // no properties routes, so a RouterFunction that will never match + routerFunction = NEVER_ROUTE; + } + else { + routerFunction = routerFunctions.values().stream().reduce(RouterFunction::andOther).orElse(null); + // puts the map of configured RouterFunctions in an attribute. Makes testing + // easy. + routerFunction = routerFunction.withAttribute("gatewayRouterFunctions", routerFunctions); + } + log.trace(LogMessage.format("RouterFunctionHolder initialized %s", routerFunction.toString())); + return new GatewayMvcPropertiesBeanDefinitionRegistrar.RouterFunctionHolder(routerFunction); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private RouterFunction getRouterFunction(RouteProperties routeProperties, String routeId) { + log.trace(LogMessage.format("Creating route for : %s", routeProperties)); + + RouterFunctions.Builder builder = route(routeId); + + // MVC.fn users won't need this anonymous filter as url will be set directly. + // Put this function first, so if a filter from a handler changes the url + // it is after this one. + builder.filter((request, next) -> { + MvcUtils.setRequestUrl(request, routeProperties.getUri()); + return next.handle(request); + }); + builder.before(BeforeFilterFunctions.routeId(routeId)); + + MultiValueMap handlerOperations = handlerDiscoverer.getOperations(); + // TODO: cache? + // translate handlerFunction + String scheme = routeProperties.getUri().getScheme(); + Map handlerArgs = new HashMap<>(); + Optional handlerOperationMethod = findOperation(handlerOperations, + scheme.toLowerCase(), handlerArgs); + if (handlerOperationMethod.isEmpty()) { + // single RouteProperties param + handlerArgs.clear(); + String routePropsKey = StringUtils.uncapitalize(RouteProperties.class.getSimpleName()); + handlerArgs.put(routePropsKey, routeProperties); + handlerOperationMethod = findOperation(handlerOperations, scheme.toLowerCase(), handlerArgs); + if (handlerOperationMethod.isEmpty()) { + throw new IllegalStateException("Unable to find HandlerFunction for scheme: " + scheme); + } + } + NormalizedOperationMethod normalizedOpMethod = handlerOperationMethod.get(); + Object response = invokeOperation(normalizedOpMethod, normalizedOpMethod.getNormalizedArgs()); + HandlerFunction handlerFunction = null; + if (response instanceof HandlerFunction) { + handlerFunction = (HandlerFunction) response; + } + else if (response instanceof HandlerDiscoverer.Result result) { + handlerFunction = result.getHandlerFunction(); + result.getFilters().forEach(builder::filter); + } + if (handlerFunction == null) { + throw new IllegalStateException( + "Unable to find HandlerFunction for scheme: " + scheme + " and response " + response); + } + + // translate predicates + MultiValueMap predicateOperations = predicateDiscoverer.getOperations(); + final AtomicReference predicate = new AtomicReference<>(); + + routeProperties.getPredicates().forEach(predicateProperties -> { + Map args = new LinkedHashMap<>(predicateProperties.getArgs()); + translate(predicateOperations, predicateProperties.getName(), args, RequestPredicate.class, + requestPredicate -> { + log.trace(LogMessage.format("Adding predicate to route %s - %s", routeId, predicateProperties)); + if (predicate.get() == null) { + predicate.set(requestPredicate); + } + else { + RequestPredicate combined = predicate.get().and(requestPredicate); + predicate.set(combined); + } + log.trace(LogMessage.format("Combined predicate for route %s - %s", routeId, predicate.get())); + }); + }); + + // combine predicate and handlerFunction + builder.route(predicate.get(), handlerFunction); + predicate.set(null); + + // translate filters + MultiValueMap filterOperations = filterDiscoverer.getOperations(); + routeProperties.getFilters().forEach(filterProperties -> { + Map args = new LinkedHashMap<>(filterProperties.getArgs()); + translate(filterOperations, filterProperties.getName(), args, HandlerFilterFunction.class, builder::filter); + }); + + builder.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, routeId); + + return builder.build(); + } + + private void translate(MultiValueMap operations, String operationName, + Map operationArgs, Class returnType, Consumer operationHandler) { + String normalizedName = StringUtils.uncapitalize(operationName); + Optional operationMethod = findOperation(operations, normalizedName, operationArgs); + if (operationMethod.isPresent()) { + NormalizedOperationMethod opMethod = operationMethod.get(); + T handlerFilterFunction = invokeOperation(opMethod, opMethod.getNormalizedArgs()); + if (handlerFilterFunction != null) { + operationHandler.accept(handlerFilterFunction); + } + } + else { + throw new IllegalArgumentException(String.format("Unable to find operation %s for %s with args %s", + returnType, normalizedName, operationArgs)); + } + } + + private Optional findOperation(MultiValueMap operations, + String operationName, Map operationArgs) { + return operations.getOrDefault(operationName, Collections.emptyList()).stream() + .map(operationMethod -> new NormalizedOperationMethod(operationMethod, operationArgs)) + .filter(opeMethod -> matchOperation(opeMethod, operationArgs)).findFirst(); + } + + private static boolean matchOperation(NormalizedOperationMethod operationMethod, Map args) { + Map normalizedArgs = operationMethod.getNormalizedArgs(); + OperationParameters parameters = operationMethod.getParameters(); + if (operationMethod.isConfigurable()) { + // this is a special case + return true; + } + if (parameters.getParameterCount() != normalizedArgs.size()) { + return false; + } + for (int i = 0; i < parameters.getParameterCount(); i++) { + if (!normalizedArgs.containsKey(parameters.get(i).getName())) { + return false; + } + } + // args contains all parameter names + return true; + } + + private T invokeOperation(OperationMethod operationMethod, Map operationArgs) { + Map args = new HashMap<>(); + if (operationMethod.isConfigurable()) { + OperationParameter operationParameter = operationMethod.getParameters().get(0); + Object config = bindConfigurable(operationMethod, args, operationParameter); + args.put(operationParameter.getName(), config); + } + else { + args.putAll(operationArgs); + } + ReflectiveOperationInvoker operationInvoker = new ReflectiveOperationInvoker(operationMethod, + this.parameterValueMapper); + InvocationContext context = new InvocationContext(args, trueNullOperationArgumentResolver); + return operationInvoker.invoke(context); + } + + private static Object bindConfigurable(OperationMethod operationMethod, Map args, + OperationParameter operationParameter) { + Class configurableType = operationParameter.getType(); + Configurable configurable = operationMethod.getMethod().getAnnotation(Configurable.class); + if (configurable != null && !configurable.value().equals(Void.class)) { + configurableType = configurable.value(); + } + Bindable bindable = Bindable.of(configurableType); + List propertySources = Collections + .singletonList(new MapConfigurationPropertySource(args)); + // TODO: potentially deal with conversion service + Binder binder = new Binder(propertySources, null, DefaultConversionService.getSharedInstance()); + Object config = binder.bindOrCreate("", bindable, new IgnoreTopLevelConverterNotFoundBindHandler()); + return config; + } + + static class TrueNullOperationArgumentResolver implements OperationArgumentResolver { + + @Override + public boolean canResolve(Class type) { + return true; + } + + @Override + public T resolve(Class type) { + return null; + } + + } + +}