Adds a weight filter to add weighted routing to groups of routes.
fixes gh-67
This commit is contained in:
@@ -36,6 +36,7 @@ import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint;
|
||||
import org.springframework.cloud.gateway.filter.ForwardRoutingFilter;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter;
|
||||
import org.springframework.cloud.gateway.filter.NettyRoutingFilter;
|
||||
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
|
||||
import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter;
|
||||
@@ -81,6 +82,7 @@ import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFac
|
||||
import org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.RemoteAddrRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.WeightRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.route.CachingRouteLocator;
|
||||
import org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator;
|
||||
import org.springframework.cloud.gateway.route.CompositeRouteLocator;
|
||||
@@ -96,8 +98,10 @@ import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.reactive.DispatcherHandler;
|
||||
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
|
||||
import org.springframework.web.reactive.socket.client.WebSocketClient;
|
||||
@@ -323,6 +327,11 @@ public class GatewayAutoConfiguration {
|
||||
return new WebsocketRoutingFilter(webSocketClient, webSocketService, headersFilters);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WeightCalculatorWebFilter weightCalculatorWebFilter(Validator validator) {
|
||||
return new WeightCalculatorWebFilter(validator);
|
||||
}
|
||||
|
||||
/*@Bean
|
||||
//TODO: default over netty? configurable
|
||||
public WebClientHttpRoutingFilter webClientHttpRoutingFilter() {
|
||||
@@ -387,6 +396,12 @@ public class GatewayAutoConfiguration {
|
||||
return new RemoteAddrRoutePredicateFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@DependsOn("weightCalculatorWebFilter")
|
||||
public WeightRoutePredicateFactory weightRoutePredicateFactory() {
|
||||
return new WeightRoutePredicateFactory();
|
||||
}
|
||||
|
||||
// GatewayFilter Factory beans
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2013-2018 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
|
||||
*
|
||||
* http://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.event;
|
||||
|
||||
import org.springframework.cloud.gateway.support.WeightConfig;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
public class WeightDefinedEvent extends ApplicationEvent {
|
||||
private final WeightConfig weightConfig;
|
||||
|
||||
public WeightDefinedEvent(Object source, WeightConfig weightConfig) {
|
||||
super(source);
|
||||
this.weightConfig = weightConfig;
|
||||
}
|
||||
|
||||
public WeightConfig getWeightConfig() {
|
||||
return weightConfig;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright 2013-2018 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
|
||||
*
|
||||
* http://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.filter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.cloud.gateway.event.PredicateArgsEvent;
|
||||
import org.springframework.cloud.gateway.event.WeightDefinedEvent;
|
||||
import org.springframework.cloud.gateway.support.ConfigurationUtils;
|
||||
import org.springframework.cloud.gateway.support.WeightConfig;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.event.SmartApplicationListener;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.style.ToStringCreator;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.WEIGHT_ATTR;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class WeightCalculatorWebFilter implements WebFilter, Ordered, SmartApplicationListener {
|
||||
|
||||
private static final Log log = LogFactory.getLog(WeightCalculatorWebFilter.class);
|
||||
|
||||
public static final int WEIGHT_CALC_FILTER_ORDER = 10001;
|
||||
|
||||
private final Validator validator;
|
||||
private Random random = new Random();
|
||||
private int order = WEIGHT_CALC_FILTER_ORDER;
|
||||
|
||||
private Map<String, GroupWeightConfig> groupWeights = new ConcurrentHashMap<>();
|
||||
|
||||
/* for testing */ WeightCalculatorWebFilter() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public WeightCalculatorWebFilter(Validator validator) {
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return order;
|
||||
}
|
||||
|
||||
public void setOrder(int order) {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
public void setRandom(Random random) {
|
||||
this.random = random;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
|
||||
return PredicateArgsEvent.class.isAssignableFrom(eventType) || // config file
|
||||
WeightDefinedEvent.class.isAssignableFrom(eventType); // java dsl
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSourceType(Class<?> sourceType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
if (event instanceof PredicateArgsEvent) {
|
||||
handle((PredicateArgsEvent) event);
|
||||
} else if (event instanceof WeightDefinedEvent) {
|
||||
addWeightConfig(((WeightDefinedEvent)event).getWeightConfig());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void handle(PredicateArgsEvent event) {
|
||||
Map<String, Object> args = event.getArgs();
|
||||
|
||||
if (args.isEmpty() || !hasRelevantKey(args)) {
|
||||
return;
|
||||
}
|
||||
|
||||
WeightConfig config = new WeightConfig(event.getRouteId());
|
||||
|
||||
ConfigurationUtils.bind(config, args,
|
||||
WeightConfig.CONFIG_PREFIX, WeightConfig.CONFIG_PREFIX, validator);
|
||||
|
||||
addWeightConfig(config);
|
||||
}
|
||||
|
||||
private boolean hasRelevantKey(Map<String, Object> args) {
|
||||
return args.keySet().stream()
|
||||
.anyMatch(key -> key.startsWith(WeightConfig.CONFIG_PREFIX + "."));
|
||||
}
|
||||
|
||||
/* for testing */ void addWeightConfig(WeightConfig weightConfig) {
|
||||
String group = weightConfig.getGroup();
|
||||
GroupWeightConfig c = groupWeights.get(group);
|
||||
if (c == null) {
|
||||
c = new GroupWeightConfig(group);
|
||||
groupWeights.put(group, c);
|
||||
}
|
||||
GroupWeightConfig config = c;
|
||||
config.weights.put(weightConfig.getRouteId(), weightConfig.getWeight());
|
||||
|
||||
//recalculate
|
||||
|
||||
// normalize weights
|
||||
int weightsSum = config.weights.values().stream().mapToInt(Integer::intValue).sum();
|
||||
|
||||
final AtomicInteger index = new AtomicInteger(0);
|
||||
config.weights.forEach((routeId, weight) -> {
|
||||
Double nomalizedWeight = weight / (double) weightsSum;
|
||||
config.normalizedWeights.put(routeId, nomalizedWeight);
|
||||
|
||||
// recalculate rangeIndexes
|
||||
config.rangeIndexes.put(index.getAndIncrement(), routeId);
|
||||
});
|
||||
|
||||
//TODO: calculate ranges
|
||||
config.ranges.clear();
|
||||
|
||||
config.ranges.add(0.0);
|
||||
|
||||
List<Double> values = new ArrayList<>(config.normalizedWeights.values());
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
Double currentWeight = values.get(i);
|
||||
Double previousRange = config.ranges.get(i);
|
||||
Double range = previousRange + currentWeight;
|
||||
config.ranges.add(range);
|
||||
}
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Recalculated group weight config "+ config);
|
||||
}
|
||||
}
|
||||
|
||||
/* for testing */ Map<String, GroupWeightConfig> getGroupWeights() {
|
||||
return groupWeights;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
Map<String, String> weights = getWeights(exchange);
|
||||
|
||||
groupWeights.forEach((group, config) -> {
|
||||
double r = this.random.nextDouble();
|
||||
|
||||
List<Double> ranges = config.ranges;
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Weight for group: "+group +", ranges: "+ranges +", r: "+r);
|
||||
}
|
||||
|
||||
for (int i = 0; i < ranges.size() - 1; i++) {
|
||||
if (r >= ranges.get(i) && r < ranges.get(i+1)) {
|
||||
String routeId = config.rangeIndexes.get(i);
|
||||
weights.put(group, routeId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Weights attr: "+weights);
|
||||
}
|
||||
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
/* for testing */ static Map<String, String> getWeights(ServerWebExchange exchange) {
|
||||
Map<String, String> weights = exchange.getAttribute(WEIGHT_ATTR);
|
||||
|
||||
if (weights == null) {
|
||||
weights = new ConcurrentHashMap<>();
|
||||
exchange.getAttributes().put(WEIGHT_ATTR, weights);
|
||||
}
|
||||
return weights;
|
||||
}
|
||||
|
||||
/* for testing */ static class GroupWeightConfig {
|
||||
String group;
|
||||
|
||||
LinkedHashMap<String, Integer> weights = new LinkedHashMap<>();
|
||||
|
||||
LinkedHashMap<String, Double> normalizedWeights = new LinkedHashMap<>();
|
||||
|
||||
LinkedHashMap<Integer, String> rangeIndexes = new LinkedHashMap<>();
|
||||
List<Double> ranges = new ArrayList<>();
|
||||
|
||||
GroupWeightConfig(String group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringCreator(this)
|
||||
.append("group", group)
|
||||
.append("weights", weights)
|
||||
.append("normalizedWeights", normalizedWeights)
|
||||
.append("rangeIndexes", rangeIndexes)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import org.springframework.web.reactive.handler.AbstractHandlerMapping;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_HANDLER_MAPPER_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -52,6 +53,7 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
|
||||
return lookupRoute(exchange)
|
||||
// .log("route-predicate-handler-mapping", Level.FINER) //name this
|
||||
.flatMap((Function<Route, Mono<?>>) r -> {
|
||||
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Mapping [" + getExchangeDesc(exchange) + "] to " + r);
|
||||
}
|
||||
@@ -59,6 +61,7 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
|
||||
exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
|
||||
return Mono.just(webHandler);
|
||||
}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
|
||||
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
|
||||
}
|
||||
@@ -84,10 +87,13 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
|
||||
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
|
||||
return this.routeLocator.getRoutes()
|
||||
.filter(route -> route.getPredicate().test(exchange))
|
||||
.filter(route -> {
|
||||
// add the current route we are testing
|
||||
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, route.getId());
|
||||
return route.getPredicate().test(exchange);
|
||||
})
|
||||
// .defaultIfEmpty() put a static Route not found
|
||||
// or .switchIfEmpty()
|
||||
// .switchIfEmpty(Mono.<Route>empty().log("noroute"))
|
||||
|
||||
@@ -42,9 +42,12 @@ public interface RoutePredicateFactory<C> extends ShortcutConfigurable, Configur
|
||||
default Predicate<ServerWebExchange> apply(Consumer<C> consumer) {
|
||||
C config = newConfig();
|
||||
consumer.accept(config);
|
||||
beforeApply(config);
|
||||
return apply(config);
|
||||
}
|
||||
|
||||
default void beforeApply(C config) {}
|
||||
|
||||
//TODO: remove after apply(Tuple) removed
|
||||
@Override
|
||||
default Class<C> getConfigClass() {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2013-2018 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
|
||||
*
|
||||
* http://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.handler.predicate;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.cloud.gateway.event.WeightDefinedEvent;
|
||||
import org.springframework.cloud.gateway.support.WeightConfig;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.WEIGHT_ATTR;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
//TODO: make this a generic Choose out of group predicate?
|
||||
public class WeightRoutePredicateFactory extends AbstractRoutePredicateFactory<WeightConfig> implements ApplicationEventPublisherAware {
|
||||
|
||||
private static final Log log = LogFactory.getLog(WeightRoutePredicateFactory.class);
|
||||
|
||||
public static final String GROUP_KEY = WeightConfig.CONFIG_PREFIX + ".group";
|
||||
public static final String WEIGHT_KEY = WeightConfig.CONFIG_PREFIX + ".weight";
|
||||
|
||||
private ApplicationEventPublisher publisher;
|
||||
|
||||
public WeightRoutePredicateFactory() {
|
||||
super(WeightConfig.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> shortcutFieldOrder() {
|
||||
return Arrays.asList(GROUP_KEY, WEIGHT_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String shortcutFieldPrefix() {
|
||||
return WeightConfig.CONFIG_PREFIX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeApply(WeightConfig config) {
|
||||
if (publisher != null) {
|
||||
publisher.publishEvent(new WeightDefinedEvent(this, config));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predicate<ServerWebExchange> apply(WeightConfig config) {
|
||||
return exchange -> {
|
||||
Map<String, String> weights = exchange.getAttributeOrDefault(WEIGHT_ATTR,
|
||||
Collections.emptyMap());
|
||||
|
||||
String routeId = exchange.getAttribute(GATEWAY_PREDICATE_ROUTE_ATTR);
|
||||
|
||||
// all calculations and comparison against random num happened in
|
||||
// WeightCalculatorWebFilter
|
||||
String group = config.getGroup();
|
||||
if (weights.containsKey(group)) {
|
||||
|
||||
String chosenRoute = weights.get(group);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("in group weight: "+ group + ", current route: " + routeId +", chosen route: " + chosenRoute);
|
||||
}
|
||||
|
||||
return routeId.equals(chosenRoute);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,7 @@ public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAwa
|
||||
Object configuration = factory.newConfig();
|
||||
|
||||
ConfigurationUtils.bind(configuration, properties,
|
||||
"", definition.getName(), validator);
|
||||
factory.shortcutFieldPrefix(), definition.getName(), validator);
|
||||
|
||||
GatewayFilter gatewayFilter = factory.apply(configuration);
|
||||
if (this.publisher != null) {
|
||||
@@ -262,7 +262,7 @@ public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAwa
|
||||
Map<String, Object> properties = factory.shortcutType().normalize(args, factory, this.parser, this.beanFactory);
|
||||
Object config = factory.newConfig();
|
||||
ConfigurationUtils.bind(config, properties,
|
||||
"", predicate.getName(), validator);
|
||||
factory.shortcutFieldPrefix(), predicate.getName(), validator);
|
||||
if (this.publisher != null) {
|
||||
this.publisher.publishEvent(new PredicateArgsEvent(this, route.getId(), properties));
|
||||
}
|
||||
|
||||
@@ -47,8 +47,9 @@ public class BooleanSpec extends UriSpec {
|
||||
return new BooleanOpSpec(routeBuilder, builder, OR);
|
||||
}
|
||||
|
||||
public BooleanOpSpec negate() {
|
||||
return new BooleanOpSpec(routeBuilder, builder, NEGATE);
|
||||
public BooleanSpec negate() {
|
||||
this.routeBuilder.negate();
|
||||
return new BooleanSpec(routeBuilder, builder);
|
||||
}
|
||||
|
||||
public UriSpec filters(Function<GatewayFilterSpec, UriSpec> fn) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.springframework.cloud.gateway.handler.predicate.MethodRoutePredicateF
|
||||
import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.RemoteAddrRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.WeightRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.route.Route;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
@@ -117,6 +118,13 @@ public class PredicateSpec extends UriSpec {
|
||||
.apply(c -> c.setSources(addrs)));
|
||||
}
|
||||
|
||||
public BooleanSpec weight(String group, int weight) {
|
||||
return predicate(getBean(WeightRoutePredicateFactory.class)
|
||||
.apply(c -> c.setGroup(group)
|
||||
.setRouteId(routeBuilder.getId())
|
||||
.setWeight(weight)));
|
||||
}
|
||||
|
||||
public BooleanSpec alwaysTrue() {
|
||||
return predicate(exchange -> true);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ public class ServerWebExchangeUtils {
|
||||
public static final String GATEWAY_ORIGINAL_REQUEST_URL_ATTR = qualify("gatewayOriginalRequestUrl");
|
||||
public static final String GATEWAY_HANDLER_MAPPER_ATTR = qualify("gatewayHandlerMapper");
|
||||
public static final String GATEWAY_SCHEME_PREFIX_ATTR = qualify("gatewaySchemePrefix");
|
||||
public static final String GATEWAY_PREDICATE_ROUTE_ATTR = qualify("gatewayPredicateRouteAttr");
|
||||
public static final String WEIGHT_ATTR = qualify("routeWeight");
|
||||
|
||||
/**
|
||||
* Used when a routing filter has been successfully call. Allows users to write custom
|
||||
|
||||
@@ -115,6 +115,10 @@ public interface ShortcutConfigurable {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
default String shortcutFieldPrefix() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate supplied argument size against {@see #shortcutFieldOrder} size.
|
||||
* Useful for variable arg predicates.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2013-2018 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
|
||||
*
|
||||
* http://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.support;
|
||||
|
||||
import org.springframework.core.style.ToStringCreator;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
@Validated
|
||||
public class WeightConfig {
|
||||
public static final String CONFIG_PREFIX = "weight";
|
||||
|
||||
@NotEmpty
|
||||
private String group;
|
||||
private String routeId;
|
||||
@Min(0)
|
||||
private int weight;
|
||||
|
||||
private WeightConfig() { }
|
||||
|
||||
public WeightConfig(String group, String routeId, int weight) {
|
||||
this.routeId = routeId;
|
||||
this.group = group;
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
public WeightConfig(String routeId) {
|
||||
this.routeId = routeId;
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public WeightConfig setGroup(String group) {
|
||||
this.group = group;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getRouteId() {
|
||||
return routeId;
|
||||
}
|
||||
|
||||
public WeightConfig setRouteId(String routeId) {
|
||||
this.routeId = routeId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getWeight() {
|
||||
return weight;
|
||||
}
|
||||
|
||||
public WeightConfig setWeight(int weight) {
|
||||
this.weight = weight;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringCreator(this)
|
||||
.append("group", group)
|
||||
.append("routeId", routeId)
|
||||
.append("weight", weight)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2013-2018 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
|
||||
*
|
||||
* http://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.filter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.cloud.gateway.event.PredicateArgsEvent;
|
||||
import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.GroupWeightConfig;
|
||||
import org.springframework.cloud.gateway.support.WeightConfig;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class WeightCalculatorWebFilterTests {
|
||||
|
||||
@Test
|
||||
public void testWeightCalculation() {
|
||||
WeightCalculatorWebFilter filter = new WeightCalculatorWebFilter();
|
||||
|
||||
String grp1 = "group1";
|
||||
String grp2 = "group2";
|
||||
int grp1idx = 1;
|
||||
int grp2idx = 1;
|
||||
|
||||
assertWeightCalculation(filter, grp1, grp1idx++, 1, asList(1.0));
|
||||
assertWeightCalculation(filter, grp2, grp2idx++, 1, asList(1.0));
|
||||
assertWeightCalculation(filter, grp1, grp1idx++, 3, asList(0.25, 0.75), 0.25);
|
||||
assertWeightCalculation(filter, grp2, grp2idx++, 1, asList(0.5, 0.5), 0.5);
|
||||
assertWeightCalculation(filter, grp1, grp1idx++, 6, asList(0.1, 0.3, 0.6), 0.1, 0.4);
|
||||
assertWeightCalculation(filter, grp2, grp2idx++, 2, asList(0.25, 0.25, 0.5), 0.25, 0.5);
|
||||
assertWeightCalculation(filter, grp2, grp2idx++, 4, asList(0.125, 0.125, 0.25, 0.5), 0.125, 0.25, 0.5);
|
||||
}
|
||||
|
||||
private void assertWeightCalculation(WeightCalculatorWebFilter filter, String group, int item,
|
||||
int weight, List<Double> normalized, Double... middleRanges) {
|
||||
String routeId = route(item);
|
||||
|
||||
filter.addWeightConfig(new WeightConfig(group, routeId, weight));
|
||||
|
||||
Map<String, GroupWeightConfig> groupWeights = filter.getGroupWeights();
|
||||
assertThat(groupWeights).containsKey(group);
|
||||
|
||||
GroupWeightConfig config = groupWeights.get(group);
|
||||
assertThat(config.group).isEqualTo(group);
|
||||
assertThat(config.weights).hasSize(item)
|
||||
.containsEntry(routeId, weight);
|
||||
assertThat(config.normalizedWeights).hasSize(item);
|
||||
|
||||
for (int i = 0; i < normalized.size(); i++) {
|
||||
assertThat(config.normalizedWeights)
|
||||
.containsEntry(route(i+1), normalized.get(i));
|
||||
}
|
||||
|
||||
for (int i = 0; i < normalized.size(); i++) {
|
||||
assertThat(config.rangeIndexes)
|
||||
.containsEntry(i, route(i+1));
|
||||
}
|
||||
|
||||
assertThat(config.ranges).hasSize(item + 1)
|
||||
.startsWith(0.0)
|
||||
.endsWith(1.0);
|
||||
|
||||
if (middleRanges.length > 0) {
|
||||
assertThat(config.ranges).contains(middleRanges);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String route(int i) {
|
||||
return "route"+i;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChooseRouteWithRandom() {
|
||||
WeightCalculatorWebFilter filter = new WeightCalculatorWebFilter();
|
||||
filter.addWeightConfig(new WeightConfig("groupa", "route1", 1));
|
||||
filter.addWeightConfig(new WeightConfig("groupa", "route2", 3));
|
||||
filter.addWeightConfig(new WeightConfig("groupa", "route3", 6));
|
||||
|
||||
Random random = mock(Random.class);
|
||||
|
||||
when(random.nextDouble())
|
||||
.thenReturn(0.05)
|
||||
.thenReturn(0.2)
|
||||
.thenReturn(0.6);
|
||||
|
||||
filter.setRandom(random);
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("http://localhost").build());
|
||||
|
||||
WebFilterChain filterChain = mock(WebFilterChain.class);
|
||||
filter.filter(exchange, filterChain);
|
||||
Map<String, String> weights = WeightCalculatorWebFilter.getWeights(exchange);
|
||||
assertThat(weights).containsEntry("groupa", "route1");
|
||||
|
||||
filter.filter(exchange, filterChain);
|
||||
weights = WeightCalculatorWebFilter.getWeights(exchange);
|
||||
assertThat(weights).containsEntry("groupa", "route2");
|
||||
|
||||
filter.filter(exchange, filterChain);
|
||||
weights = WeightCalculatorWebFilter.getWeights(exchange);
|
||||
assertThat(weights).containsEntry("groupa", "route3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receivesPredicateArgsEvent() {
|
||||
WeightCalculatorWebFilter filter = mock(WeightCalculatorWebFilter.class);
|
||||
doNothing().when(filter).addWeightConfig(any(WeightConfig.class));
|
||||
doCallRealMethod().when(filter).handle(any(PredicateArgsEvent.class));
|
||||
|
||||
HashMap<String, Object> args = new HashMap<>();
|
||||
args.put("weight.group", "group1");
|
||||
args.put("weight.weight", "1");
|
||||
PredicateArgsEvent event = new PredicateArgsEvent(this, "routeA", args);
|
||||
filter.handle(event);
|
||||
|
||||
ArgumentCaptor<WeightConfig> configCaptor = ArgumentCaptor.forClass(WeightConfig.class);
|
||||
verify(filter).addWeightConfig(configCaptor.capture());
|
||||
|
||||
WeightConfig weightConfig = configCaptor.getValue();
|
||||
assertThat(weightConfig.getGroup()).isEqualTo("group1");
|
||||
assertThat(weightConfig.getRouteId()).isEqualTo("routeA");
|
||||
assertThat(weightConfig.getWeight()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2013-2018 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
|
||||
*
|
||||
* http://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.handler.predicate;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter;
|
||||
import org.springframework.cloud.gateway.route.RouteLocator;
|
||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
|
||||
import org.springframework.cloud.gateway.test.BaseWebClientTests;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT)
|
||||
@DirtiesContext
|
||||
public class WeightRoutePredicateFactoryIntegrationTests extends BaseWebClientTests {
|
||||
|
||||
@Autowired
|
||||
private WeightCalculatorWebFilter filter;
|
||||
|
||||
@Test
|
||||
public void highWeight() {
|
||||
filter.setRandom(getRandom(0.9));
|
||||
|
||||
testClient.get().uri("/get")
|
||||
.header(HttpHeaders.HOST, "www.weighthigh.org")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().valueEquals(ROUTE_ID_HEADER, "weight_high_test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lowWeight() {
|
||||
filter.setRandom(getRandom(0.1));
|
||||
|
||||
testClient.get().uri("/get")
|
||||
.header(HttpHeaders.HOST, "www.weightlow.org")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().valueEquals(ROUTE_ID_HEADER, "weight_low_test");
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@SpringBootConfiguration
|
||||
@Import(DefaultTestConfig.class)
|
||||
public static class TestConfig {
|
||||
|
||||
@Value("${test.uri}")
|
||||
private String uri;
|
||||
|
||||
public TestConfig(WeightCalculatorWebFilter filter) {
|
||||
Random random = getRandom(0.4);
|
||||
|
||||
filter.setRandom(random);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
|
||||
return builder.routes()
|
||||
.route("weight_low_test", r ->
|
||||
r.weight("group1", 2)
|
||||
.and().host("**.weightlow.org")
|
||||
.filters(f -> f.prefixPath("/httpbin"))
|
||||
.uri(this.uri))
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static Random getRandom(double value) {
|
||||
Random random = mock(Random.class);
|
||||
when(random.nextDouble())
|
||||
.thenReturn(value);
|
||||
return random;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -222,11 +222,19 @@ spring:
|
||||
- AddRequestHeader=X-Request-Foo, Bar
|
||||
- AddRequestHeader=X-Request-Baz, Bat
|
||||
|
||||
# =====================================
|
||||
- id: weight_high_test
|
||||
uri: ${test.uri}
|
||||
predicates:
|
||||
- Host=**.weighthigh.org
|
||||
- Weight=group1, 8
|
||||
|
||||
# =====================================
|
||||
- id: header_test
|
||||
uri: ${test.uri}
|
||||
predicates:
|
||||
- Header=Foo, .*
|
||||
|
||||
# =====================================
|
||||
- id: default_path_to_httpbin
|
||||
uri: ${test.uri}
|
||||
|
||||
Reference in New Issue
Block a user