커스텀 스프링 클라우드 게이트웨이 필터 작성

1. 개요

이 튜토리얼에서는 커스텀 Spring Cloud Gateway 필터를 작성하는 방법을 배웁니다.

이 프레임 워크는 이전 게시물 인 Exploring the New Spring Cloud Gateway에서 많은 기본 제공 필터를 살펴 보았습니다.

이번에는 API Gateway를 최대한 활용하기 위해 사용자 지정 필터를 작성하겠습니다.

먼저 게이트웨이에서 처리하는 모든 단일 요청에 영향을 미치는 전역 필터를 만드는 방법을 살펴 보겠습니다. 그런 다음 특정 경로 및 요청에 세부적으로 적용 할 수있는 게이트웨이 필터 팩토리를 작성합니다.

마지막으로, 요청이나 응답을 수정하는 방법, 요청을 다른 서비스에 대한 호출과 연결하는 방법까지 더 고급 시나리오에서 작업 할 것입니다.

2. 프로젝트 설정

API 게이트웨이로 사용할 기본 애플리케이션을 설정하는 것으로 시작합니다.

2.1. Maven 구성

Spring Cloud 라이브러리로 작업 할 때 항상 종속성 관리 구성을 설정하여 종속성을 처리하는 것이 좋습니다.

   org.springframework.cloud spring-cloud-dependencies Hoxton.SR4 pom import   

이제 사용중인 실제 버전을 지정하지 않고도 Spring Cloud 라이브러리를 추가 할 수 있습니다.

 org.springframework.cloud spring-cloud-starter-gateway 

최신 Spring Cloud Release Train 버전은 Maven Central 검색 엔진을 사용하여 찾을 수 있습니다. 물론 우리는 항상 해당 버전이 Spring Cloud 문서에서 사용중인 Spring Boot 버전과 호환되는지 확인해야합니다.

2.2. API Gateway 구성

포트 8081 에서 로컬로 실행되는 두 번째 애플리케이션이 있다고 가정합니다.이 애플리케이션은 / resource를 누를 때 리소스 (간단 함을 위해 단순 String ) 를 노출 합니다 .

이를 염두에두고이 서비스에 대한 요청을 프록시하도록 게이트웨이를 구성합니다. 간단히 말해서 URI 경로에 / service 접두사 를 사용하여 게이트웨이에 요청을 보낼 때 호출을이 서비스로 전달합니다.

따라서 게이트웨이에서 / service / resource 를 호출 하면 문자열 응답을 받아야합니다 .

이를 위해 애플리케이션 속성을 사용하여이 경로를 구성합니다 .

spring: cloud: gateway: routes: - id: service_route uri: //localhost:8081 predicates: - Path=/service/** filters: - RewritePath=/service(?/?.*), $\{segment}

또한 게이트웨이 프로세스를 제대로 추적 할 수 있도록 일부 로그도 활성화합니다.

logging: level: org.springframework.cloud.gateway: DEBUG reactor.netty.http.client: DEBUG

3. 글로벌 필터 생성

게이트웨이 핸들러가 요청이 경로와 일치한다고 판단하면 프레임 워크는 필터 체인을 통해 요청을 전달합니다. 이러한 필터는 요청이 전송되기 전이나 후에 로직을 실행할 수 있습니다.

이 섹션에서는 간단한 전역 필터를 작성하는 것으로 시작합니다. 즉, 모든 단일 요청에 영향을 미칩니다.

먼저 프록시 요청이 전송되기 전에 로직을 실행하는 방법을 살펴 보겠습니다 ( "pre"필터라고도 함).

3.1. 전역 "사전"필터 논리 작성

우리가 말했듯이, 여기서 주요 목표는 필터가 실제로 정확한 순간에 실행되는 것을 보는 것이므로이 시점에서 간단한 필터를 만들 것입니다. 간단한 메시지를 기록하기 만하면됩니다.

커스텀 글로벌 필터를 생성하기 위해해야 ​​할 일은 Spring Cloud Gateway GlobalFilter 인터페이스 를 구현하고 이를 컨텍스트에 빈으로 추가하는 것입니다.

@Component public class LoggingGlobalPreFilter implements GlobalFilter { final Logger logger = LoggerFactory.getLogger(LoggingGlobalPreFilter.class); @Override public Mono filter( ServerWebExchange exchange, GatewayFilterChain chain) { logger.info("Global Pre Filter executed"); return chain.filter(exchange); } }

