Merge branch 'FantasticDream/main'

This commit is contained in:
spencergibb
2024-11-14 11:07:46 -05:00
5 changed files with 188 additions and 14 deletions

View File

@@ -272,8 +272,8 @@ public class GatewayAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public FilteringWebHandler filteringWebHandler(List<GlobalFilter> globalFilters) {
return new FilteringWebHandler(globalFilters);
public FilteringWebHandler filteringWebHandler(List<GlobalFilter> globalFilters, GatewayProperties properties) {
return new FilteringWebHandler(globalFilters, properties.isRouteFilterCacheEnabled());
}
@Bean

View File

@@ -68,6 +68,19 @@ public class GatewayProperties {
*/
private boolean failOnRouteDefinitionError = true;
/**
* Enables the route filter cache, defaults to false.
*/
private boolean routeFilterCacheEnabled = false;
public boolean isRouteFilterCacheEnabled() {
return routeFilterCacheEnabled;
}
public void setRouteFilterCacheEnabled(boolean routeFilterCacheEnabled) {
this.routeFilterCacheEnabled = routeFilterCacheEnabled;
}
public List<RouteDefinition> getRoutes() {
return routes;
}
@@ -109,6 +122,7 @@ public class GatewayProperties {
.append("defaultFilters", defaultFilters)
.append("streamingMediaTypes", streamingMediaTypes)
.append("failOnRouteDefinitionError", failOnRouteDefinitionError)
.append("routeFilterCacheEnabled", routeFilterCacheEnabled)
.toString();
}

View File

@@ -18,18 +18,21 @@ package org.springframework.cloud.gateway.handler;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.context.ApplicationListener;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
@@ -49,14 +52,28 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.G
* @author Yuxin Wang
* @since 0.1
*/
public class FilteringWebHandler implements WebHandler {
public class FilteringWebHandler implements WebHandler, ApplicationListener<RefreshRoutesEvent> {
protected static final Log logger = LogFactory.getLog(FilteringWebHandler.class);
private final List<GatewayFilter> globalFilters;
private final ConcurrentHashMap<Route, List<GatewayFilter>> routeFilterMap = new ConcurrentHashMap();
private final boolean routeFilterCacheEnabled;
@Deprecated
public FilteringWebHandler(List<GlobalFilter> globalFilters) {
this(globalFilters, false);
}
public FilteringWebHandler(List<GlobalFilter> globalFilters, boolean routeFilterCacheEnabled) {
this.globalFilters = loadFilters(globalFilters);
this.routeFilterCacheEnabled = routeFilterCacheEnabled;
}
/* for testing */ ConcurrentHashMap<Route, List<GatewayFilter>> getRouteFilterMap() {
return routeFilterMap;
}
private static List<GatewayFilter> loadFilters(List<GlobalFilter> filters) {
@@ -76,20 +93,17 @@ public class FilteringWebHandler implements WebHandler {
}).collect(Collectors.toList());
}
/*
* TODO: relocate @EventListener(RefreshRoutesEvent.class) void handleRefresh() {
* this.combinedFiltersForRoute.clear();
*/
@Override
public void onApplicationEvent(RefreshRoutesEvent event) {
if (this.routeFilterCacheEnabled) {
routeFilterMap.clear();
}
}
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
List<GatewayFilter> gatewayFilters = route.getFilters();
List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
combined.addAll(gatewayFilters);
// TODO: needed or cached?
AnnotationAwareOrderComparator.sort(combined);
List<GatewayFilter> combined = getCombinedFilters(route);
if (logger.isDebugEnabled()) {
logger.debug("Sorted gatewayFilterFactories: " + combined);
@@ -98,6 +112,23 @@ public class FilteringWebHandler implements WebHandler {
return new DefaultGatewayFilterChain(combined).filter(exchange);
}
protected List<GatewayFilter> getCombinedFilters(Route route) {
if (this.routeFilterCacheEnabled) {
return routeFilterMap.computeIfAbsent(route, this::getAllFilters);
}
else {
return getAllFilters(route);
}
}
protected List<GatewayFilter> getAllFilters(Route route) {
List<GatewayFilter> gatewayFilters = route.getFilters();
List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
combined.addAll(gatewayFilters);
AnnotationAwareOrderComparator.sort(combined);
return combined;
}
private static class DefaultGatewayFilterChain implements GatewayFilterChain {
private final int index;

View File

@@ -46,7 +46,12 @@ public class RouteRefreshListener implements ApplicationListener<ApplicationEven
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent) {
ContextRefreshedEvent refreshedEvent = (ContextRefreshedEvent) event;
if (!WebServerApplicationContext.hasServerNamespace(refreshedEvent.getApplicationContext(), "management")) {
boolean isManagementCtxt = WebServerApplicationContext
.hasServerNamespace(refreshedEvent.getApplicationContext(), "management");
boolean isLoadBalancerCtxt = refreshedEvent.getApplicationContext().getDisplayName() != null
&& refreshedEvent.getApplicationContext().getDisplayName().startsWith("LoadBalancerClientFactory-");
if (!isManagementCtxt && !isLoadBalancerCtxt) {
reset();
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2013-2020 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.handler;
import java.net.URI;
import java.time.Duration;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
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.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.web.reactive.function.BodyInserters;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@SpringBootTest(webEnvironment = RANDOM_PORT,
properties = { "spring.cloud.gateway.route-filter-cache-enabled=true",
"management.endpoint.gateway.enabled=true", "management.endpoints.web.exposure.include=*",
"spring.cloud.gateway.actuator.verbose.enabled=true" })
@DirtiesContext
public class FilteringWebHandlerCacheEnabledIntegrationTests extends BaseWebClientTests {
@Autowired
private FilteringWebHandler webHandler;
@Test
public void filteringWebHandlerCacheEnabledWorks() {
// prime the cache
callRoute("/get");
assertThat(webHandler.getRouteFilterMap()).hasSize(1);
callRoute("/anything/testRoute1");
assertThat(webHandler.getRouteFilterMap()).hasSize(2);
RouteDefinition testRouteDefinition = new RouteDefinition();
testRouteDefinition.setId("testRoute2");
testRouteDefinition.setUri(URI.create("lb://testservice"));
FilterDefinition filterDefinition = new FilterDefinition("PrefixPath=/httpbin");
testRouteDefinition.getFilters().add(filterDefinition);
PredicateDefinition hostRoutePredicateDefinition = new PredicateDefinition("Path=/anything/testRoute2");
testRouteDefinition.setPredicates(Arrays.asList(hostRoutePredicateDefinition));
testClient.post()
.uri("http://localhost:" + port + "/actuator/gateway/routes/testRoute2")
.accept(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(testRouteDefinition))
.exchange()
.expectStatus()
.isCreated();
testClient.post()
.uri("http://localhost:" + port + "/actuator/gateway/refresh")
.exchange()
.expectStatus()
.isOk();
callRoute("/get");
callRoute("/anything/testRoute1");
callRoute("/anything/testRoute2");
assertThat(webHandler.getRouteFilterMap()).hasSize(3);
}
private void callRoute(String uri) {
testClient.mutate()
.responseTimeout(Duration.ofMinutes(5))
.build()
.get()
.uri(uri)
.exchange()
.expectStatus()
.isOk();
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
public static class TestConfig {
@Bean
RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("get_route", r -> r.path("/get").filters(f -> f.prefixPath("/httpbin")).uri("lb://testservice"))
.route("testRoute1",
r -> r.path("/anything/testRoute1")
.filters(f -> f.prefixPath("/httpbin"))
.uri("lb://testservice"))
.build();
}
}
}