Почему обработчик для конечной точки REST доступен дважды при доступе из WebClient? - PullRequest
0 голосов
/ 08 октября 2019

Это вторая попытка с пересмотренным демонстрационным кодом, который, надеюсь, лучше иллюстрирует проблему. Код был урезан, чтобы удалить все элементы, кроме тех, которые демонстрируют возникшую проблему.

Добавить примечание: Было проведено дополнительное тестирование, и результаты опубликованы как ответ (недостаток, расширяющий этоПочта). Возможно, это «ожидаемое поведение», но я все еще пытаюсь понять «почему».

Код «работает» в том смысле, что возвращает ожидаемую информацию (либо строка, либо список строк). Однако, когда WebClient используется для доступа к конечной точке REST ( localhost: 8080 / test / DemoClient ), которая возвращает Flux, в соответствующий обработчик выполняется два вызова ( DemoMainHandler.getAll ()). Я не вижу, где выполняется второй вызов DemoMainHandler.getAll () , но я обеспокоен потенциальными проблемами с производительностью, если это произойдет в производственной среде.

В предоставленном кодевсе выполняется в одном приложении Spring Webflux, поэтому для кода DemoClient не существует отдельного процесса.

Доступ к конечной точке REST на localhost: 8080 / test / DemoClient /2 , кажется, работает правильно, возвращая Mono Почтальону со значением «Only One». Что еще более важно, DemoMainHandler.getById () вызывается только один раз.

Однако доступ к конечной точке REST в localhost: 8080 / test / DemoClient дает результаты, которые являютсянемного касательно. Значения String, возвращаемые Postman через Flux, выглядят нормально, но

  1. DemoClientHandler.getAll () вызывается при доступе к конечной точке REST
  2. * DemoClientHandler.getAll () вызывает DemoClient.getAll ()
  3. DemoClient.getAll () использует WebClient для доступа к конечной точке REST на localhost:8080 / test / DemoMain
  4. DemoClient.getAll () использует flatMapMany для итерации по возвращенному ClientResponse и извлечения потока изтело ответа
  5. Поток, сгенерированный DemoCLient.getAll () , возвращается в DemoClientHandler.getAll ()
  6. DemoClientHandler.getAll() проверяет поток, определяет, имеет ли он один или несколько элементов, и возвращает поток в ServerResponse начальному клиенту (в данном случае, Postman)
  7. Postmanзатем распаковывает Flux (map? flatMap?) и отображает возвращенные строки ("CallMeOnce")

Я не понимаю, почему DemoClientHandler.getAll () вызывается второй раз, как указано вторым System.out.println () вывод на консоль. Похоже, это связано с использованием Flux в качестве типа возвращаемого значения?

Добавить примечание: При вероятности, что проблема была каким-то образом вызвана конструкцией .exchange (). FlatMapMany () , Я пытался использовать .retrieve (). BodyToFlux () конструкция (см. Прокомментированный код в DemoClient ). Тот же результат (т. Е. DemoMainHandler.getAll () , кажется, вызывается дважды).

Консольный вывод

2019-10-07 08:16:18.953  INFO 9384 --- [           main] c.example.testdupe.TestDupeApplication   : Starting TestDupeApplication on M7730-LFR with PID 9384 (D:\sandbox\TestDupe\build\classes\java\main started by LesR in D:\sandbox\TestDupe)
2019-10-07 08:16:18.953  INFO 9384 --- [           main] c.example.testdupe.TestDupeApplication   : No active profile set, falling back to default profiles: default
2019-10-07 08:16:20.062  INFO 9384 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
2019-10-07 08:16:20.062  INFO 9384 --- [           main] c.example.testdupe.TestDupeApplication   : Started TestDupeApplication in 1.324 seconds (JVM running for 1.871)

***** Invoke localhost:8080/test/DemoClient/{id}
DemoClientHandler.getById( ServerRequest )
DemoClient.getById( 2 )
DemoMainHandler.getById( ServerRequest )

***** Invoke localhost:8080/test/DemoClient
DemoClientHandler.getAll( ServerRequest )
DemoClientHandler.getAll() >> BEFORE invoking demoClient.getAll()
DemoClient.getAll()
DemoClient.getAll() >> RETURN fluxString
DemoClientHandler.getAll() >>  AFTER invoking demoClient.getAll()
DemoMainHandler.getAll( ServerRequest )
DemoMainHandler.getAll( ServerRequest )

Пример кода

@SpringBootApplication
public class TestDupeApplication {

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


@Configuration
public class DemoClientRouter {

    @Bean
    public RouterFunction<ServerResponse> clientRoutes(DemoClientHandler requestHandler) {
        return nest(path("/test"),
                nest(accept(APPLICATION_JSON),
                        RouterFunctions.route(RequestPredicates.GET("/DemoClient"), requestHandler::getAll)
                                    .andRoute(RequestPredicates.GET("/DemoClient/{id}"), requestHandler::getById)));
    }
}


@Component
public class DemoClientHandler {

    @Autowired
    DemoClient demoClient;

    public Mono<ServerResponse> getAll(ServerRequest request) {
        System.out.println("DemoClientHandler.getAll( ServerRequest )");
        System.out.println("DemoClientHandler.getAll() >> BEFORE invoking demoClient.getAll()");
        Flux<String> fluxString = demoClient.getAll();
        System.out.println("DemoClientHandler.getAll() >>  AFTER invoking demoClient.getAll()");

        return fluxString.hasElements().flatMap(hasElement -> {
            return hasElement ? ServerResponse.ok()
                                              .contentType(MediaType.APPLICATION_JSON)
                                              .body(fluxString, String.class)
                              : ServerResponse.noContent().build();
        });
    }