여기서 무슨 일이 일어나고 있는지 쉽게 볼 수 있습니다. 이 필터가 호출되면 메시지를 기록하고 필터 체인 실행을 계속합니다.

이제 Reactive 프로그래밍 모델과 Spring Webflux API에 익숙하지 않은 경우 약간 까다로울 수있는 "post"필터를 정의 해 보겠습니다.

3.2. 글로벌 "포스트"필터 로직 작성

방금 정의한 전역 필터에 대해 주목해야 할 또 다른 사항은 GlobalFilter 인터페이스가 하나의 메서드 만 정의 한다는 것입니다. 따라서 람다 표현식으로 표현할 수있어 필터를 편리하게 정의 할 수 있습니다.

예를 들어 구성 클래스에서 "post"필터를 정의 할 수 있습니다.

@Configuration public class LoggingGlobalFiltersConfigurations { final Logger logger = LoggerFactory.getLogger( LoggingGlobalFiltersConfigurations.class); @Bean public GlobalFilter postGlobalFilter() { return (exchange, chain) -> { return chain.filter(exchange) .then(Mono.fromRunnable(() -> { logger.info("Global Post Filter executed"); })); }; } }

간단히 말해서, 여기서 우리는 체인이 실행을 완료 한 후 새로운 Mono 인스턴스를 실행하고 있습니다.

게이트웨이 서비스에서 / service / resource URL 을 호출 하고 로그 콘솔을 확인하여 지금 사용해 보겠습니다 .

