Как заставить реактивный @WebFilter правильно работать с @RequestBody? - PullRequest
3 голосов
/ 01 ноября 2019

Я пытаюсь создать реактивный @WebFilter, который выполняет вещи до и после фактического обмена сервером (т. Е. Код контроллера, обрабатывающий запрос):

public static class Foobar implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return Mono.empty()
                .doOnEach(s -> /* BEFORE */))
                .then(chain.filter(exchange) /* CONTROLLER */)
                .and(Mono.empty().doOnEach(s -> /* AFTER */))
                .and(Mono.empty().doFinally(s -> /* FINALLY */));
    }
}

Все работает, как и ожидалось, для простых GET запросов, которые возвращают Mono:

@RestController
@RequestMapping
public static class Foo {

    @GetMapping
    @PostMapping(value = "foo")
    public Mono<String> foo(ServerWebExchange exchange) {
        return Mono.just("FOOBAR").map(e -> "OK");
    }
}

Но что-то действительно неожиданное происходит, когда контроллер получает параметр, аннотированный как @RequestBody. Скажем, например, запрос POST, который получает Mono<String> от клиента:

@RestController
@RequestMapping
public static class Bar {

    @PostMapping(value = "bar")
    public Mono<String> bar(ServerWebExchange exchange, @RequestBody Mono<String> text) {
        return text.map(s -> "OK");
    }
}

В этом случае все шаги в моем фильтре выполняются до того, как контроллер получит завершить запрос. Это означает, что веб-обмен выполняется независимо от фильтра, и поэтому я не могу ничего сделать сразу после отправки ответа клиенту.

Поэтому мне интересно:

  • Это какая-то ошибка в Spring?
  • Я что-то не так делаю?
  • Или это просто ожидаемое поведение?

Я создал небольшой Gistсодержащий тестовый пример, который воспроизводит проблему:


Редактировать после комментария Брайана:

Я все еще думаю, что это может бытьошибка, потому что Mono.then, кажется, не имеет никакого эффекта вообще:

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
            return chain.filter(exchange)
                .doOnSubscribe(s -> logger.info("onSubscribe response committed:" +
                        exchange.getResponse().isCommitted()))
                .then().doOnEach(s -> logger.info("then doOnEach response committed:" +
                        exchange.getResponse().isCommitted()))
                .doFinally(s -> logger.info("doFinally response committed:" +
                        exchange.getResponse().isCommitted()));
        }

Кроме того, если я помещаю материал в doOnEach, он тоже не выполняется:

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
            return chain.filter(exchange)
                    .doOnSubscribe(s -> logger.info("FILTER-BEFORE-CHAIN/commited=" +
                        response.isCommitted()))
                    .doOnEach(s -> logger.info("FILTER-AFTER-CHAIN/commited=" +
                        response.isCommitted()))
                    .doFinally(s -> logger.info("FILTER-FINALLY/commited=" +
                        response.isCommitted()));
        }

1 Ответ

1 голос
/ 02 ноября 2019

Я не думаю, что это ошибка в Spring (или в Reactor в данном случае), а скорее неправильный выбор операторов для достижения того, что вы пытаетесь сделать.

  • Mono.doOnEach выполняется для каждого сигнала (следующий элемент, завершение, ошибка, отмена);в нашем случае это будет выполняться несколько раз для каждого запроса.
  • Mono.and присоединяет сигналы завершения - поэтому он ожидает выполнения обоих Mono и затем завершается. Но оба Monos не выполняются последовательно, они подписаны одновременно. Mono.just завершается сразу, независимо от того, что происходит с цепочкой фильтров.

В вашем случае вам не нужно иметь что-то более сложное, чем добавление одного оператора при запуске обработки (doOnSubscribe происходит, когда сопоставление выполнено, и мы начинаем выполнять обработчик), и другое, когда мы закончим (doFinally происходит, когда оно выполнено, с завершением, ошибкой или отменой).

    @Component
    public class MyFilter implements WebFilter {

        Logger logger = LoggerFactory.getLogger(getClass());

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
            return chain.filter(exchange)
                .doOnSubscribe(s -> logger.info("onSubscribe response committed:" +
                        exchange.getResponse().isCommitted()))
                .doFinally(s -> logger.info("doFinally response committed:" + 
                        exchange.getResponse().isCommitted()));
        }
    }

Обратите внимание, что этот подход работает до тех пор, пока вы не выполняете операции блокировки в методах DoOnXYZ, поскольку они являются методами побочных эффектов. Это создаст значительные проблемы с производительностью в вашем приложении (и реактор может даже отклонить эту операцию за исключением). Так что ведение журнала, добавление элемента на карту - это хорошо, но публикация чего-либо в очереди событий, например, - нет. В этом случае вместо цепных операций следует использовать Mono.then().

Редактировать

Не думаю, что есть ошибка с Mono.then() - doOnEach, работает только с отправленными сигналамивниз по течению (далее, ошибка, завершена). В этом случае вы должны получить только полный сигнал. Если вы хотите получить контекст во всех случаях, вы можете взглянуть на Mono.deferWithContext.

...