Adds support for complex Configurable factory methods.
Adds @Configurable to signify that this is a complex config object and use boot binding to create and populate it. Retry and CircuitBreaker are initially supported. Adds SimpleFilterSupplier to ease creation of FilterSupplier objects. Fixes gh-3172
This commit is contained in:
@@ -48,9 +48,12 @@ public abstract class AbstractGatewayDiscoverer {
|
||||
}
|
||||
catch (NoClassDefFoundError e) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(LogMessage.format("NoClassDefFoundError discovering supplier %s for type %s", supplierClass, returnType));
|
||||
} else if (log.isTraceEnabled()) {
|
||||
log.debug(LogMessage.format("NoClassDefFoundError discovering supplier %s for type %s", supplierClass, returnType), e);
|
||||
log.debug(LogMessage.format("NoClassDefFoundError discovering supplier %s for type %s",
|
||||
supplierClass, returnType));
|
||||
}
|
||||
else if (log.isTraceEnabled()) {
|
||||
log.debug(LogMessage.format("NoClassDefFoundError discovering supplier %s for type %s",
|
||||
supplierClass, returnType), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.common;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.stereotype.Indexed;
|
||||
|
||||
@Target({ ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Indexed
|
||||
public @interface Configurable {
|
||||
|
||||
Class<?> value() default Void.class;
|
||||
|
||||
}
|
||||
@@ -19,6 +19,7 @@ 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;
|
||||
@@ -32,13 +33,19 @@ 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;
|
||||
@@ -46,6 +53,7 @@ import org.springframework.cloud.gateway.server.mvc.invoke.reflect.OperationMeth
|
||||
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;
|
||||
@@ -264,6 +272,10 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrar implements ImportBeanDe
|
||||
private static boolean matchOperation(NormalizedOperationMethod operationMethod, Map<String, String> args) {
|
||||
Map<String, String> normalizedArgs = operationMethod.getNormalizedArgs();
|
||||
OperationParameters parameters = operationMethod.getParameters();
|
||||
if (operationMethod.isConfigurable()) {
|
||||
// this is a special case
|
||||
return true;
|
||||
}
|
||||
if (parameters.getParameterCount() != normalizedArgs.size()) {
|
||||
return false;
|
||||
}
|
||||
@@ -277,13 +289,37 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrar implements ImportBeanDe
|
||||
}
|
||||
|
||||
private <T> T invokeOperation(OperationMethod operationMethod, Map<String, String> operationArgs) {
|
||||
Map<String, Object> args = new HashMap<>(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
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.cloud.gateway.server.mvc.common.Configurable;
|
||||
import org.springframework.cloud.gateway.server.mvc.common.NameUtils;
|
||||
import org.springframework.cloud.gateway.server.mvc.common.Shortcut;
|
||||
import org.springframework.cloud.gateway.server.mvc.invoke.OperationParameter;
|
||||
@@ -50,6 +51,11 @@ public class NormalizedOperationMethod implements OperationMethod {
|
||||
return delegate.getMethod();
|
||||
}
|
||||
|
||||
public boolean isConfigurable() {
|
||||
Configurable annotation = delegate.getMethod().getAnnotation(Configurable.class);
|
||||
return annotation != null && delegate.getParameters().getParameterCount() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OperationParameters getParameters() {
|
||||
return delegate.getParameters();
|
||||
|
||||
@@ -16,10 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.gateway.server.mvc.filter;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
@@ -189,11 +186,10 @@ public abstract class Bucket4jFilterFunctions {
|
||||
|
||||
}
|
||||
|
||||
static class FilterSupplier implements org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier {
|
||||
public static class FilterSupplier extends SimpleFilterSupplier {
|
||||
|
||||
@Override
|
||||
public Collection<Method> get() {
|
||||
return Arrays.asList(Bucket4jFilterFunctions.class.getMethods());
|
||||
public FilterSupplier() {
|
||||
super(Bucket4jFilterFunctions.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,10 +17,8 @@
|
||||
package org.springframework.cloud.gateway.server.mvc.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
@@ -32,6 +30,7 @@ import jakarta.servlet.ServletException;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.gateway.server.mvc.common.Configurable;
|
||||
import org.springframework.cloud.gateway.server.mvc.common.HttpStatusHolder;
|
||||
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
|
||||
import org.springframework.cloud.gateway.server.mvc.common.Shortcut;
|
||||
@@ -66,6 +65,12 @@ public abstract class CircuitBreakerFilterFunctions {
|
||||
Consumer<CircuitBreakerConfig> configConsumer) {
|
||||
CircuitBreakerConfig config = new CircuitBreakerConfig();
|
||||
configConsumer.accept(config);
|
||||
return circuitBreaker(config);
|
||||
}
|
||||
|
||||
@Shortcut
|
||||
@Configurable
|
||||
public static HandlerFilterFunction<ServerResponse, ServerResponse> circuitBreaker(CircuitBreakerConfig config) {
|
||||
Set<HttpStatusCode> failureStatuses = config.getStatusCodes().stream()
|
||||
.map(status -> HttpStatusHolder.valueOf(status).resolve()).collect(Collectors.toSet());
|
||||
return (request, next) -> {
|
||||
@@ -191,11 +196,10 @@ public abstract class CircuitBreakerFilterFunctions {
|
||||
|
||||
}
|
||||
|
||||
public static class FilterSupplier implements org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier {
|
||||
public static class FilterSupplier extends SimpleFilterSupplier {
|
||||
|
||||
@Override
|
||||
public Collection<Method> get() {
|
||||
return Arrays.asList(CircuitBreakerFilterFunctions.class.getMethods());
|
||||
public FilterSupplier() {
|
||||
super(CircuitBreakerFilterFunctions.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,10 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.gateway.server.mvc.filter;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.cloud.gateway.server.mvc.common.HttpStatusHolder;
|
||||
@@ -228,11 +225,10 @@ public interface FilterFunctions {
|
||||
return ofResponseProcessor(AfterFilterFunctions.setStatus(statusCode));
|
||||
}
|
||||
|
||||
class FilterSupplier implements org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier {
|
||||
class FilterSupplier extends SimpleFilterSupplier {
|
||||
|
||||
@Override
|
||||
public Collection<Method> get() {
|
||||
return Arrays.asList(FilterFunctions.class.getMethods());
|
||||
public FilterSupplier() {
|
||||
super(FilterFunctions.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import java.util.Set;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.cloud.gateway.server.mvc.common.Configurable;
|
||||
import org.springframework.cloud.gateway.server.mvc.common.Shortcut;
|
||||
import org.springframework.core.NestedRuntimeException;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -53,6 +55,12 @@ public abstract class RetryFilterFunctions {
|
||||
public static HandlerFilterFunction<ServerResponse, ServerResponse> retry(Consumer<RetryConfig> configConsumer) {
|
||||
RetryConfig config = new RetryConfig();
|
||||
configConsumer.accept(config);
|
||||
return retry(config);
|
||||
}
|
||||
|
||||
@Shortcut
|
||||
@Configurable
|
||||
public static HandlerFilterFunction<ServerResponse, ServerResponse> retry(RetryConfig config) {
|
||||
RetryTemplateBuilder retryTemplateBuilder = RetryTemplate.builder();
|
||||
CompositeRetryPolicy compositeRetryPolicy = new CompositeRetryPolicy();
|
||||
Map<Class<? extends Throwable>, Boolean> retryableExceptions = new HashMap<>();
|
||||
@@ -191,4 +199,12 @@ public abstract class RetryFilterFunctions {
|
||||
|
||||
}
|
||||
|
||||
public static class FilterSupplier extends SimpleFilterSupplier {
|
||||
|
||||
public FilterSupplier() {
|
||||
super(RetryFilterFunctions.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.filter;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
public class SimpleFilterSupplier implements FilterSupplier {
|
||||
|
||||
private final Class<?> filtersClass;
|
||||
|
||||
public SimpleFilterSupplier(Class<?> filtersClass) {
|
||||
this.filtersClass = filtersClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Method> get() {
|
||||
return Arrays.asList(filtersClass.getMethods());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,10 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.gateway.server.mvc.filter;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.security.Principal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.cloud.gateway.server.mvc.common.Shortcut;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
|
||||
@@ -58,11 +55,10 @@ public abstract class TokenRelayFilterFunctions {
|
||||
};
|
||||
}
|
||||
|
||||
class FilterSupplier implements org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier {
|
||||
public static class FilterSupplier extends SimpleFilterSupplier {
|
||||
|
||||
@Override
|
||||
public Collection<Method> get() {
|
||||
return Arrays.asList(TokenRelayFilterFunctions.class.getMethods());
|
||||
public FilterSupplier() {
|
||||
super(TokenRelayFilterFunctions.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -73,6 +73,10 @@ public abstract class HandlerFunctions {
|
||||
return new LookupProxyExchangeHandlerFunction();
|
||||
}
|
||||
|
||||
public static HandlerFunction<ServerResponse> no() {
|
||||
return http();
|
||||
}
|
||||
|
||||
static class LookupProxyExchangeHandlerFunction implements HandlerFunction<ServerResponse> {
|
||||
|
||||
private final URI uri;
|
||||
|
||||
@@ -26,4 +26,8 @@ public interface OperationMethod {
|
||||
|
||||
OperationParameters getParameters();
|
||||
|
||||
default boolean isConfigurable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier=\
|
||||
org.springframework.cloud.gateway.server.mvc.filter.Bucket4jFilterFunctions.FilterSupplier,\
|
||||
org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions.FilterSupplier,\
|
||||
org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.FilterSupplier,\
|
||||
org.springframework.cloud.gateway.server.mvc.filter.TokenRelayFilterFunctions.FilterSupplier,\
|
||||
org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.FilterSupplier
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
|
||||
Map<String, RouterFunction> routerFunctions = getRouterFunctions(context);
|
||||
|
||||
assertThat(routerFunctions).hasSizeGreaterThanOrEqualTo(5).containsKeys("listRoute1", "route1",
|
||||
"route2CustomId", "listRoute2", "listRoute3");
|
||||
"route2CustomId", "listRoute2", "listRoute3", "listRoute4");
|
||||
RouterFunction listRoute1RouterFunction = routerFunctions.get("listRoute1");
|
||||
listRoute1RouterFunction.accept(new AbstractRouterFunctionsVisitor() {
|
||||
@Override
|
||||
@@ -176,7 +176,7 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
void refreshWorks(ConfigurableApplicationContext context) {
|
||||
Map<String, RouterFunction> routerFunctions = getRouterFunctions(context);
|
||||
assertThat(routerFunctions).hasSize(5);
|
||||
assertThat(routerFunctions).hasSize(6);
|
||||
TestPropertyValues.of("spring.cloud.gateway.mvc.routesMap.route3.uri=https://example3.com",
|
||||
"spring.cloud.gateway.mvc.routesMap.route3.predicates[0].name=Path",
|
||||
"spring.cloud.gateway.mvc.routesMap.route3.predicates[0].args.pattern=/anything/mapRoute3",
|
||||
@@ -196,7 +196,7 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
|
||||
GatewayMvcProperties properties = context.getBean(GatewayMvcProperties.class);
|
||||
assertThat(properties.getRoutesMap()).hasSize(3).containsKey("route3");
|
||||
routerFunctions = getRouterFunctions(context);
|
||||
assertThat(routerFunctions).hasSize(6);
|
||||
assertThat(routerFunctions).hasSize(7);
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
|
||||
@@ -16,19 +16,14 @@
|
||||
|
||||
package org.springframework.cloud.gateway.server.mvc.test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier;
|
||||
import org.springframework.cloud.gateway.server.mvc.filter.SimpleFilterSupplier;
|
||||
import org.springframework.web.servlet.function.HandlerFilterFunction;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
public class TestFilterSupplier implements FilterSupplier {
|
||||
public class TestFilterSupplier extends SimpleFilterSupplier {
|
||||
|
||||
@Override
|
||||
public Collection<Method> get() {
|
||||
return Arrays.asList(TestFilterSupplier.class.getMethods());
|
||||
public TestFilterSupplier() {
|
||||
super(TestFilterSupplier.class);
|
||||
}
|
||||
|
||||
public static HandlerFilterFunction<ServerResponse, ServerResponse> localServerPortUriResolver() {
|
||||
|
||||
@@ -26,7 +26,7 @@ spring.cloud.gateway.mvc:
|
||||
- HttpbinUriResolver=
|
||||
- AddRequestHeader=X-Test,listRoute1
|
||||
- id: listRoute2
|
||||
uri: https://examplel2.com
|
||||
uri: no://op
|
||||
predicates:
|
||||
- Method=GET,POST
|
||||
- Path=/anything/listRoute2
|
||||
@@ -44,6 +44,22 @@ spring.cloud.gateway.mvc:
|
||||
args:
|
||||
name: X-Test
|
||||
values: listRoute3
|
||||
- id: listRoute4
|
||||
uri: https://example1.com
|
||||
predicates:
|
||||
- Path=/anything/example1
|
||||
filters:
|
||||
- name: Retry
|
||||
args:
|
||||
retries: 3
|
||||
series: SERVER_ERROR
|
||||
methods: GET,POST
|
||||
- name: CircuitBreaker
|
||||
args:
|
||||
id: mycb
|
||||
statusCodes: 500,501
|
||||
fallbackpath: GET,POST
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.springframework.cloud.gateway.server.mvc: TRACE
|
||||
|
||||
Reference in New Issue
Block a user