Personal experience and source code analysis of filter chain of Spring Cloud Gateway

Keywords: Spring Spring Boot Spring Cloud

preface:
In fact, I just want to understand the construction principle of the Filter linked list in the spring cloud gateway and the timing of injection into the Bean. If the dynamic injection Filter is a Bean, what timing injection will take effect by default. If it cannot take effect for what reason, which method classes can be forced to take effect by rewriting.

Filters and filter chains

The Spring Cloud Gateway is divided into GatewayFilter and GlobalFilter according to its scope.

  • GatewayFilter: it needs to be configured through spring.cloud.routes.filters under specific routes, which only works on the current route, or through spring.cloud.default-filters, which is configured globally and works on all routes
  • GlobalFilter: a global filter, which does not need to be configured in the configuration file and acts on all routes. It is finally packaged into a filter recognizable by GatewayFilterChain through the GatewayFilterAdapter. It is a core filter that converts the URI of the request service and route into the request address of the real service. It does not need to be configured. It is loaded during system initialization, And act on each route.

Some key classes and interfaces

The OrderGatewayFilter class implements the GatewayFilter and Ordered interfaces, and packages the target filter into sortable object types. It is the packaging class of the target filter.

The GatewayFilterAdapter class implements the GatewayFilter, which mainly wraps the GlobalFilter filter into the corresponding of the GatewayFilter type. Is a wrapper class for GlobalFilter filters.

FilteringWebHandler converts GlobalFilter into GatewayFilter and generates DefaultGatewayFilterChain.

Gateway filterchain gateway filter linked list - Interface

Implementation class above DefaultGatewayFilterChain
The linked list filtered by the gateway is used for the linked call of the filter. The default implementation of the filter linked list interface.

Source code analysis

GatewayAutoConfiguration

Configure the class and register the FilteringWebHandler as a Bean

......
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@EnableConfigurationProperties
@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class })
@AutoConfigureAfter({ GatewayReactiveLoadBalancerClientAutoConfiguration.class,
		GatewayClassPathWarningAutoConfiguration.class })
@ConditionalOnClass(DispatcherHandler.class)
//This configuration class basically loads most of the Gateway beans
public class GatewayAutoConfiguration { 
//The context will scan the configuration class, and then scan the method for Bean injection
//All kinds of messy handlers are injected into beans through this configuration class
	......	
	//Scan here according to the method of obtaining the configuration class to inject the FilteringWebHandler
	//All filters have been injected before the filtering webhandler is injected
	/**In other words, the injection priority of pure @ Configuration is not so high, that is, it will not be injected very early
	/*No one inherits ContextAware, BeanPostProcessor or @ Component
	/*That will load first.
	/*So all filters have been instantiated before,
	/*Here, all beans of GlobalFilter type will be injected as dependencies
	/* If a filter is added after this, it should not take effect)
	/* Because the filter chain is final,,,,, which will be described in detail later
	**/
	@Bean
	public FilteringWebHandler filteringWebHandler(List<GlobalFilter> globalFilters) {
		return new FilteringWebHandler(globalFilters);
	}

	@Bean
	public GlobalCorsProperties globalCorsProperties() {
		return new GlobalCorsProperties();
	}

	//Routing processing here depends on a webHandler, which should be the filter under routing
	@Bean
	public RoutePredicateHandlerMapping routePredicateHandlerMapping(FilteringWebHandler webHandler,
			RouteLocator routeLocator, GlobalCorsProperties globalCorsProperties, Environment environment) {
		return new RoutePredicateHandlerMapping(webHandler, routeLocator, globalCorsProperties, environment);
	}
	......

FilteringWebHandler

  • org.springframework.cloud.gateway.handler.FilteringWebHandler
package org.springframework.cloud.gateway.handler;

......
import org.springframework.web.server.WebHandler;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;

/**
 * WebHandler that delegates to a chain of {@link GlobalFilter} instances and
 * {@link GatewayFilterFactory} instances then to the target {@link WebHandler}.
 *
 * @author Rossen Stoyanchev
 * @author Spencer Gibb
 * @since 0.1
 */
public class FilteringWebHandler implements WebHandler {

	protected static final Log logger = LogFactory.getLog(FilteringWebHandler.class);
	
	//The global filter chain cannot be changed after the final type is initially defined. The advantage is thread safety
	private final List<GatewayFilter> globalFilters;

	//All globalfilters obtained through the configuration class become a final linked list
	public FilteringWebHandler(List<GlobalFilter> globalFilters) {
		this.globalFilters = loadFilters(globalFilters);
	}

	//Convert GlobaFilter into GatewayFilter and string it into a linked list
	private static List<GatewayFilter> loadFilters(List<GlobalFilter> filters) {
		return filters.stream().map(filter -> {
			GatewayFilterAdapter gatewayFilter = new GatewayFilterAdapter(filter);
			if (filter instanceof Ordered) {
				int order = ((Ordered) filter).getOrder();
				return new OrderedGatewayFilter(gatewayFilter, order);
			}
			return gatewayFilter;
		}).collect(Collectors.toList());
	}

