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; + } + + } + +}