Architecture/MSA

Spring Cloud Gateway - Global Filter

체리필터 2021. 1. 6. 11:16
반응형

기본적인 GlobalFilter 사용법

Global Filter를 커스터마이징 해서 사용하는 방법은 GlobalFilter 인터페이스를 구현하여 filter를 재정의 하면 된다.

@Slf4j
@Component
public class MacaronCustomGlobalFilter implements GlobalFilter, Ordered {
    @Bean
    public GlobalFilter customFilter() {
        return new MacaronCustomGlobalFilter();
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("##################### custom global filter #####################");
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

아래와 같이 ApiGateWay를 호출하면 log가 보임.

노란색 로그 확인. 2번 호출 되는것은 gateway filter factories에 아래와 같이 2번 들어가 있어서 그런 듯

2021-01-06 09:14:27.011 DEBUG 29800 --- [ctor-http-nio-3] o.s.c.g.handler.FilteringWebHandler      : 
Sorted gatewayFilterFactories: 
[
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@1859e55c}, order = -2147483648],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@cb03411}, order = -2147482648],
	[GatewayFilterAdapter{delegate=com.kst.macaront.gw.filter.MacaronCustomGlobalFilter@53079ae6}, order = -1],
	[GatewayFilterAdapter{delegate=com.kst.macaront.gw.filter.MacaronCustomGlobalFilter@718ad3a6}, order = -1],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@3f6906f4}, order = -1],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@d229912}, order = 0],
	[
		[StripPrefix parts = 1], order = 0
	],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@190bf8e4}, order = 10000],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@603c2dee}, order = 10100],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@5a06eeef}, order = 2147483646],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@50d666a2}, order = 2147483647],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@7a8b7e11}, order = 2147483647]
]

gateway filter factory에 2번 등록되어져서 2번 호출되는 이유는 찾아 봐야 할 듯

아래부터는 기 구현되어져 있는 Global Filter들이다.

 

Forward Routing Filter

매뉴얼( docs.spring.io/spring-cloud-gateway/docs/2.2.6.BUILD-SNAPSHOT/reference/html/#forward-routing-filter ) 상 내용에서는 url scheme에서 forward:// 패턴이 있게 된다면 DispatcherHandler 가 처리하게 된다고 함.

이와 관련하여 볼 수 있는 소스는 아래에 있음.

org.springframework.cloud.gateway.filter.ForwardRoutingFilter.java

public class ForwardRoutingFilter implements GlobalFilter, Ordered {

	private static final Log log = LogFactory.getLog(ForwardRoutingFilter.class);

	private final ObjectProvider<DispatcherHandler> dispatcherHandlerProvider;

	// do not use this dispatcherHandler directly, use getDispatcherHandler() instead.
	private volatile DispatcherHandler dispatcherHandler;

	public ForwardRoutingFilter(
			ObjectProvider<DispatcherHandler> dispatcherHandlerProvider) {
		this.dispatcherHandlerProvider = dispatcherHandlerProvider;
	}

	private DispatcherHandler getDispatcherHandler() {
		if (dispatcherHandler == null) {
			dispatcherHandler = dispatcherHandlerProvider.getIfAvailable();
		}

		return dispatcherHandler;
	}

	@Override
	public int getOrder() {
		return Ordered.LOWEST_PRECEDENCE;
	}

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);

		String scheme = requestUrl.getScheme();
		if (isAlreadyRouted(exchange) || !"forward".equals(scheme)) {
			return chain.filter(exchange);
		}

		// TODO: translate url?

		if (log.isTraceEnabled()) {
			log.trace("Forwarding to URI: " + requestUrl);
		}

		return this.getDispatcherHandler().handle(exchange);
	}

}

소스에서 볼 수 있는 것 처럼 scheme 검사하고 forward일 경우 dispatcherHandler 통해서 처리하는 것으로 보임.

순서는 Ordered.LOWEST_PRECEDENCE 라고 선언해 두었는데 해당 값을 찾아 보면 "int LOWEST_PRECEDENCE = Integer.MAX_VALUE;"라고선언되어있음. 가장 마지막에 실행되는 것으로 보임

사용은 아래 소스에서 기본적으로 Bean 생성해서 사용하는 것으로 보임.