	//This handle method should be used for routing processing
	@Override
	public Mono<Void> handle(ServerWebExchange exchange) {
		//Get route
		Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
		//It should be the filter under the get route
		List<GatewayFilter> gatewayFilters = route.getFilters();
		List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
		//Merge the filter under route and global filter
		combined.addAll(gatewayFilters);
		// TODO: needed or cached?
		//Sort according to order
		AnnotationAwareOrderComparator.sort(combined);

		if (logger.isDebugEnabled()) {
			logger.debug("Sorted gatewayFilterFactories: " + combined);
		}
		
		//Returns the default gateway filter chain
		return new DefaultGatewayFilterChain(combined).filter(exchange);
	}

	//A static nested class DefaultGatewayFilterChain is an independent class
	// Default filter chain implementation class
	private static class DefaultGatewayFilterChain implements GatewayFilterChain {

		private final int index;
		
		//Another final
		private final List<GatewayFilter> filters;

		//Constructor initializes the linked list of final attribute
		DefaultGatewayFilterChain(List<GatewayFilter> filters) {
			this.filters = filters;
			this.index = 0;
		}

		//Two parameters, one is the main chain, and the other refers to the location of the filter chain
		private DefaultGatewayFilterChain(DefaultGatewayFilterChain parent, int index) {
			this.filters = parent.getFilters();
			this.index = index;
		}

		public List<GatewayFilter> getFilters() {
			return filters;
		}
		
		//All filters will override this method, or there will be such a filter method
		//Generally, the processing logic of the filter is written in the filter method
		//Then a filter will call the next filter to form a responsibility chain
		//After dealing with your own affairs, leave it to the next person
		@Override
		public Mono<Void> filter(ServerWebExchange exchange) {
			return Mono.defer(() -> {
				if (this.index < filters.size()) {
					GatewayFilter filter = filters.get(this.index);
					DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this, this.index + 1);
					return filter.filter(exchange, chain);
				}
				else {
					return Mono.empty(); // complete
				}
			});
		}
	}

	private static class GatewayFilterAdapter implements GatewayFilter {

		private final GlobalFilter delegate;

		GatewayFilterAdapter(GlobalFilter delegate) {
			this.delegate = delegate;
		}

		@Override
		public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
			return this.delegate.filter(exchange, chain);
		}

		@Override
		public String toString() {
			final StringBuilder sb = new StringBuilder("GatewayFilterAdapter{");
			sb.append("delegate=").append(delegate);
			sb.append('}');
			return sb.toString();
		}
	}

}

RoutePredicateHandlerMapping

