Spring cloud upgrade 2020.0.x - 41. Explanation of the basic process of spring cloud gateway

Keywords: Spring Cloud

Code address of this series: https://github.com/JoJoTec/spring-cloud-parent

Let's continue to analyze the WebHandler mentioned in the previous section. After adding Spring Cloud Sleuth and Prometheus related dependencies, the processing flow of Spring Cloud Gateway is as follows:

Spring Cloud Gateway entry - > DefaultWebFilterChain of Webflux

Spring Cloud Gateway is an asynchronous response gateway developed based on Spring WebFlux. The asynchronous response code is difficult to understand and read. Here I share a way to understand it. Through this process, we can understand the workflow and underlying principles of Spring Cloud Gateway. In fact, it can be understood that the process in the figure above is to spell out a complete Mono (or Flux) flow, and finally subscribe to execute.

When a request is received, it will go through org.springframework.web.server.handler.DefaultWebFilterChain, which is the call chain of WebFilter. This link includes three webfilters:

  • org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter: after adding Prometheus related dependencies, this MetricsWebFilter will be used to record the request processing time and collect relevant indicators.
  • org.springframework.cloud.sleuth.instrument.web.TraceWebFilter: after adding Spring Cloud Sleuth related dependencies, there will be this TraceWebFilter.
  • Org.springframework.cloud.gateway.handler.predict.weightcalculatorwebfilter: Spring Cloud Gateway routing weight related configuration function related implementation classes, which we don't care about here.

In this DefaultWebFilterChain, we will form such a Mono. We will mark them in turn. The first is the entry code org.springframework.web.server.handler.DefaultWebFilterChain#filter:

public Mono<Void> filter(ServerWebExchange exchange) {
	return Mono.defer(() ->
	        // this.currentFilter != null means that the WebFilter chain has not ended
	        // this.chain != null means that the WebFilter chain is not empty
			this.currentFilter != null && this.chain != null ?
					//When the WebFilter chain does not end, call WebFilter
					invokeFilter(this.currentFilter, this.chain, exchange) :
					//When WebFilter ends, call handler 
					this.handler.handle(exchange));
}

For the first MetricsWebFilter in our WebFilter chain, assuming that the corresponding collection statistics are enabled, the generated Mono is:

return Mono.defer(() ->
	chain.filter(exchange).transformDeferred((call) -> {
		long start = System.nanoTime();
		return call
				//When successful, record the response time
				.doOnSuccess((done) -> MetricsWebFilter.this.onSuccess(exchange, start))
				//In case of failure, the response time and exceptions are recorded
				.doOnError((cause) -> MetricsWebFilter.this.onError(exchange, start, cause));
	});
);

For convenience, we have simplified the code here. Because we want to splice all Mono and Flux of the whole link to complete the link, the onSuccess(exchange, start) method in MetricsWebFilter has been changed to the pseudo code of MetricsWebFilter.this.onSuccess(exchange, start).

Then, according to the source code analysis of DefaultWebFilterChain, the chain.filter(exchange) will continue the WebFilter link to the next WebFilter, TraceWebFilter. After TraceWebFilter, Mono will become:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, chain.filter(exchange), TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//The processing related to MetricsWebFilter is given in the previous code and omitted here
	});
);

It can be seen that in TraceWebFilter, the whole internal mono (subsequent results of chain. Filter (exchange)) is encapsulated into a MonoWebFilterTrace, which is also the key implementation of maintaining link tracking information.

Continue the WebFilter link and pass through the last WebFilter, weightcalculatorwebfilter; We don't care about this WebFilter. There are some calculation operations for routing weight, which we can ignore directly here. In this way, we have completed all WebFilter links and come to the final call DefaultWebFilterChain.this.handler, which is org.springframework.web.reactive.DispatcherHandler. In the dispatcher handler, we calculate the route and send the request to the qualified gateway filter. After DispatcherHandler, Mono will become:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		Flux.fromIterable(DispatcherHandler.this.handlerMappings) //Read all handlerMappings
			.concatMap(mapping -> mapping.getHandler(exchange)) //Call the getHandler methods of all handlerMappings in sequence. If there is a corresponding Handler, it will return. Otherwise, it will return Mono.empty();
			.next() //Find the first Handler that does not return Mono.empty()
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //If no handlerMapping other than Mono.empty() is returned, 404 is returned directly
			.flatMap(handler -> DispatcherHandler.this.invokeHandler(exchange, handler)) //Call the corresponding Handler
			.flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //Processing results
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//The processing related to MetricsWebFilter is given in the previous code and omitted here
	});
);

