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:
sgibb
2023-12-19 19:12:05 -05:00
parent 88662b6708
commit e360c22f4a
16 changed files with 188 additions and 44 deletions

View File

@@ -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);
}
}
}

View File

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

View File

@@ -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

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -26,4 +26,8 @@ public interface OperationMethod {
OperationParameters getParameters();
default boolean isConfigurable() {
return false;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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() {

View File

@@ -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