public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
	
	//It's all set to final
	private final FilteringWebHandler webHandler;
	private final RouteLocator routeLocator;
	private final Integer managementPort;
	private final ManagementPortType managementPortType;

	/**
	/* The gateway configuration class injects beans through this constructor
	/* Obtained through other dependency injection:
	/* Filter processor, route locator, global cross domain access configuration, running environment
	*/
	public RoutePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator,
			GlobalCorsProperties globalCorsProperties, Environment environment) {
		
		//This is not a global filter. It should be configured under routing
		this.webHandler = webHandler;
		this.routeLocator = routeLocator;
		this.managementPort = getPortProperty(environment, "management.server.");
		this.managementPortType = getManagementPortType(environment);

		/**
		Reason for calling #setOrder(1),
		Spring Cloud Gateway The gateway webfluxendpoint of provides HTTP API,
		You don't need to go through the gateway,
		It performs request matching processing through RequestMappingHandlerMapping.
		RequestMappingHandlerMapping order = 0,
		You need to be in front of RoutePredicateHandlerMapping.
		*/
		setOrder(1);
		setCorsConfigurations(globalCorsProperties.getCorsConfigurations());
	}
	

	private ManagementPortType getManagementPortType(Environment environment) {
		Integer serverPort = getPortProperty(environment, "server.");
		if (this.managementPort != null && this.managementPort < 0) {
			return DISABLED;
		}
		return ((this.managementPort == null || (serverPort == null && this.managementPort.equals(8080))
				|| (this.managementPort != 0 && this.managementPort.equals(serverPort))) ? SAME : DIFFERENT);
	}

	private static Integer getPortProperty(Environment environment, String prefix) {
		return environment.getProperty(prefix + "port", Integer.class);
	}

	//Match the Route and return the FilteringWebHandler that handles the Route 
	@Override
	protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
		// don't handle requests on management port if set and different than server port
		if (this.managementPortType == DIFFERENT && this.managementPort != null
				&& exchange.getRequest().getURI().getPort() == this.managementPort) {
			return Mono.empty();
		}
		exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());

		return lookupRoute(exchange)  //route matching
				// .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);
					}

					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) + "]");
					}
				})));
	}

	@Override
	protected CorsConfiguration getCorsConfiguration(Object handler, ServerWebExchange exchange) {
		// TODO: support cors configuration via properties on a route see gh-229
		// see RequestMappingHandlerMapping.initCorsConfiguration()
		// also see
		// https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/test/java/org/springframework/web/cors/reactive/CorsWebFilterTests.java
		return super.getCorsConfiguration(handler, exchange);
	}

	// TODO: get desc from factory?
	private String getExchangeDesc(ServerWebExchange exchange) {
		StringBuilder out = new StringBuilder();
		out.append("Exchange: ");
		out.append(exchange.getRequest().getMethod());
		out.append(" ");
		out.append(exchange.getRequest().getURI());
		return out.toString();
	}

	protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
		return this.routeLocator.getRoutes() //getRoutes() method to obtain all routes
				.concatMap(route -> Mono.just(route).filterWhen(r -> {
					// add the current route we are testing
					exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
					return r.getPredicate().apply(exchange);
				})
						// instead of immediately stopping main flux due to error, log and
						// swallow it
						.doOnError(e -> logger.error("Error applying predicate for route: " + route.getId(), e))
						.onErrorResume(e -> Mono.empty()))
				// .defaultIfEmpty() put a static Route not found
				// or .switchIfEmpty()
				// .switchIfEmpty(Mono.<Route>empty().log("noroute"))
				.next()
				// TODO: error handling
				.map(route -> {
					if (logger.isDebugEnabled()) {
						logger.debug("Route matched: " + route.getId());
					}
					validateRoute(route, exchange);
					return route;
				});
	}

	//This is an empty method that you can override yourself
	@SuppressWarnings("UnusedParameters")
	protected void validateRoute(Route route, ServerWebExchange exchange) {
	}

	protected String getSimpleName() {
		return "RoutePredicateHandlerMapping";
	}

	public enum ManagementPortType {
		DISABLED,
		SAME,
		DIFFERENT;
	}
}

ServerWebExchange

//Contract for HTTP request response interaction. 
//It provides access to HTTP requests and responses, and also exposes other properties and functions related to server-side processing, such as request properties.
public interface ServerWebExchange {

	String LOG_ID_ATTRIBUTE = ServerWebExchange.class.getName() + ".LOG_ID";

	ServerHttpRequest getRequest();

	ServerHttpResponse getResponse();

	Map<String, Object> getAttributes();


	@SuppressWarnings("unchecked")
	@Nullable
	default <T> T getAttribute(String name) {
		return (T) getAttributes().get(name);
	}

	@SuppressWarnings("unchecked")
	default <T> T getRequiredAttribute(String name) {
		T value = getAttribute(name);
		Assert.notNull(value, () -> "Required attribute '" + name + "' is missing");
		return value;
	}

	@SuppressWarnings("unchecked")
	default <T> T getAttributeOrDefault(String name, T defaultValue) {
		return (T) getAttributes().getOrDefault(name, defaultValue);
	}

	
	Mono<WebSession> getSession();

	<T extends Principal> Mono<T> getPrincipal();

	Mono<MultiValueMap<String, String>> getFormData();

	Mono<MultiValueMap<String, Part>> getMultipartData();

	LocaleContext getLocaleContext();

	@Nullable
	ApplicationContext getApplicationContext();

	boolean isNotModified();

	boolean checkNotModified(Instant lastModified);

	boolean checkNotModified(String etag);

	boolean checkNotModified(@Nullable String etag, Instant lastModified);

	String transformUrl(String url);

	void addUrlTransformer(Function<String, String> transformer);

	String getLogPrefix();

	default Builder mutate() {
		return new DefaultServerWebExchangeBuilder(this);
	}

	interface Builder {

		Builder request(Consumer<ServerHttpRequest.Builder> requestBuilderConsumer);

		Builder request(ServerHttpRequest request);

		Builder response(ServerHttpResponse response);

		Builder principal(Mono<Principal> principalMono);

		ServerWebExchange build();
	}

}

SCG overview

Spring cloud 2.0 implements Spring Cloud Gateway to replace ZUUL.
SCG is built based on spring 5's Reactor and spring boot 2, using Netty as the underlying communication framework.

