Help us understand the problem. What is going on with this article?

WebFluxのフィルタを実装してみる

引き続きWebFluxの話です。
前回までで主にController〜Service層でのリアクティブプログラミングの実装を紹介しました。

今回はリクエストの前後の共通処理として利用するフィルタの実装例を紹介します。

FilterではなくWebFliterを実装する

Spring MVCでは javax.servlet.Filter を実装しますが、WebFluxでは org.springframework.web.server.WebFilter を実装します。

以下に、リクエスト処理の前後でログ出力するだけの、簡単なフィルタをそれぞれ実装してみました。
違いをみてみましょう。

Spring MVCのFilter実装例

リクエスト処理の前後にログを出力するフィルタ(Filter)
@Slf4j
public class TimestampFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest)request;
        // before
        doBeforeRequest(req);
        try {
            // do request
            chain.doFilter(request, response);
            // after (success)
            doAfterRequest(req);
        } catch (Throwable throwable) {
            // after (with error)
            doAfterRequestWithError(req, throwable);
            throw throwable;
        }
    }

    private void doBeforeRequest(HttpServletRequest request) {
        String uri = request.getRequestURI();
        long start = System.currentTimeMillis();
        // start log(ex: /hoge [IN]: 1582989973940)
        log.info(String.format("%s [IN]: %d", uri, start));
    }

    private void doAfterRequest(HttpServletRequest request) {
        String uri = request.getRequestURI();
        long end = System.currentTimeMillis();
        // end log(ex: /hoge [OUT]: 1582989974053)
        log.info(String.format("%s [OUT]: %d", uri, end));
    }

    private void doAfterRequestWithError(HttpServletRequest request, Throwable throwable) {
        String uri = request.getRequestURI();
        long end = System.currentTimeMillis();
        // end with error log(ex: /hoge [OUT]: 1582989974053 with Error(java.lang.RuntimeException))
        log.info(String.format("%s [OUT]: %d with Error(%s)", uri, end, throwable.toString()));
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}

これと同等の処理を WebFilter で実装したのが以下です。

Spring WebFluxのWebFilter実装例

リクエスト処理の前後にログを出力するフィルタ(WebFilter)
@Slf4j
@Component
public class TimestampFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(exchange).transformDeferred(call -> doFilter(exchange, call));
    }

    private Publisher<Void> doFilter(ServerWebExchange exchange, Mono<Void> call) {
        // before
        return Mono.fromRunnable(() -> doBeforeRequest(exchange))
                // do request
                .then(call)
                // after (success)
                .doOnSuccess((done) -> doAfterRequest(exchange))
                // after (with error)
                .doOnError((throwable -> doAfterRequestWithError(exchange, throwable)));
    }

    private void doBeforeRequest(ServerWebExchange exchange) {
        String uri = exchange.getRequest().getURI().toString();
        long start = System.currentTimeMillis();
        // start log(ex: /hoge [IN]: 1582989973940)
        log.info(String.format("%s [IN]: %d", uri, start));
    }

    private void doAfterRequest(ServerWebExchange exchange) {
        String uri = exchange.getRequest().getURI().toString();
        long end = System.currentTimeMillis();
        // end log(ex: /hoge [OUT]: 1582989974053)
        log.info(String.format("%s [OUT]: %d", uri, end));
    }

    private void doAfterRequestWithError(ServerWebExchange exchange, Throwable throwable) {
        String uri = exchange.getRequest().getURI().toString();
        long end = System.currentTimeMillis();
        // end with error log(ex: /hoge [OUT]: 1582989974053 with Error(java.lang.RuntimeException))
        log.info(String.format("%s [OUT]: %d with Error(%s)", uri, end, throwable.toString()));
    }
}

(Mono#compose(Function)は非推奨となったため、Mono#transformDeferred(Function)に修正しました。2020/04/01)

Mono.fromRunnable(~) は、戻り値の無い処理(void)をリアクティブに処理し、その後続を then(~) に記述します。
例では .then( call ) を指定し、次のフィルタまたはコントローラに処理を移譲しています。

さらに後続を doOnSuccess(~)doOnError(~) で繋げることで、事後処理を実装しています。

最後に、もう少し実践的なフィルタの実装サンプルを見てみましょう。

カスタム認証フィルタの実装例

以下は、保護されたリソースへのアクセス時にユーザ認証済みであるかをチェックする、いわゆる認証チェックフィルタの実装例です。

カスタム認証チェックフィルタ
@Slf4j
@Component
@Order(1)
public class AuthFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(exchange).transformDeferred(call -> doFilter(exchange, call));
    }

    private Publisher<Void> doFilter(ServerWebExchange exchange, Mono<Void> call) {
        return exchange.getSession().flatMap(webSession -> {
            Map<String, Object> attributes = webSession.getAttributes();
            // セッションからユーザ情報を取得
            Optional<User> optionalUser =
                    Optional.ofNullable((User)attributes.get(User.class.getName()));
            return optionalUser
                    // ユーザがある場合はリクエストを処理
                    .map(user -> call)
                    // ユーザが無い場合は未認証エラーをスロー
                    .orElseThrow(UnauthorizedException::new);
        });
    }
}

最初のセッション情報の取得 exchange.getSession()Mono<WebSession> を返すため、後続の書き方は以前紹介した 複数の順次APIコール が参考になるかと思います。

また、この例では未認証時はカスタムエラー(UnauthorizedException)をスローするので、共通のExceptionHandlerで 401:Unauthrized レスポンスに変換してあげる想定です。

もし、フィルタ内でレスポンスまで生成したい場合は以下のように実装できます。

WebFilter内でレスポンスまで生成
    ...省略...

    private Publisher<Void> doFilter(ServerWebExchange exchange, Mono<Void> call) {
        return exchange.getSession().flatMap(webSession -> {
            Map<String, Object> attributes = webSession.getAttributes();
            // セッションからユーザ情報を取得
            Optional<User> optionalUser =
                    Optional.ofNullable((User)attributes.get(User.class.getName()));
            return optionalUser
                    // ユーザがある場合はリクエストを処理
                    .map(user -> call) // do request
                    // ユーザが無い場合は未認証エラーレスポンスを生成
                    .orElse(writeUnauthorizedResponse(exchange));
        });
    }

    private Mono<Void> writeUnauthorizedResponse(ServerWebExchange exchange) {
        // 401:UnauthorizedエラーとしてJSONレスポンスを生成
        ServerHttpResponse response = exchange.getResponse();
        String body = "{\"message\":\"ログイン認証が必要です。\"}";
        return writeResponse(response, HttpStatus.UNAUTHORIZED, body);
    }

    private Mono<Void> writeResponse(ServerHttpResponse response, HttpStatus status, String jsonBody) {
        response.setStatusCode(status);
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
        DataBufferFactory dbf = response.bufferFactory();
        return response.writeWith(Mono.just(dbf.wrap(jsonBody.getBytes())));
    }

今回のまとめ

WebFluxでフィルタは

  • WebFilter のサブクラスとして実装する
  • 中身はやはりリアクティブに処理を記述する (前々回の記事を参考)

となります。

kilvis
小型ガジェット好きのJavaエンジニア。
codeberry-inc
株式会社コードベリーの技術ブログです。
https://codeberry.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away