1. 概要

このチュートリアルでは、カスタムSpring Cloudゲートウェイフィルターを作成する方法を学習します。

このフレームワークは、前回の投稿新しいSpring Cloud Gatewayの探索で紹介しました。ここでは、多くの組み込みフィルターを確認しました。

この機会に、さらに深く掘り下げて、APIGatewayを最大限に活用するためのカスタムフィルターを作成します。

最初に、ゲートウェイによって処理されるすべての単一の要求に影響を与えるグローバルフィルターを作成する方法を確認します。 次に、特定のルートとリクエストにきめ細かく適用できるゲートウェイフィルターファクトリを作成します。

最後に、より高度なシナリオに取り組み、リクエストまたはレスポンスを変更する方法、さらにはリクエストを他のサービスへの呼び出しとリアクティブな方法でチェーンする方法を学習します。

2. プロジェクトの設定

まず、APIゲートウェイとして使用する基本的なアプリケーションを設定します。

2.1. Maven構成

Spring Cloudライブラリを使用する場合は、依存関係を処理するために依存関係管理構成を設定することをお勧めします。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR4</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

これで、使用している実際のバージョンを指定せずにSpring Cloudライブラリを追加できます。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

最新のSpringCloud Release Trainバージョンは、MavenCentral検索エンジンを使用して見つけることができます。 もちろん、バージョンがSpring Cloudドキュメントで使用しているSpring Bootバージョンと互換性があることを常に確認する必要があります。

2.2. APIゲートウェイの構成

ポート8081でローカルに実行されている、 / resource を押すとリソース(簡単にするために単純な String )を公開する2番目のアプリケーションがあると想定します。 ]。

これを念頭に置いて、このサービスへのリクエストをプロキシするようにゲートウェイを構成します。 簡単に言うと、URIパスに / service プレフィックスを付けてゲートウェイにリクエストを送信すると、このサービスに呼び出しが転送されます。

したがって、ゲートウェイで / service / resource を呼び出すと、String応答を受信する必要があります。

これを実現するために、アプリケーションプロパティを使用してこのルートを構成します。

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

さらに、ゲートウェイプロセスを適切にトレースできるようにするために、いくつかのログも有効にします。

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

3. グローバルフィルターの作成

ゲートウェイハンドラーが要求がルートと一致すると判断すると、フレームワークは要求をフィルターチェーンに渡します。 これらのフィルターは、要求が送信される前または後にロジックを実行する場合があります。

このセクションでは、簡単なグローバルフィルターを作成することから始めます。 つまり、すべてのリクエストに影響します。

まず、プロキシリクエストが送信される前にロジックを実行する方法を確認します(「プレ」フィルターとも呼ばれます)。

3.1. グローバルな「プレ」フィルターロジックの作成

すでに述べたように、この時点で単純なフィルターを作成します。ここでの主な目的は、フィルターが実際に正しいタイミングで実行されていることを確認することだけだからです。 単純なメッセージをログに記録するだけでうまくいきます。

カスタムグローバルフィルターを作成するために必要なのは、Spring Cloud Gateway GlobalFilter インターフェースを実装し、それをBeanとしてコンテキストに追加することだけです。

@Component
public class LoggingGlobalPreFilter implements GlobalFilter {

    final Logger logger =
      LoggerFactory.getLogger(LoggingGlobalPreFilter.class);

    @Override
    public Mono<Void> filter(
      ServerWebExchange exchange,
      GatewayFilterChain chain) {
        logger.info("Global Pre Filter executed");
        return chain.filter(exchange);
    }
}

ここで何が起こっているかを簡単に確認できます。 このフィルターが呼び出されると、メッセージをログに記録し、フィルターチェーンの実行を続行します。

ここで、「ポスト」フィルターを定義しましょう。これは、リアクティブプログラミングモデルとSpring Webflux API に慣れていない場合は、少し注意が必要です。

3.2. グローバルな「ポスト」フィルターロジックの作成

定義したグローバルフィルターについてもう1つ注意すべき点は、GlobalFilterインターフェイスが1つのメソッドのみを定義していることです。 したがって、ラムダ式として表現できるため、フィルターを簡単に定義できます。

