RouterFunctionでリクエストをインターセプトする

はじめに

Router Functionを利用しているときのサーブレットフィルターとかインターセプターみたいなのってどうやってやるんだろって言うのが気になったのでちょっと調べてみます。

やってみる

環境

環境は以下の通り

$ java --version
openjdk 15 2020-09-15
OpenJDK Runtime Environment (build 15+36-1562)
OpenJDK 64-Bit Server VM (build 15+36-1562, mixed mode, sharing)

$ mvn --version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 15, vendor: Oracle Corporation, runtime: /home/someone/.sdkman/candidates/java/15-open
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-51-generic", arch: "amd64", family: "unix"

プロジェクトはSpring Initializrで作成して設定は以下のようにしました。

f:id:yuya_hirooka:20201016044037p:plain

ざっくりやるとこ

大きくは以下の2つの手順を踏むっぽいです

  • HandlerFilterFunctionを実装ししたFilter関数を作成する
  • RouterFunctionにFilterとして登録する

下準備

まずはハンドラーを作成して、Router Functionクラスに登録します。

@Component
public class HelloResource {

    public Mono<ServerResponse> greeting(ServerRequest request){
        String name = request.queryParam("name").orElse("world");
        return ServerResponse.ok().body(Mono.just(String.format("Hello, %s", name)), String.class);
    }
}
@SpringBootApplication
public class RouterFunctionFilterApplication {

    private HelloResource helloResource;

    public RouterFunctionFilterApplication(HelloResource helloResource) {
        this.helloResource = helloResource;
    }

    public static void main(String[] args) {
        SpringApplication.run(RouterFunctionFilterApplication.class, args);
    }

    @Bean
    public RouterFunction router() {
        return RouterFunctions.route(GET("/hello"), helloResource::greeting);
    }

}

ハンドラーはクエリストリングで名前を受け取って挨拶を返すだけのものです。
cURLでリクエストを送ります。

$ curl localhost:8080/hello?name=henohenomoheji
Hello, henohenomoheji

ここまでで下準備は完了です。

HandlerFilterFunctionを実装ししたFilter関数を作成する

Router Functionでリクエストをインターセプトする場合フィルターを行なうためのクラス(もしくは関数)を作成する必要があります。このFilterはHandlerFilterFunctionインターフェースを実装することでフィルター関数を作成します。
このブログではリクエストを受け取ってその情報をログに出力するだけの簡単なフィルターを作成します。
単にフィルター関数をラムダとして書いてやることもできますが、わかりやすさのために一旦フィルタークラスを作成しようと思います。

HandlerFilterFunctionのfilter(ServerRequest request, HandlerFunction<T> next)javadocをみると以下のように書かれています。

/**
* Apply this filter to the given handler function. The given
* {@linkplain HandlerFunction handler function} represents the next entity in the chain,
* and can be {@linkplain HandlerFunction#handle(ServerRequest) invoked} in order to
* proceed to this entity, or not invoked to block the chain.
* @param request the request
* @param next the next handler or filter function in the chain
* @return the filtered response
* @see ServerRequestWrapper
*/
Mono<R> filter(ServerRequest request, HandlerFunction<T> next);

引数としてリクエストと次のフィルター(もしくはハンドラー)を表すHandlerFunction受け取るみたいですね。 HandlerFunctionインターフェースはhandle(ServerRequest request)メソッドを持っていてこいつにリクエストを渡すことで次のフィルターをチェインできるっぽいです。また、javadocによるとその戻り値はresponseになるようなので、レスポンスになにか共通処理を入れたい場合はその戻り値に対してゴニョゴニョできるみたいです。
今回の場合は戻り値に特に処理はくわえないので、たんにhandle(ServerRequest request)の実行をそのままレスポンスとして返してやれば良さそうです。
具体的には以下のようなクラスを作成します。

public class RequestLoggingFilter implements HandlerFilterFunction {

    private final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);

    @Override
    public Mono filter(ServerRequest request, HandlerFunction next) {
        logger.info(String.format("%s, %s", request, request.queryParams()));
        return next.handle(request);
    }
}

これでフィルターはできました。

RouterFunctionに作成したフィルターを登録する

作成したフィルターをRouterFunctionに登録します。

@SpringBootApplication
public class RouterFunctionFilterApplication {

    private final HelloResource helloResource;

    public RouterFunctionFilterApplication(HelloResource helloResource) {
        this.helloResource = helloResource;
    }

    public static void main(String[] args) {
        SpringApplication.run(RouterFunctionFilterApplication.class, args);
    }

    @Bean
    public RouterFunction router() {
        return RouterFunctions.route(GET("/hello"), helloResource::greeting)
                .filter(new RequestLoggingFilter());
    }

}

RouterFunctions.BuilderのfilterメソッドにHndlerFunctionの実装を渡してやると内部でRouterFunctions.FilteredRouterFunctionと呼ばれるWrapされたRouterFunctionを返してくれます。

リクエストを送ると以下のログが出力されました。

2020-10-17 11:48:37.360  INFO 20922 --- [or-http-epoll-2] d.h.r.RequestLoggingFilter               : HTTP GET /hello, {name=[henohenomoheji]}

複数フィルターを登録する

別のフィルターをラムダで作って登録します。単に、fitelr()メソッドを再度呼び出して登録してやれば良さそうです。

@Bean
    public RouterFunction router() {
        return RouterFunctions.route(GET("/hello"), helloResource::greeting)
                .filter(new RequestLoggingFilter())
                .filter((r, n)->{
                    logger.info("filtered 1");
                    return n.handle(r);
                });

    }

リクエストを送ると、ログには以下のように出力されました。

2020-10-17 11:43:20.276  INFO 20430 --- [or-http-epoll-2] d.h.r.RouterFunctionFilterApplication    : filtered 1
2020-10-17 11:43:20.277  INFO 20430 --- [or-http-epoll-2] d.h.r.RequestLoggingFilter               : HTTP GET /hello, {name=[henohenomoheji]}

フィルターは後から登録されたものが先に実行されます。
まだ、ちゃんと調べてませんが、フィルターやハンドラーはRouterFunctionのコンポジットパターンとして扱われるようで、基本的には後から登録されたものが先に実行されるような構造になっているみたいです。