    public Mono<ServerResponse> getById(ServerRequest request) {
        System.out.println("DemoClientHandler.getById( ServerRequest )");
        Mono<String> monoString;

        return demoClient.getById( 2 ).flatMap(stringVal -> ServerResponse.ok()
                                                                          .contentType(MediaType.APPLICATION_JSON)
                                                                          .body(Mono.just(stringVal), String.class))
                                      .switchIfEmpty(ServerResponse.notFound().build());
    }
}


@Component
public class DemoClient {

    private final WebClient client;

    public DemoClient() {
        client = WebClient.create();
    }

    public Flux<String> getAll() {
        System.out.println("DemoClient.getAll()");
        Flux<String> fluxString;

        fluxString = client.get().uri("http://localhost:8080/test/DemoMain")
                                 .accept(MediaType.APPLICATION_JSON)
                                 .exchange()
                                 .flatMapMany(response -> response.bodyToFlux(String.class));

        // fluxString = client.get().uri("http://localhost:8080/test/DemoMain")
        //                          .accept(MediaType.APPLICATION_JSON)
        //                          .retrieve()
        //                          .bodyToFlux(String.class);

        System.out.println("DemoClient.getAll() >> RETURN fluxString");
        return fluxString;
    }

    public Mono<String> getById(int id) {
        System.out.printf("DemoClient.getById( %d )%n", id);
        return client.get().uri("http://localhost:8080/test/DemoMain/" + id)
                           .accept(MediaType.APPLICATION_JSON)
                           .exchange()
                           .flatMap(response -> response.bodyToMono(String.class));
    }
}


@Configuration
public class DemoMainRouter {
    @Bean
    public RouterFunction<ServerResponse> demoPOJORoute(DemoMainHandler requestHandler) {
        return nest(path("/test"),
                nest(accept(APPLICATION_JSON),
                        RouterFunctions.route(RequestPredicates.GET("/DemoMain"), requestHandler::getAll)
                                    .andRoute(RequestPredicates.GET("/DemoMain/{id}"), requestHandler::getById)));
    }
}


@Component
public class DemoMainHandler {

    public Mono<ServerResponse> getAll(ServerRequest request) {
        System.out.println("DemoMainHandler.getAll( ServerRequest )");

        return ServerResponse.ok()
                             .contentType(MediaType.APPLICATION_JSON)
                             .body(Flux.just("Call", "Me", "Once"), String.class);
    }

    public Mono<ServerResponse> getById(ServerRequest request) {
        System.out.println("DemoMainHandler.getById( ServerRequest )");

        return ServerResponse.ok()
                             .contentType(MediaType.APPLICATION_JSON)
                             .body(Mono.just("Only One"), String.class);
    }
}

Этот код добавлен для поддержки последующего обсуждения ...

@Component
public class DemoClient {

    private final WebClient client;

    public DemoClient() {
        client = WebClient.create();
    }

    public Flux<String> getAll() {
        Flux<String> fluxString;

        Mono<ClientResponse> monoCR = client.get().uri("http://localhost:8080/test/DemoMain")
                                                  .accept(MediaType.APPLICATION_JSON)
                                                  .exchange();

        fluxString = monoCR.flatMapMany(clientResponse -> clientResponse.bodyToFlux(String.class));

//        fluxString.subscribe();
//        return fluxString;
        return Flux.just("Foo", "Bar");
    }

1 Ответ

0 голосов
/ 08 октября 2019

Последующее обсуждение. Не совсем ответ, но похоже, что он движется в правильном направлении.

Изменено DemoClient.gatAll () , чтобы "расцепить" операции над потоком / потоком, надеясь получить некоторое понимание,Вот что я сделал / нашел:

  1. Я ввел переменную Mono для хранения результата доступа к WebClient на localhost: 8080 / test / DemoMain
  2. Iнезависимо вызвал monoCR.flatMapMany () для получения возвращаемого потока и назначил поток для fluxString
  3. Я добавил fluxString.subscribe; оператор, просто чтобы иметь возможность подписаться на возвращаемый Flux, ничего не делая с ним
  4. Я ввел return Flux.just ("Foo", "Bar"); оператор, просточтобы было что вернуть, если я решил не возвращать fluxString

Когда я закомментировал fluxString.subscribe () ' и return fluxString; операторов, нет выходных данных из DemoMainHandler.getAll () . Я предполагаю, что это «не удивительно», так как ничто не подписывается на сгенерированный Flux, поэтому DemoMainHandler.getAll () не вызывается, так как нет необходимости в Flux. fluxString.subscribe (); но оставить return fluxString; закомментировано, я вижу один println () вывод из DemoMainHandler.getAll () ,Опять же, я воспринимаю это как «не сюрприз», так как на Flux теперь подписываются, хотя с результатом ничего не сделано. Итак, DemoMainHandler.getAll () вызывается и выводит его println () content.

Наконец, я закомментировал fluxString.subscribe (); и return Flux.just ("Foo", "bar"); операторы и без комментариев * return fluxString; ". Это создает два println () выходов из DemoMainHandler.getAll () , о котором я спрашивал.

Исходя из результатов простой подписки на возвращаемый Flux, я предполагаю, что первая println () вывод из DemoMainHandler.getAll () является результатом неявной подписки от имени Почтальона (т. е. «конечного потребителя»). Но это все еще оставляет меня с вопросом «Почему второе * 1067»* println () вывод из DemoMainHandler.getAll () ? "Действительно ли Reactor вызывает DemoMainHandler.getAll () один раз для подписки и второй раз, когда фактическое содержимоеобработано? Или?

Вроде как такое поведение (т.е. двавызов метода-обработчика) происходит только при возврате Flux (см. пример DemoMainHandler.getById () ).

...