handlerMappings include:

  • org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndPointHandlerMapping: this HandlerMapping is available here because we have added executor related dependencies in our project. Activator related path mapping is not what we care about here. However, it can be seen that the activator related path takes precedence over the Spring Cloud Gateway configuration route
  • Org.springframework.boot.actuate.endpoint.web.reactive.controllerendpointahandlermapping: this HandlerMapping is available because we have added executor related dependencies in our project. The activator related path mapping annotated with @ ControllerEndpoint or @ RestControllerEndpoint annotation is not our concern here.
  • org.springframework.web.reactive.function.server.support.RouterFunctionMapping: in spring Webflux, you can define many different routerfunctions to control path routing, but this is not what we care about here. However, it can be seen that the customized RouterFunction takes precedence over the routing configuration of Spring Cloud Gateway
  • org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping: HandlerMapping for the path annotated by @ RequestMapping is not our concern here. However, it can be seen that if you specify the RequestMapping path in the Spring Cloud Gateway, the routing configuration will take precedence over the Spring Cloud Gateway.
  • org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping: This is the HandlerMapping of Spring Cloud Gateway. It will read the configuration of Spring Cloud Gateway and generate routes. This is what we want to analyze in detail here.

In fact, these handlerMappings must follow the logic of routepredictehandlermapping, so our Mono can be simplified to:

 return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.getHandler(exchange)
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //If no handlerMapping other than Mono.empty() is returned, 404 is returned directly
			.flatMap(handler -> DispatcherHandler.this.invokeHandler(exchange, handler)) //Call the corresponding Handler
			.flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //Processing results
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//The processing related to MetricsWebFilter is given in the previous code and omitted here
	});
);

Let's look at RoutePredicateHandlerMapping. First, these handlerMapping inherit the abstract class org.springframework.web.reactive.handler.AbstractHandlerMapping. The implementation of getHandler in Mono we spliced earlier is actually in this abstract class:

 public Mono<Object> getHandler(ServerWebExchange exchange) {
	//Call the abstract method getHandlerInternal to get the real Handler
	return getHandlerInternal(exchange).map(handler -> {
		//Here are some log records for the handler
		if (logger.isDebugEnabled()) {
			logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
		}
		// Cross domain processing
		ServerHttpRequest request = exchange.getRequest();
		if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
			CorsConfiguration config = (this.corsConfigurationSource != null ?
					this.corsConfigurationSource.getCorsConfiguration(exchange) : null);
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
			config = (config != null ? config.combine(handlerConfig) : handlerConfig);
			if (config != null) {
				config.validateAllowCredentials();
			}
			if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
				return NO_OP_HANDLER;
			}
		}
		return handler;
	});
}

It can be seen that the core is the getHandlerInternal(exchange) method of each implementation class. Therefore, in our spliced Mono, we will ignore the map processing after handler in the abstract class.

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.getHandlerInternal(exchange)
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //If no handlerMapping other than Mono.empty() is returned, 404 is returned directly
			.flatMap(handler -> DispatcherHandler.this.invokeHandler(exchange, handler)) //Call the corresponding Handler
			.flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //Processing results
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//The processing related to MetricsWebFilter is given in the previous code and omitted here
	});
);

Next, through the getHandlerInternal(exchange) method of RoutePredicateHandlerMapping, our Mono becomes:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.lookupRoute(exchange) //Find route on request
				.flatMap((Function<Route, Mono<?>>) r -> {
					exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //Put the route into Attributes, which we will use later
					return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //Returns the FilteringWebHandler of RoutePredicateHandlerMapping
				}).switchIfEmpty( //If it is Mono.empty(), the route is not found
					Mono.empty() //Return Mono.empty()
					.then(Mono.fromRunnable(() -> { //After returning Mono.empty(), log
						if (logger.isTraceEnabled()) {
							logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
					}
				})))
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //If no handlerMapping other than Mono.empty() is returned, 404 is returned directly
			.flatMap(handler -> DispatcherHandler.this.invokeHandler(exchange, handler)) //Call the corresponding Handler
			.flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //Processing results
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//The processing related to MetricsWebFilter is given in the previous code and omitted here
	});
);

RoutePredicateHandlerMapping.this.lookupRoute(exchange) finds routes according to the request. We won't expand this in detail. In fact, we find appropriate routes according to your Spring Cloud Gateway configuration. Next, let's look at calling the corresponding Handler, that is, FilteringWebHandler. DispatcherHandler.this.invokeHandler(exchange, handler) is not expanded in detail here. We know that it is actually calling the Handler's handle method, that is, the FilteringWebHandler's handle method, so our Mono becomes:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.lookupRoute(exchange) //Find route on request
				.flatMap((Function<Route, Mono<?>>) r -> {
					exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //Put the route into Attributes, which we will use later
					return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //Returns the FilteringWebHandler of RoutePredicateHandlerMapping
				}).switchIfEmpty( //If it is Mono.empty(), the route is not found
					Mono.empty() 
					.then(Mono.fromRunnable(() -> { //After returning Mono.empty(), log
						if (logger.isTraceEnabled()) {
							logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
					}
				})))
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //If no handlerMapping other than Mono.empty() is returned, 404 is returned directly
			.then(FilteringWebHandler.this.handle(exchange).then(Mono.empty())) //Call the corresponding Handler
			.flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //Processing results
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//The processing related to MetricsWebFilter is given in the previous code and omitted here
	});
);

