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
This commit is contained in:
committed by
GitHub
parent
8c9fcdd4ea
commit
fbb72e2164
@@ -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);
|
||||
|
||||
@@ -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<Class<?>> 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<Class<?>> 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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ServerResponse> 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<String, RouterFunction> 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<String, OperationMethod> handlerOperations = handlerDiscoverer.getOperations();
|
||||
// TODO: cache?
|
||||
// translate handlerFunction
|
||||
String scheme = routeProperties.getUri().getScheme();
|
||||
Map<String, Object> handlerArgs = new HashMap<>();
|
||||
Optional<NormalizedOperationMethod> 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<ServerResponse> handlerFunction = null;
|
||||
if (response instanceof HandlerFunction<?>) {
|
||||
handlerFunction = (HandlerFunction<ServerResponse>) 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<String, OperationMethod> predicateOperations = predicateDiscoverer.getOperations();
|
||||
final AtomicReference<RequestPredicate> predicate = new AtomicReference<>();
|
||||
|
||||
routeProperties.getPredicates().forEach(predicateProperties -> {
|
||||
Map<String, Object> 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<String, OperationMethod> filterOperations = filterDiscoverer.getOperations();
|
||||
routeProperties.getFilters().forEach(filterProperties -> {
|
||||
Map<String, Object> 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 <T> void translate(MultiValueMap<String, OperationMethod> operations, String operationName,
|
||||
Map<String, Object> operationArgs, Class<T> returnType, Consumer<T> operationHandler) {
|
||||
String normalizedName = StringUtils.uncapitalize(operationName);
|
||||
Optional<NormalizedOperationMethod> 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<NormalizedOperationMethod> findOperation(MultiValueMap<String, OperationMethod> operations,
|
||||
String operationName, Map<String, Object> 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<String, Object> args) {
|
||||
Map<String, Object> 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> T invokeOperation(OperationMethod operationMethod, Map<String, Object> operationArgs) {
|
||||
Map<String, Object> 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<String, Object> 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<ConfigurationPropertySource> 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> T resolve(Class<T> type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply holds the composite gateway RouterFunction. This class can be refresh scope
|
||||
* without fear of having multiple RouterFunction mappings.
|
||||
|
||||
@@ -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<ServerResponse> 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<String, RouterFunction> 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<String, OperationMethod> handlerOperations = handlerDiscoverer.getOperations();
|
||||
// TODO: cache?
|
||||
// translate handlerFunction
|
||||
String scheme = routeProperties.getUri().getScheme();
|
||||
Map<String, Object> handlerArgs = new HashMap<>();
|
||||
Optional<NormalizedOperationMethod> 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<ServerResponse> handlerFunction = null;
|
||||
if (response instanceof HandlerFunction<?>) {
|
||||
handlerFunction = (HandlerFunction<ServerResponse>) 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<String, OperationMethod> predicateOperations = predicateDiscoverer.getOperations();
|
||||
final AtomicReference<RequestPredicate> predicate = new AtomicReference<>();
|
||||
|
||||
routeProperties.getPredicates().forEach(predicateProperties -> {
|
||||
Map<String, Object> 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<String, OperationMethod> filterOperations = filterDiscoverer.getOperations();
|
||||
routeProperties.getFilters().forEach(filterProperties -> {
|
||||
Map<String, Object> 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 <T> void translate(MultiValueMap<String, OperationMethod> operations, String operationName,
|
||||
Map<String, Object> operationArgs, Class<T> returnType, Consumer<T> operationHandler) {
|
||||
String normalizedName = StringUtils.uncapitalize(operationName);
|
||||
Optional<NormalizedOperationMethod> 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<NormalizedOperationMethod> findOperation(MultiValueMap<String, OperationMethod> operations,
|
||||
String operationName, Map<String, Object> 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<String, Object> args) {
|
||||
Map<String, Object> 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> T invokeOperation(OperationMethod operationMethod, Map<String, Object> operationArgs) {
|
||||
Map<String, Object> 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<String, Object> 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<ConfigurationPropertySource> 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> T resolve(Class<T> type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user