package org.springframework.cloud.gateway.config;
/**
 * @author Spencer Gibb
 * @author Ziemowit Stolarczyk
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@EnableConfigurationProperties
@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class,
		WebFluxAutoConfiguration.class })
@AutoConfigureAfter({ GatewayLoadBalancerClientAutoConfiguration.class,
		GatewayClassPathWarningAutoConfiguration.class })
@ConditionalOnClass(DispatcherHandler.class)
public class GatewayAutoConfiguration {

...

	@Bean
	@ConditionalOnEnabledGlobalFilter
	public ForwardRoutingFilter forwardRoutingFilter(
			ObjectProvider<DispatcherHandler> dispatcherHandler) {
		return new ForwardRoutingFilter(dispatcherHandler);
	}

 

LoadBalancerClientFilter

url scheme에 lb:// 이 있다면 "이름"의 실제 호스트, 포트 정보를 resolve 해서 호출하게 됨.

예를 들어 eureka를 쓰는 경우 lb://myservice scheme 으로 호출하게 되면 eureka에서 해당 서비스의 이름을 쓰는 인스턴스 중 하나를 리턴하여 호출하게 됨.

관련된 필터 소스는 아래와 같음.

package org.springframework.cloud.gateway.filter;

/**
 * @deprecated in favour of {@link ReactiveLoadBalancerClientFilter}
 * @author Spencer Gibb
 * @author Tim Ysewyn
 */
@Deprecated
public class LoadBalancerClientFilter implements GlobalFilter, Ordered {

	/**
	 * Filter order for {@link LoadBalancerClientFilter}.
	 */
	public static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10100;

	private static final Log log = LogFactory.getLog(LoadBalancerClientFilter.class);

	protected final LoadBalancerClient loadBalancer;

	private LoadBalancerProperties properties;

	public LoadBalancerClientFilter(LoadBalancerClient loadBalancer,
			LoadBalancerProperties properties) {
		this.loadBalancer = loadBalancer;
		this.properties = properties;
	}

	@Override
	public int getOrder() {
		return LOAD_BALANCER_CLIENT_FILTER_ORDER;
	}

	@Override
	@SuppressWarnings("Duplicates")
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
		String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
		if (url == null
				|| (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
			return chain.filter(exchange);
		}
		// preserve the original url
		addOriginalRequestUrl(exchange, url);

		if (log.isTraceEnabled()) {
			log.trace("LoadBalancerClientFilter url before: " + url);
		}

		final ServiceInstance instance = choose(exchange);

		if (instance == null) {
			throw NotFoundException.create(properties.isUse404(),
					"Unable to find instance for " + url.getHost());
		}

		URI uri = exchange.getRequest().getURI();

		// if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
		// if the loadbalancer doesn't provide one.
		String overrideScheme = instance.isSecure() ? "https" : "http";
		if (schemePrefix != null) {
			overrideScheme = url.getScheme();
		}

		URI requestUrl = loadBalancer.reconstructURI(
				new DelegatingServiceInstance(instance, overrideScheme), uri);

		if (log.isTraceEnabled()) {
			log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
		}

		exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
		return chain.filter(exchange);
	}

	protected ServiceInstance choose(ServerWebExchange exchange) {
		return loadBalancer.choose(
				((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
	}

}

Deprecated 된 이유는 매뉴얼에서 알려주는 다음의 이유 때문인 것으로 보임

LoadBalancerClientFilter는 Blocking을 사용하는 ribbon 클라이언트를 사용해서인 듯

위의 내용으로 인해 ReactiveLoadBalancerClientFilter 를 앞으로 사용해야 하는 듯.

관련된 내용은 spring.io/blog/2020/03/25/spring-tips-spring-cloud-loadbalancer 참고하면 좋을 것으로 보임.

order 순서도 "public static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10100;" 와 같이 설정되어 있는데 정확한 의미는 모르겠음.

service를 못 찾을 경우 기본 설정 상 503 코드가 리턴 되지만 다음과 같이 yml 파일에 설정하면 404로 리턴되게 됨.

spring:
  cloud:
    gateway:
      loadbalancer:
        use404: true

 

ReactiveLoadBalancerClientFilter

기본적인 내용은 LoadBalancerClientFilter 와 같아 보이며 위에서 이야기 했듯이 spring.cloud.loadbalancer.ribbon.enabled 값을 false로 하게 될 경우 사용하게 된다.

관련된 소스는 너무 길어 package 및 Class 이름만 참고.

org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter.java

ribbon.enabled 값을 false로 하고 호출하게 될 경우 아래와 같이 gateway filter factory의 순서가 바뀌는 것을 볼 수 있다.

2021-01-06 09:00:25.590 DEBUG 20820 --- [ctor-http-nio-3] o.s.c.g.handler.FilteringWebHandler      : 
Sorted gatewayFilterFactories:

[
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@3149409c}, order = -2147483648],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@367b22e5}, order = -2147482648],
	[GatewayFilterAdapter{delegate=com.kst.macaront.gw.filter.MacaronCustomGlobalFilter@6528d339}, order = -1],
	[GatewayFilterAdapter{delegate=com.kst.macaront.gw.filter.MacaronCustomGlobalFilter@2dd2ff87}, order = -1],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@bdda8a7}, order = -1],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@2a9f8d47}, order = 0],
	[
		[StripPrefix parts = 1], order = 0
	],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@1c421b0f}, order = 10000],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@6a38e3d1}, order = 10150],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@51297528}, order = 2147483646],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@28cf179c}, order = 2147483647],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@4ce18cec}, order = 2147483647]
]