Since the corresponding Handler is called and Mono.empty() is returned, the following flatMap will not be executed. So we can remove the final processing result. So our Mono becomes:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.lookupRoute(exchange) //Find route on request
				.flatMap((Function<Route, Mono<?>>) r -> {
					exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //Put the route into Attributes, which we will use later
					return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //Returns the FilteringWebHandler of RoutePredicateHandlerMapping
				}).switchIfEmpty( //If it is Mono.empty(), the route is not found
					Mono.empty() 
					.then(Mono.fromRunnable(() -> { //After returning Mono.empty(), log
						if (logger.isTraceEnabled()) {
							logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
					}
				})))
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //If no handlerMapping other than Mono.empty() is returned, 404 is returned directly
			.then(FilteringWebHandler.this.handle(exchange).then(Mono.empty()))), //Call the corresponding Handler
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//The processing related to MetricsWebFilter is given in the previous code and omitted here
	});
);

FilteringWebHandler.this.handle(exchange) actually takes the route from the Attributes, takes the corresponding GatewayFilters from the route, puts them in the same List as the global GatewayFilters, and sorts them according to the order of these GatewayFilters (the order can be determined by implementing the org.springframework.core.Ordered interface), Then generate the DefaultGatewayFilterChain, that is, the GatewayFilter link. The corresponding source code is:

public Mono<Void> handle(ServerWebExchange exchange) {
	//Take the route from the Attributes and the corresponding GatewayFilters from the route
	Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
	List<GatewayFilter> gatewayFilters = route.getFilters();
	//Put it in the same List as the global GatewayFilters
	List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
	combined.addAll(gatewayFilters);
	//Sort according to the order of these gatewayfilters (you can make the order by implementing the 'org.springframework.core.Ordered' interface)
	AnnotationAwareOrderComparator.sort(combined);
	
	if (logger.isDebugEnabled()) {
		logger.debug("Sorted gatewayFilterFactories: " + combined);
	}
	//Generate call chain
	return new DefaultGatewayFilterChain(combined).filter(exchange);
}

The GatewayFilter call chain is similar to the WebFilter call chain. Refer to the source code of DefaultGatewayFilterChain:

public Mono<Void> filter(ServerWebExchange exchange) {
	return Mono.defer(() -> {
		//If the link does not end, continue the link
		if (this.index < filters.size()) {
			GatewayFilter filter = filters.get(this.index);
			//Here, index + 1 is used, that is, the next gateway filter in the link is called
			DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this, this.index + 1);
			//If you want to continue the link in each filter, you will call chain.filter(exchange), which is also the way we use when developing gateway filter
			return filter.filter(exchange, chain);
		}
		else {
			//When the end is reached, the link ends
			return Mono.empty(); // complete
		}
	});
}

Therefore, after DefaultGatewayFilterChain, our Mono will become:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.lookupRoute(exchange) //Find route on request
				.flatMap((Function<Route, Mono<?>>) r -> {
					exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //Put the route into Attributes, which we will use later
					return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //Returns the FilteringWebHandler of RoutePredicateHandlerMapping
				}).switchIfEmpty( //If it is Mono.empty(), the route is not found
					Mono.empty() 
					.then(Mono.fromRunnable(() -> { //After returning Mono.empty(), log
						if (logger.isTraceEnabled()) {
							logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
					}
				})))
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //If no handlerMapping other than Mono.empty() is returned, 404 is returned directly
			.then(new DefaultGatewayFilterChain(combined).filter(exchange).then(Mono.empty()))), //Call the corresponding Handler
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//The processing related to MetricsWebFilter is given in the previous code and omitted here
	});
);

Continue to expand the link call of DefaultGatewayFilterChain, and you can get:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.lookupRoute(exchange) //Find route on request
				.flatMap((Function<Route, Mono<?>>) r -> {
					exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //Put the route into Attributes, which we will use later
					return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //Returns the FilteringWebHandler of RoutePredicateHandlerMapping
				}).switchIfEmpty( //If it is Mono.empty(), the route is not found
					Mono.empty() 
					.then(Mono.fromRunnable(() -> { //After returning Mono.empty(), log
						if (logger.isTraceEnabled()) {
							logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
					}
				})))
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //If no handlerMapping other than Mono.empty() is returned, 404 is returned directly
			.then(
				Mono.defer(() -> {
					//If the link does not end, continue the link
					if (DefaultGatewayFilterChain.this.index < DefaultGatewayFilterChain.this.filters.size()) {
						GatewayFilter filter = DefaultGatewayFilterChain.this.filters.get(DefaultGatewayFilterChain.this.index);
						//Here, index + 1 is used, that is, the next gateway filter in the link is called
						DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(DefaultGatewayFilterChain.this, DefaultGatewayFilterChain.this.index + 1);
						//If you want to continue the link in each filter, you will call chain.filter(exchange), which is also the way we use when developing gateway filter
						return filter.filter(exchange, chain);
					}
					else {
						return Mono.empty(); //Link complete
					}
				})
				.then(Mono.empty()))
			), //Call the corresponding Handler
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//The processing related to MetricsWebFilter is given in the previous code and omitted here
	});
);

In this way, a complete Mono call chain of Spring Cloud Gateway for routing requests is formed.

WeChat search "my programming meow" attention to the official account, daily brush, easy to upgrade technology, and capture all kinds of offer:

Posted by woza_uk on Thu, 25 Nov 2021 19:24:29 -0800