[SPRING] Spring Cloud Gateway - 3

[SPRING] Spring Cloud Gateway - 3

Last modified on 2025-04-18 , by hjjae2

spring-cloud-starter-gateway #

Dependency #

  • spring-cloud-starter
  • spring-boot-starter-webflux
    • netty
  • spring-cloud-starter-loadbalancer
    • (optional)
  • spring-cloud-gateway-server
    • 실질적으로 이 모듈에 Gateway와 관련된 클래스(코드)가 있다고 보면 될 것 같다.

spring-cloud-gateway-server #

Dependency #

기본적인 Dependency 가 모두 들어있다. (다만, optional 활성 여부를 꼭 확인할 것)

  • spring-boot-starter
  • spring-boot-starter-validation
  • io.projectreactor.addons:reactor-extra
  • spring-boot-starter-oauth2-client (optional)
  • spring-boot-starter-actuator (optional)

Flow #

https://dlsrb6342.github.io/2019/05/14/spring-cloud-gateway-%EA%B5%AC%EC%A1%B0/ 여기의 글도 보기 좋다.


HttpServerOperations (reactor-netty-http)
    --->
        HttpWebHandlerAdapter(spring-web)
                          ---> (WebHandler.handle(), DispatcherHandler.handle())
                                RoutePredicateHandlerMapping(spring-cloud-gateway-server)
                                                      --->  
                                                          FilteringWebHandler.handle()



DispatcherHandler #

public class DispatcherHandler implements WebHandler, PreFlightRequestHandler, ApplicationContextAware {

	@Nullable
	private List<HandlerMapping> handlerMappings;

	@Nullable
	private List<HandlerAdapter> handlerAdapters;

	@Nullable
	private List<HandlerResultHandler> resultHandlers;

	...

HandlerMapping 은 기본적으로 아래 7가지를 가지고 있다.

이것들은 순서(Order)를 갖고 있다. 낮은 값(INT) 앞에 배치하고, 높은 값일수록 뒤에 배치한다.

  • AdditionalHealthEndpointPathsWebFluxHandlerMapping
  • WebFluxEndpointHandlerMapping
  • ControllerEndpointHandlerMapping
  • RouterFunctionMapping
  • RequestMappingHandlerMapping
  • RoutePredicateHandlerMapping
  • SimpleUrlHandlerMapping


HandlerAdapter 는 기본적으로 아래 4가지를 가지고 있다.

  • WebSocketHandlerAdapter
  • RequestMappingHandlerAdapter
  • HandlerFunctionAdapter
  • SimpleHandlerAdapter


HandlerResultHandler 는 기본적으로 아래 4가지를 가지고 있다.

  • ResponseEntityResultHandler
  • ServerResponseResultHandler
  • ResponseBodyResultHandler
  • ViewResolutionResultHandler



RoutePredicateHandlerMapping (AbstractHandlerMapping, HandlerMapping) #

DispatcherHandler (WebHandler)

public class DispatcherHandler implements WebHandler, PreFlightRequestHandler, ApplicationContextAware {

  ... 

	@Override
	public Mono<Void> handle(ServerWebExchange exchange) {
		if (this.handlerMappings == null) {
			return createNotFoundError();
		}
		if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
			return handlePreFlight(exchange);
		}
		return Flux.fromIterable(this.handlerMappings)
				.concatMap(mapping -> mapping.getHandler(exchange)) // 여기 : AbstractHandlerMapping.getHandler() 로 이어진다.
				.next()
				.switchIfEmpty(createNotFoundError())
				.flatMap(handler -> invokeHandler(exchange, handler))
				.flatMap(result -> handleResult(exchange, result));
	}

  ...

}

AbstractHandlerMapping

public abstract class AbstractHandlerMapping extends ApplicationObjectSupport implements HandlerMapping, Ordered, BeanNameAware {