たとえば、構成クラスで「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 http://localhost/service/resource]
  to Route{id='service_route', uri=http://localhost:8081, order=0, predicate=Paths: [/service/**],
  match trailing slash: true, gatewayFilters=[[[RewritePath /service(?<segment>/?.*) = '${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=http://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

ご覧のとおり、ゲートウェイがリクエストをサービスに転送する前後に、フィルターが効果的に実行されます。

当然、「前」と「後」のロジックを1つのフィルターに組み合わせることができます。

@Component
public class FirstPreLastPostGlobalFilter
  implements GlobalFilter, Ordered {

    final Logger logger =
      LoggerFactory.getLogger(FirstPreLastPostGlobalFilter.class);

    @Override
    public Mono<Void> 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インターフェースを実装することもできます。

フィルタチェーンの性質上、優先度の低い(チェーン内の下位の)フィルタは、早い段階で「pre」ロジックを実行しますが、「post」実装は後で呼び出されます。

4. GatewayFilterの作成

グローバルフィルターは非常に便利ですが、一部のルートにのみ適用されるきめ細かいカスタムゲートウェイフィルター操作を実行する必要がある場合がよくあります。

4.1. GatewayFilterFactoryの定義

GatewayFilter を実装するには、GatewayFilterFactoryインターフェイスを実装する必要があります。 Spring Cloud Gatewayは、プロセスを簡素化するための抽象クラスAbstractGatewayFilterFactoryクラスも提供します。

@Component
public class LoggingGatewayFilterFactory extends 
  AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {

    final Logger logger =
      LoggerFactory.getLogger(LoggingGatewayFilterFactory.class);

    public LoggingGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        // ...
    }

    public static class Config {
        // ...
    }
}

ここでは、GatewayFilterFactoryの基本構造を定義しました。 Configクラスを使用して、フィルターを初期化するときにフィルターをカスタマイズします。

この場合、たとえば、構成で3つの基本フィールドを定義できます。

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をプロパティに登録する

これで、アプリケーションのプロパティで以前に定義したルートにフィルターを簡単に登録できます。

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

構成引数を指定するだけです。 ここで重要な点は、このアプローチが正しく機能するには、LoggingGatewayFilterFactory.Configクラスで構成された引数のないコンストラクターとセッターが必要なことです。

代わりにコンパクト表記を使用してフィルターを構成する場合は、次のように実行できます。

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

工場をもう少し微調整する必要があります。 つまり、 ShortcutFieldOrder メソッドをオーバーライドして、ショートカットプロパティが使用する順序と引数の数を示す必要があります。

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

4.3. GatewayFilterの注文

フィルターチェーン内のフィルターの位置を構成する場合は、プレーンラムダ式の代わりに AbstractGatewayFilterFactory#applyメソッドからOrderedGatewayFilterインスタンスを取得できます。

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

4.4. GatewayFilterをプログラムで登録する

さらに、プログラムでフィルターを登録することもできます。 今度は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>/?.*)", "$\\{segment}")
              .filter(loggingFactory.apply(
              new Config("My Custom Message", true, true))))
            .uri("http://localhost:8081"))
      .build();
}

5. 高度なシナリオ

これまでのところ、ゲートウェイプロセスのさまざまな段階でメッセージをログに記録するだけです。

通常、より高度な機能を提供するためにフィルターが必要です。 たとえば、受け取ったリクエストを確認または操作したり、取得したレスポンスを変更したり、リアクティブストリームを他の異なるサービスへの呼び出しとチェーンしたりする必要がある場合があります。

次に、これらのさまざまなシナリオの例を示します。

5.1. リクエストの確認と変更

架空のシナリオを想像してみましょう。 私たちのサービスは、 localequeryパラメーターに基づいてコンテンツを提供していました。 次に、代わりに Accept-Language headerを使用するようにAPIを変更しましたが、一部のクライアントは引き続きクエリパラメーターを使用しています。

したがって、次のロジックに従って正規化するようにゲートウェイを構成する必要があります。

  1. Accept-Language ヘッダーを受け取った場合、それを維持したいと思います
  2. それ以外の場合は、 localequeryパラメーター値を使用します
  3. それも存在しない場合は、デフォルトのロケールを使用してください
  4. 最後に、localeクエリパラメータを削除します

注:ここでは簡単にするために、フィルターロジックのみに焦点を当てます。 実装全体を見るには、チュートリアルの最後にコードベースへのリンクがあります。