기존에는 LoadBalancerClientFilter를 사용하는 모습을 볼 수 있었지만 LoadBalancerClientFilter는 없어지고 대신에 ReactiveLoadBalancerClientFilter가 로그에서 보여진다.

NettyRoutingFilter, NettyWriteResponseFilter

netty를 사용하는 필터 같은데 정확히는 모르겠음.

RouteToRequestUrlFilter

scheme 패턴을 검토하고 url을 생성해서 proxy 해주는 필터로 보임.

RouteToRequestUrlFilter.java의 filter mehtod를 디버깅 해 보면 아래와 같이 url을 생성해서 다음 filter로 넘겨 줌.

http://localhost:8100/chauffeur/areas?areaType=STATE&parentCode=KR00 로 요청을 하였지만 route.predicate의 룰에 따라 http://localhost:8100/areas?areaType=STATE&parentCode=KR00 로 바뀌게 됨.

route 해줄 uri가 lb://CHAUFFEUR 이기 때문에 from인 http://localhost:8100을 lb://CHAUFFEUR로 바뀌게 된다.

WebsocketRoutingFilter

웹소켓 라우팅 필터로 ws://로 진입 시 해당 서비스로 proxy 해 준다.

GatewayMetricsFilter

gateway의 상태 정보를 보여주기 위한 용도로 사용 하는 것으로 보임.

springboot actuator를 사용하기 때문에 implementation 'org.springframework.boot:spring-boot-starter-actuator' 를 의존성에 넣어야 하며 metrics 관련 값은 다음과 같이 셋팅 되어 있어야 함.

spring:
  cloud:
    gateway:
      metrics:
        enabled: true

http://localhost:8100/actuator/metrics/gateway.requests 와 같이 접속하면 내용이 나온다고 하는데 잘 안나옴. 정확한 이유 모르겠음.

Marking An Exchange As Routed

중복되는 routing을 안하기 위해 ServerWebExchange의 gatewayAlreadyRouted 속성을 사용하여 판단한다. 이미 routing이 되었다면 다시 routing 하지 않고 skip 처리한다.

이를 사용하기 위해서 ServerWebExchangeUtils 의 GATEWAY_ALREADY_ROUTED_ATTR 상수를 사용한다.

	/**
	 * Used when a routing filter has been successfully called. Allows users to write
	 * custom routing filters that disable built in routing filters.
	 */
	public static final String GATEWAY_ALREADY_ROUTED_ATTR = qualify(
			"gatewayAlreadyRouted");

해당 값은 다음과 같은 메소드를 이용하여 판단하거나 저장한다.

	public static void setAlreadyRouted(ServerWebExchange exchange) {
		exchange.getAttributes().put(GATEWAY_ALREADY_ROUTED_ATTR, true);
	}

	public static void removeAlreadyRouted(ServerWebExchange exchange) {
		exchange.getAttributes().remove(GATEWAY_ALREADY_ROUTED_ATTR);
	}

	public static boolean isAlreadyRouted(ServerWebExchange exchange) {
		return exchange.getAttributeOrDefault(GATEWAY_ALREADY_ROUTED_ATTR, false);
	}

 

 

 

반응형