DEBUG --- o.s.c.g.h.RoutePredicateHandlerMapping: Route matched: service_route DEBUG --- o.s.c.g.h.RoutePredicateHandlerMapping: Mapping [Exchange: GET //localhost/service/resource] to Route{id='service_route', uri=//localhost:8081, order=0, predicate=Paths: [/service/**], match trailing slash: true, gatewayFilters=[[[RewritePath /service(?/?.*) = '${segment}'], order = 1]]} INFO --- c.b.s.c.f.global.LoggingGlobalPreFilter: Global Pre Filter executed DEBUG --- r.netty.http.client.HttpClientConnect: [id: 0x58f7e075, L:/127.0.0.1:57215 - R:localhost/127.0.0.1:8081] Handler is being applied: {uri=//localhost:8081/resource, method=GET} DEBUG --- r.n.http.client.HttpClientOperations: [id: 0x58f7e075, L:/127.0.0.1:57215 - R:localhost/127.0.0.1:8081] Received response (auto-read:false) : [Content-Type=text/html;charset=UTF-8, Content-Length=16] INFO --- c.f.g.LoggingGlobalFiltersConfigurations: Global Post Filter executed DEBUG --- r.n.http.client.HttpClientOperations: [id: 0x58f7e075, L:/127.0.0.1:57215 - R:localhost/127.0.0.1:8081] Received last HTTP packet

보시다시피 게이트웨이가 요청을 서비스로 전달하기 전후에 필터가 효과적으로 실행됩니다.

당연히 "사전"및 "사후"로직을 단일 필터에 결합 할 수 있습니다.

@Component public class FirstPreLastPostGlobalFilter implements GlobalFilter, Ordered { final Logger logger = LoggerFactory.getLogger(FirstPreLastPostGlobalFilter.class); @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { logger.info("First Pre Global Filter"); return chain.filter(exchange) .then(Mono.fromRunnable(() -> { logger.info("Last Post Global Filter"); })); } @Override public int getOrder() { return -1; } }

체인의 필터 배치에 관심이 있다면 Ordered 인터페이스를 구현할 수도 있습니다 .

필터 체인의 특성으로 인해 우선 순위가 낮은 필터 (체인에서 더 낮은 순서)는 이전 단계에서 "사전"로직을 실행하지만 "사후"구현은 나중에 호출됩니다.

4. 만들기 GatewayFilter

전역 필터는 매우 유용하지만 일부 경로에만 적용되는 세분화 된 사용자 지정 게이트웨이 필터 작업을 실행해야하는 경우가 많습니다.

4.1. GatewayFilterFactory 정의

구현하기 위해 GatewayFilter를 , 우리가 구현해야합니다 GatewayFilterFactory의 인터페이스를. 봄 클라우드 게이트웨이는 또한 공정의 단순화하기 위해 추상 클래스를 제공 AbstractGatewayFilterFactory 클래스를 :

@Component public class LoggingGatewayFilterFactory extends AbstractGatewayFilterFactory { final Logger logger = LoggerFactory.getLogger(LoggingGatewayFilterFactory.class); public LoggingGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { // ... } public static class Config { // ... } }

여기에서 GatewayFilterFactory 의 기본 구조를 정의했습니다 . 필터를 초기화 할 때 Config 클래스를 사용하여 필터를 사용자 정의합니다.

예를 들어이 경우 구성에서 세 가지 기본 필드를 정의 할 수 있습니다.

public static class Config { private String baseMessage; private boolean preLogger; private boolean postLogger; // contructors, getters and setters... }

간단히 말해서 이러한 필드는 다음과 같습니다.

  1. 로그 항목에 포함될 사용자 지정 메시지
  2. 요청을 전달하기 전에 필터가 기록해야하는지 여부를 나타내는 플래그
  3. 프록시 된 서비스에서 응답을받은 후 필터가 기록해야하는지 여부를 나타내는 플래그

이제 이러한 구성을 사용하여 GatewayFilter 인스턴스 를 검색 할 수 있습니다. 다시 람다 함수로 표시 할 수 있습니다.

@Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { // Pre-processing if (config.isPreLogger()) { logger.info("Pre GatewayFilter logging: " + config.getBaseMessage()); } return chain.filter(exchange) .then(Mono.fromRunnable(() -> { // Post-processing if (config.isPostLogger()) { logger.info("Post GatewayFilter logging: " + config.getBaseMessage()); } })); }; }

4.2. 등록과 GatewayFilter 속성과를

We can now easily register our filter to the route we defined previously in the application properties:

... filters: - RewritePath=/service(?/?.*), $\{segment} - name: Logging args: baseMessage: My Custom Message preLogger: true postLogger: true

We simply have to indicate the configuration arguments. An important point here is that we need a no-argument constructor and setters configured in our LoggingGatewayFilterFactory.Config class for this approach to work properly.

If we want to configure the filter using the compact notation instead, then we can do:

filters: - RewritePath=/service(?/?.*), $\{segment} - Logging=My Custom Message, true, true

We'll need to tweak our factory a little bit more. In short, we have to override the shortcutFieldOrder method, to indicate the order and how many arguments the shortcut property will use:

@Override public List shortcutFieldOrder() { return Arrays.asList("baseMessage", "preLogger", "postLogger"); }

4.3. Ordering the GatewayFilter

If we want to configure the position of the filter in the filter chain, we can retrieve an OrderedGatewayFilter instance from the AbstractGatewayFilterFactory#apply method instead of a plain lambda expression:

@Override public GatewayFilter apply(Config config) { return new OrderedGatewayFilter((exchange, chain) -> { // ... }, 1); }

4.4. Registering the GatewayFilter Programmatically

Furthermore, we can register our filter programmatically, too. Let's redefine the route we've been using, this time by setting up a RouteLocator bean:

@Bean public RouteLocator routes( RouteLocatorBuilder builder, LoggingGatewayFilterFactory loggingFactory) { return builder.routes() .route("service_route_java_config", r -> r.path("/service/**") .filters(f -> f.rewritePath("/service(?/?.*)", "$\\{segment}") .filter(loggingFactory.apply( new Config("My Custom Message", true, true)))) .uri("//localhost:8081")) .build(); }

5. Advanced Scenarios

So far, all we've been doing is logging a message at different stages of the gateway process.

Usually, we need our filters to provide more advanced functionality. For instance, we may need to check or manipulate the request we received, modify the response we're retrieving, or even chain the reactive stream with calls to other different services.

Next, we'll see examples of these different scenarios.

5.1. Checking and Modifying the Request

Let's imagine a hypothetical scenario. Our service used to serve its content based on a locale query parameter. Then, we changed the API to use the Accept-Language header instead, but some clients are still using the query parameter.

Thus, we want to configure the gateway to normalize following this logic:

  1. if we receive the Accept-Language header, we want to keep that
  2. otherwise, use the locale query parameter value
  3. if that's not present either, use a default locale
  4. finally, we want to remove the locale query param

Note: To keep things simple here, we'll focus only on the filter logic; to have a look at the whole implementation we'll find a link to the codebase at the end of the tutorial.

Let's configure our gateway filter as a “pre” filter then:

(exchange, chain) -> { if (exchange.getRequest() .getHeaders() .getAcceptLanguage() .isEmpty()) { // populate the Accept-Language header... } // remove the query param... return chain.filter(exchange); };

Here we're taking care of the first aspect of the logic. We can see that inspecting the ServerHttpRequest object is really simple. At this point, we accessed only its headers, but as we'll see next, we can obtain other attributes just as easily:

String queryParamLocale = exchange.getRequest() .getQueryParams() .getFirst("locale"); Locale requestLocale = Optional.ofNullable(queryParamLocale) .map(l -> Locale.forLanguageTag(l)) .orElse(config.getDefaultLocale());

Now we've covered the next two points of the behavior. But we haven't modified the request, yet. For this, we'll have to make use of the mutate capability.

With this, the framework will be creating a Decorator of the entity, maintaining the original object unchanged.

Modifying the headers is simple because we can obtain a reference to the HttpHeaders map object:

exchange.getRequest() .mutate() .headers(h -> h.setAcceptLanguageAsLocales( Collections.singletonList(requestLocale)))

But, on the other hand, modifying the URI is not a trivial task.

We'll have to obtain a new ServerWebExchange instance from the original exchange object, modifying the original ServerHttpRequest instance:

ServerWebExchange modifiedExchange = exchange.mutate() // Here we'll modify the original request: .request(originalRequest -> originalRequest) .build(); return chain.filter(modifiedExchange);

Now it's time to update the original request URI by removing the query params:

originalRequest -> originalRequest.uri( UriComponentsBuilder.fromUri(exchange.getRequest() .getURI()) .replaceQueryParams(new LinkedMultiValueMap()) .build() .toUri())

There we go, we can try it out now. In the codebase, we added log entries before calling the next chain filter to see exactly what is getting sent in the request.

5.2. Modifying the Response

Proceeding with the same case scenario, we'll define a “post” filter now. Our imaginary service used to retrieve a custom header to indicate the language it finally chose instead of using the conventional Content-Language header.

Hence, we want our new filter to add this response header, but only if the request contains the locale header we introduced in the previous section.

(exchange, chain) -> { return chain.filter(exchange) .then(Mono.fromRunnable(() -> { ServerHttpResponse response = exchange.getResponse(); Optional.ofNullable(exchange.getRequest() .getQueryParams() .getFirst("locale")) .ifPresent(qp -> { String responseContentLanguage = response.getHeaders() .getContentLanguage() .getLanguage(); response.getHeaders() .add("Bael-Custom-Language-Header", responseContentLanguage); }); })); }

We can obtain a reference to the response object easily, and we don't need to create a copy of it to modify it, as with the request.

This is a good example of the importance of the order of the filters in the chain; if we configure the execution of this filter after the one we created in the previous section, then the exchange object here will contain a reference to a ServerHttpRequest that will never have any query param.

It doesn't even matter that this is effectively triggered after the execution of all the “pre” filters because we still have a reference to the original request, thanks to the mutate logic.

5.3. Chaining Requests to Other Services

The next step in our hypothetical scenario is relying on a third service to indicate which Accept-Language header we should use.

Thus, we'll create a new filter which makes a call to this service, and uses its response body as the request header for the proxied service API.

In a reactive environment, this means chaining requests to avoid blocking the async execution.

In our filter, we'll start by making the request to the language service:

(exchange, chain) -> { return WebClient.create().get() .uri(config.getLanguageEndpoint()) .exchange() // ... }

Notice we're returning this fluent operation, because, as we said, we'll chain the output of the call with our proxied request.

The next step will be to extract the language – either from the response body or from the configuration if the response was not successful – and parse it:

// ... .flatMap(response -> { return (response.statusCode() .is2xxSuccessful()) ? response.bodyToMono(String.class) : Mono.just(config.getDefaultLanguage()); }).map(LanguageRange::parse) // ...

Finally, we'll set the LanguageRange value as the request header as we did before, and continue the filter chain:

.map(range -> { exchange.getRequest() .mutate() .headers(h -> h.setAcceptLanguage(range)) .build(); return exchange; }).flatMap(chain::filter);

That's it, now the interaction will be carried out in a non-blocking manner.

6. Conclusion

사용자 정의 Spring Cloud Gateway 필터를 작성하는 방법과 요청 및 응답 엔티티를 조작하는 방법을 배웠으므로 이제이 프레임 워크를 최대한 활용할 준비가되었습니다.

항상 그렇듯이 모든 전체 예제는 GitHub에서 찾을 수 있습니다. 테스트하려면 Maven을 통해 통합 및 라이브 테스트를 실행해야합니다.