  @Override
	public Mono<Object> getHandler(ServerWebExchange exchange) {
		return getHandlerInternal(exchange).map(handler -> {      // 여기 : RoutePredicateHandlerMapping.getHandlerInternal() 로 이어진다.
			if (logger.isDebugEnabled()) {
				logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
			}
			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;
		});
	}

}



RoutePredicateHandlerMapping #

public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {

	private final FilteringWebHandler webHandler;

  private final RouteLocator routeLocator;

  ...

  @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)
				// .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);     // ← 여기 : FilteringWebHandler 를 반환한다.
				}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
					exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
					if (logger.isTraceEnabled()) {
						logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
					}
				})));
	}

  ...

	protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
		return this.routeLocator.getRoutes()
				// individually filter routes so that filterWhen error delaying is not a
				// problem
				.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;
				});

		/*
		 * TODO: trace logging if (logger.isTraceEnabled()) {
		 * logger.trace("RouteDefinition did not match: " + routeDefinition.getId()); }
		 */
	}
2022-07-17 20:01:37.968 DEBUG 91322 --- [ctor-http-nio-2] o.s.c.g.h.RoutePredicateHandlerMapping   : Route matched: 4162e05b-be86-4b3b-a2c7-bfcc4d2c89b4
2022-07-17 20:01:37.968 DEBUG 91322 --- [ctor-http-nio-2] o.s.c.g.h.RoutePredicateHandlerMapping   : Mapping [Exchange: GET http://localhost:8080/api/samples] to Route{id='4162e05b-be86-4b3b-a2c7-bfcc4d2c89b4', uri=http://localhost:8081, order=0, predicate=PredicateSpec$$Lambda$529/0x0000000800510c40, gatewayFilters=[[ModifyResponseBody New content type = [null], In class = Object, Out class = ResponseFormatFilter.Response]], metadata={}}
2022-07-17 20:01:37.968 DEBUG 91322 --- [ctor-http-nio-2] o.s.c.g.h.RoutePredicateHandlerMapping   : [d9c36f28-2] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@2937f9e0



FilteringWebHandler #

public class FilteringWebHandler implements WebHandler {

	protected static final Log logger = LogFactory.getLog(FilteringWebHandler.class);

	private final List<GatewayFilter> globalFilters;

	...

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

		if (logger.isDebugEnabled()) {
			logger.debug("Sorted gatewayFilterFactories: " + combined);
		}

		return new DefaultGatewayFilterChain(combined).filter(exchange);    // 여기 : Filter Chain 에 넘긴다.
	}

  ...
}

DefaultGatewayFilterChain

public class FilteringWebHandler implements WebHandler {

  ...
  
	private static class DefaultGatewayFilterChain implements GatewayFilterChain {

		private final int index;

		private final List<GatewayFilter> filters;

		...

		@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);  // 여기 : 우리가 등록한 filter(혹은 기본적으로 등록된 filter) 들이 동작한다.
				}
				else {
					return Mono.empty(); // complete
				}
			});
		}

	}

아래 로그는 FilteringWebHandler 로그이다.

등록된 Filter 들을 확인할 수 있다.

2022-07-17 20:01:37.968 DEBUG 91322 --- [ctor-http-nio-2] o.s.c.g.handler.FilteringWebHandler      : Sorted gatewayFilterFactories: [[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@5707f613}, order = -2147483648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@526e8108}, order = -2147482648], [ModifyResponseBody New content type = [null], In class = Object, Out class = ResponseFormatFilter.Response], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@11787b64}, order = -1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@319642db}, order = 0], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.GatewayMetricsFilter@35bfa1bb}, order = 0], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@77b3752b}, order = 10000], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration$NoLoadBalancerClientFilter@6b321262}, order = 10150], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@59498d94}, order = 2147483646], GatewayFilterAdapter{delegate=com.baemin.openapi.filter.global.SampleCustomGlobalGatewayFilter@713a35c5}, [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@62aeddc8}, order = 2147483647], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@6367a688}, order = 2147483647]]