Dispatcher handler: the total controller of the request, similar to the dispatcher servlet in WebMVC

  • DispatcherHandler receives the request, matches HandlerMapping, and will match RoutePredicateHandlerMapping.
  • RoutePredicateHandlerMapping receives the request, matches the Route, and configures some Route data for the Route.
  • The FilteringWebHandler obtains the GatewayFilter array under the Route through exchange and combines it with the global filter to create a GatewayFilterChain to process the request.

  • Filter: similar to Zuul's filter in concept, it can be used to intercept and modify requests and secondary process upstream responses. The filter is an instance of the org.springframework.cloud.gateway.filter.GatewayFilter class.
  • Route: the basic component module of gateway configuration, which is similar to Zuul's route configuration module. A route module is defined by an ID, a target URI, a set of assertions, and a set of filters. If the assertion is true, the route matches and the destination URI is accessed.
  • Predicate: This is a Java 8 predicate that can be used to match anything from an HTTP request, such as headers or parameters. The input type of the assertion is a ServerWebExchage.

Other supplements

Zuul

Zuul provides a framework that supports dynamic loading, compiling and running of these filters. These filters process request or response results sequentially through the responsibility chain. There is no direct communication between filters, but some information can be shared through the requestcontext (ThreadLocal thread level cache) parameter passed by the responsibility chain.

Although zuul supports any language that can run on the JVM, zuul's filters can only be written in Groovy. The prepared filter script is generally placed in the fixed directory of the zuul server. The zuul server will start a thread timing area to poll the modified or newly added filters, and then dynamically compile and load them into memory. Then, when subsequent requests come in, the newly added or modified filters will take effect.

Spring cloud integrates and enhances zuul. Zuul, as a gateway layer, is also a micro service. Zuul can sense which Provider instances are online, and can automatically forward REST requests to the specified back-end micro service Provider by configuring routing rules.

Analogy source code (supplementary)

The filter chain in the Gateway is similar to org.springframework.web.server.handler.FilteringWebHandler
In fact, the same is true

//WebHandlerDecorator: 
//WebHandler decorator, using decoration mode to realize the expansion of related functions
//FilteringWebHandler: 
//The class for filtering through WebFilter is similar to the Filter in Servlet
public class FilteringWebHandler extends WebHandlerDecorator {

	//It is directly defined as final here, which means that the chain cannot be modified after loading
	private final DefaultWebFilterChain chain;

	//Constructor passed in a filter chain
	public FilteringWebHandler(WebHandler handler, List<WebFilter> filters) {
		super(handler);
		this.chain = new DefaultWebFilterChain(handler, filters);
	}

	//Returns a read-only linked list -- > configured filter linked list
	public List<WebFilter> getFilters() {
		return this.chain.getFilters();
	}
	
	@Override
	public Mono<Void> handle(ServerWebExchange exchange) {
		return this.chain.filter(exchange);
	}

}
public class DefaultWebFilterChain implements WebFilterChain {

	private final List<WebFilter> allFilters;

	private final WebHandler handler;

	@Nullable
	private final WebFilter currentFilter;

	@Nullable
	private final DefaultWebFilterChain chain;


	public DefaultWebFilterChain(WebHandler handler, List<WebFilter> filters) {
		Assert.notNull(handler, "WebHandler is required");
		this.allFilters = Collections.unmodifiableList(filters);
		this.handler = handler;
		DefaultWebFilterChain chain = initChain(filters, handler);
		this.currentFilter = chain.currentFilter;
		this.chain = chain.chain;
	}

	private static DefaultWebFilterChain initChain(List<WebFilter> filters, WebHandler handler) {
		DefaultWebFilterChain chain = new DefaultWebFilterChain(filters, handler, null, null);
		ListIterator<? extends WebFilter> iterator = filters.listIterator(filters.size());
		while (iterator.hasPrevious()) {
			chain = new DefaultWebFilterChain(filters, handler, iterator.previous(), chain);
		}
		return chain;
	}

	/**
	 * Private constructor to represent one link in the chain.
	 */
	private DefaultWebFilterChain(List<WebFilter> allFilters, WebHandler handler,
			@Nullable WebFilter currentFilter, @Nullable DefaultWebFilterChain chain) {

		this.allFilters = allFilters;
		this.currentFilter = currentFilter;
		this.handler = handler;
		this.chain = chain;
	}

	@Deprecated
	public DefaultWebFilterChain(WebHandler handler, WebFilter... filters) {
		this(handler, Arrays.asList(filters));
	}


	public List<WebFilter> getFilters() {
		return this.allFilters;
	}

	public WebHandler getHandler() {
		return this.handler;
	}


	@Override
	public Mono<Void> filter(ServerWebExchange exchange) {
		return Mono.defer(() ->
				this.currentFilter != null && this.chain != null ?
						invokeFilter(this.currentFilter, this.chain, exchange) :
						this.handler.handle(exchange));
	}

	private Mono<Void> invokeFilter(WebFilter current, DefaultWebFilterChain chain, ServerWebExchange exchange) {
		String currentName = current.getClass().getName();
		return current.filter(exchange, chain).checkpoint(currentName + " [DefaultWebFilterChain]");
	}

}

Posted by Jip on Tue, 23 Nov 2021 15:35:56 -0800