次に、ゲートウェイフィルターを「事前」フィルターとして構成しましょう。

(exchange, chain) -> {
    if (exchange.getRequest()
      .getHeaders()
      .getAcceptLanguage()
      .isEmpty()) {
        // populate the Accept-Language header...
    }

    // remove the query param...
    return chain.filter(exchange);
};

ここでは、ロジックの最初の側面を処理しています。 ServerHttpRequestオブジェクトの検査は非常に簡単であることがわかります。 この時点では、ヘッダーのみにアクセスしましたが、次に説明するように、他の属性も同じように簡単に取得できます。

String queryParamLocale = exchange.getRequest()
  .getQueryParams()
  .getFirst("locale");

Locale requestLocale = Optional.ofNullable(queryParamLocale)
  .map(l -> Locale.forLanguageTag(l))
  .orElse(config.getDefaultLocale());

これで、動作の次の2つのポイントについて説明しました。 ただし、リクエストはまだ変更されていません。 このために、変異機能を利用する必要があります。

これにより、フレームワークはエンティティのデコレータを作成し、元のオブジェクトを変更せずに維持します。

HttpHeaders mapオブジェクトへの参照を取得できるため、ヘッダーの変更は簡単です。

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

ただし、一方で、URIの変更は簡単な作業ではありません。

元のexchangeオブジェクトから、元の ServerHttpRequest インスタンスを変更して、新しいServerWebExchangeインスタンスを取得する必要があります。

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

return chain.filter(modifiedExchange);

次に、クエリパラメータを削除して、元のリクエストURIを更新します。

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

さあ、試してみましょう。 コードベースでは、次のチェーンフィルターを呼び出す前にログエントリを追加して、リクエストで何が送信されているかを正確に確認しました。

5.2. 応答の変更

同じケースのシナリオを進めて、ここで「投稿」フィルターを定義します。 私たちの架空のサービスは、従来の Content-Language ヘッダーを使用する代わりに、最終的に選択した言語を示すカスタムヘッダーを取得するために使用されていました。

したがって、新しいフィルターでこの応答ヘッダーを追加する必要がありますが、これは、前のセクションで紹介したlocaleヘッダーがリクエストに含まれている場合に限ります。

(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);
                });
        }));
}

応答オブジェクトへの参照を簡単に取得でき、要求のように変更するためにそのコピーを作成する必要はありません。

これは、チェーン内のフィルターの順序の重要性の良い例です。 前のセクションで作成したものの後にこのフィルターの実行を構成すると、ここの exchange オブジェクトには、クエリパラメーターを持つことのないServerHttpRequestへの参照が含まれます。

mutate logicのおかげで、元のリクエストへの参照がまだ残っているため、すべての「pre」フィルターの実行後にこれが効果的にトリガーされることも問題ではありません。

5.3. 他のサービスへのリクエストの連鎖

架空のシナリオの次のステップは、3番目のサービスに依存して、使用するAccept-Languageヘッダーを示します。

したがって、このサービスを呼び出す新しいフィルターを作成し、その応答本文をプロキシされたサービスAPIのリクエストヘッダーとして使用します。

リアクティブ環境では、これは、非同期実行のブロックを回避するために要求を連鎖させることを意味します。

フィルタでは、言語サービスにリクエストを送信することから始めます。

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

この流暢な操作を返すことに注意してください。これは、前述したように、呼び出しの出力をプロキシされたリクエストにチェーンするためです。

次のステップは、応答本文または応答が成功しなかった場合の構成から言語を抽出し、それを解析することです。

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

最後に、前と同じように LanguageRange valueをリクエストヘッダーとして設定し、フィルターチェーンを続行します。

.map(range -> {
    exchange.getRequest()
      .mutate()
      .headers(h -> h.setAcceptLanguage(range))
      .build();

    return exchange;
}).flatMap(chain::filter);

これで、インタラクションは非ブロッキング方式で実行されます。

6. 結論

カスタムSpring Cloudゲートウェイフィルターの作成方法と、要求エンティティと応答エンティティの操作方法を学習したので、このフレームワークを最大限に活用する準備が整いました。

いつものように、すべての完全な例は、GitHubにあります。 テストするには、Mavenを介して統合テストとライブテストを実行する必要があることに注意してください。