아래는 하나의 Request에 대한 전체 로그이다.

... [ctor-http-nio-2] reactor.netty.http.server.HttpServer     : [d9c36f28-2, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:53838] Handler is being applied: org.springframework.http.server.reactive.ReactorHttpHandlerAdapter@687105a6
... [ctor-http-nio-2] o.s.w.s.adapter.HttpWebHandlerAdapter    : [d9c36f28-2] HTTP GET "/api/samples"
... [ctor-http-nio-2] o.s.c.g.h.RoutePredicateHandlerMapping   : Route matched: 4162e05b-be86-4b3b-a2c7-bfcc4d2c89b4
... [ctor-http-nio-2] o.s.c.g.h.RoutePredicateHandlerMapping   : Mapping [Exchange: GET http://localhost:8080/api/samples] to Route{id='4162e05b-be86-4b3b-a2c7-bfcc4d2c89b4', uri=http://localhost:8081, order=0, predicate=PredicateSpec$$Lambda$529/0x0000000800510c40, gatewayFilters=[[ModifyResponseBody New content type = [null], In class = Object, Out class = ResponseFormatFilter.Response]], metadata={}}
... [ctor-http-nio-2] o.s.c.g.h.RoutePredicateHandlerMapping   : [d9c36f28-2] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@2937f9e0
... [ctor-http-nio-2] o.s.c.g.handler.FilteringWebHandler      : Sorted gatewayFilterFactories: [[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@5707f613}, order = -2147483648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@526e8108}, order = -2147482648], [ModifyResponseBody New content type = [null], In class = Object, Out class = ResponseFormatFilter.Response], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@11787b64}, order = -1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@319642db}, order = 0], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.GatewayMetricsFilter@35bfa1bb}, order = 0], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@77b3752b}, order = 10000], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration$NoLoadBalancerClientFilter@6b321262}, order = 10150], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@59498d94}, order = 2147483646], GatewayFilterAdapter{delegate=com.baemin.openapi.filter.global.SampleCustomGlobalGatewayFilter@713a35c5}, [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@62aeddc8}, order = 2147483647], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@6367a688}, order = 2147483647]]
... [ctor-http-nio-2] r.n.resources.PooledConnectionProvider   : [d27fde7a, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081] Channel acquired, now: 1 active connections, 0 inactive connections and 0 pending acquire requests.
... [ctor-http-nio-2] r.netty.http.client.HttpClientConnect    : [d27fde7a-2, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081] Handler is being applied: {uri=http://localhost:8081/api/samples, method=GET}
... [ctor-http-nio-2] r.n.r.DefaultPooledConnectionProvider    : [d27fde7a-2, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081] onStateChange(GET{uri=/api/samples, connection=PooledConnection{channel=[id: 0xd27fde7a, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081]}}, [request_prepared])
... [ctor-http-nio-2] reactor.netty.channel.FluxReceive        : [d9c36f28-2, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:53838] FluxReceive{pending=0, cancelled=false, inboundDone=false, inboundError=null}: subscribing inbound receiver
... [ctor-http-nio-2] r.n.r.DefaultPooledConnectionProvider    : [d27fde7a-2, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081] onStateChange(GET{uri=/api/samples, connection=PooledConnection{channel=[id: 0xd27fde7a, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081]}}, [request_sent])
... [ctor-http-nio-2] r.n.http.client.HttpClientOperations     : [d27fde7a-2, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081] Received response (auto-read:false) : [Content-Type=application/json, Transfer-Encoding=chunked, Date=Sun, 17 Jul 2022 11:01:37 GMT]
... [ctor-http-nio-2] r.n.r.DefaultPooledConnectionProvider    : [d27fde7a-2, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081] onStateChange(GET{uri=/api/samples, connection=PooledConnection{channel=[id: 0xd27fde7a, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081]}}, [response_received])
... [ctor-http-nio-2] reactor.netty.channel.FluxReceive        : [d27fde7a-2, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081] FluxReceive{pending=0, cancelled=false, inboundDone=false, inboundError=null}: subscribing inbound receiver
... [ctor-http-nio-2] r.n.http.client.HttpClientOperations     : [d27fde7a-2, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081] Received last HTTP packet
... [ctor-http-nio-2] o.s.http.codec.json.Jackson2JsonDecoder  : Decoded [{timestamp=0, errorCode=, statusCode=200, statusMessage=OK, data=[{name=, age=12}, {name=길동 (truncated)...]
... [ctor-http-nio-2] o.s.http.codec.json.Jackson2JsonEncoder  : Encoding [Response(data={timestamp=0, errorCode=, statusCode=200, statusMessage=OK, data=[{name=현재, age= (truncated)...]
... [ctor-http-nio-2] r.n.http.server.HttpServerOperations     : [d9c36f28-2, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:53838] Decreasing pending responses, now 0
... [ctor-http-nio-2] r.n.http.server.HttpServerOperations     : [d9c36f28-2, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:53838] Last HTTP packet was sent, terminating the channel
... [ctor-http-nio-2] o.s.w.s.adapter.HttpWebHandlerAdapter    : [d9c36f28-2] Completed 200 OK
... [ctor-http-nio-2] r.n.http.server.HttpServerOperations     : [d9c36f28-2, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:53838] Last HTTP response frame
... [ctor-http-nio-2] r.n.r.DefaultPooledConnectionProvider    : [d27fde7a, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081] onStateChange(GET{uri=/api/samples, connection=PooledConnection{channel=[id: 0xd27fde7a, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081]}}, [response_completed])
... [ctor-http-nio-2] r.n.r.DefaultPooledConnectionProvider    : [d27fde7a, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081] onStateChange(GET{uri=/api/samples, connection=PooledConnection{channel=[id: 0xd27fde7a, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081]}}, [disconnecting])
... [ctor-http-nio-2] r.n.r.DefaultPooledConnectionProvider    : [d27fde7a, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081] Releasing channel
... [ctor-http-nio-2] r.n.resources.PooledConnectionProvider   : [d27fde7a, L:/127.0.0.1:53841 - R:localhost/127.0.0.1:8081] Channel cleaned, now: 0 active connections, 1 inactive connections and 0 pending acquire requests.
... [ctor-http-nio-2] r.n.r.DefaultPooledConnectionProvider    : [d27fde7a, L:/127.0.0.1:53841 ! R:localhost/127.0.0.1:8081] onStateChange(PooledConnection{channel=[id: 0xd27fde7a, L:/127.0.0.1:53841 ! R:localhost/127.0.0.1:8081]}, [disconnecting])



참고 (L, R, -, !) #

L : Local Address
R : Remote Address
- : active (active and so connected)
! : not active


AbstractChannel 에서 확인할 수 있다.

public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {

  ...

  @Override
  public String toString() {
      boolean active = isActive();
      if (strValActive == active && strVal != null) {
          return strVal;
      }

      SocketAddress remoteAddr = remoteAddress();
      SocketAddress localAddr = localAddress();
      if (remoteAddr != null) {
          StringBuilder buf = new StringBuilder(96)
              .append("[id: 0x")
              .append(id.asShortText())
              .append(", L:")
              .append(localAddr)
              .append(active? " - " : " ! ")
              .append("R:")
              .append(remoteAddr)
              .append(']');
          strVal = buf.toString();
      } else if (localAddr != null) {
          StringBuilder buf = new StringBuilder(64)
              .append("[id: 0x")
              .append(id.asShortText())
              .append(", L:")
              .append(localAddr)
              .append(']');
          strVal = buf.toString();
      } else {
          StringBuilder buf = new StringBuilder(16)
              .append("[id: 0x")
              .append(id.asShortText())
              .append(']');
          strVal = buf.toString();
      }

      strValActive = active;
      return strVal;
  }

  ...
}