Spring Boot 5 WebClient Сначала проверьте HTTPStatus перед проверкой заголовка ответа HTTP

Я пытаюсь подтвердить значение заголовка ответа HTTP с помощью Spring 5 WebClient, но только если веб-вызов отвечает кодом состояния HTTP 200. В этом случае использования, если аутентификация не удалась, вызов API возвращается с HTTP 401 без заголовка ответа. У меня есть следующий код, который функционально работает, но он дважды выполняет веб-вызов (потому что я дважды блокирую). Если не считать блокировки только заголовка HTTP-ответа и установки try / catch для NPE, когда заголовок отсутствует, есть ли какой-нибудь «более чистый» способ сделать это?

import java.net.URI;
import java.time.Duration;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.ExchangeFunctions;


import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@SpringBootApplication
public class ContentCheckerApplication {

private static final Logger LOGGER = LoggerFactory.getLogger(ContentCheckerApplication.class);

private ExchangeFunction exchange = ExchangeFunctions.create(new ReactorClientHttpConnector());         

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(ContentCheckerApplication.class);
    // prevent SpringBoot from starting a web server
    app.setWebApplicationType(WebApplicationType.NONE);
    app.run(args);
}

@Bean
public CommandLineRunner myCommandLineRunner() {

    return args -> {
              // Our reactive code will be declared here
        LinkedMultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();

        formData.add("username", args[2]);
        formData.add("password", args[3]);

        ClientRequest request = ClientRequest.method(HttpMethod.POST, new URI(args[0]+"/api/token"))
                .body(BodyInserters.fromFormData(formData)).build();

        Mono<ClientResponse> mresponse = exchange.exchange(request);
        Mono<String> mnewToken = mresponse.map(response -> response.headers().asHttpHeaders().getFirst("WSToken"));
        LOGGER.info("Blocking for status code...");
        HttpStatus statusCode = mresponse.block(Duration.ofMillis(1500)).statusCode();
        LOGGER.info("Got status code!");

        if (statusCode.value() == 200) {

            String newToken = mnewToken.block(Duration.ofMillis(1500));
            LOGGER.info("Auth token is: " + newToken);

        } else {
            LOGGER.info("Unable to authenticate successfully! Status code: "+statusCode.value());
        }
       };
    }
}

person BJ Weschke    schedule 27.11.2017    source источник
comment
Вы не используете WebClient, вы сами моделируете WebClient. Просто используйте WebClient, связывайте правильные вызовы и не блокируйте.   -  person M. Deinum    schedule 27.11.2017
comment
Но зачем вы составляете карту, если вы хотите сделать это только в том случае, если статус в порядке ... Карта после этого. Не выполняйте потенциально бесполезных операций.   -  person M. Deinum    schedule 27.11.2017
comment
Это работает, но мне все еще нужно блокировать, я думаю, чтобы обеспечить тайм-аут 1500 мс. Есть ли что-то, что позволяет мне установить тайм-аут в веб-клиенте, чтобы я мог подписаться на Mono ‹String› вместо блокировки?   -  person BJ Weschke    schedule 27.11.2017
comment
Что нужно для принудительного тайм-аута?   -  person M. Deinum    schedule 28.11.2017
comment
Это не так важно для этого конкретного запроса, но в этом приложении есть сотни других вызовов API (если у меня есть токен), которые мне нужно быстро пройти. Когда я делал это ранее с помощью Spring AsyncRestTemplate, если бы я оставил длительный тайм-аут и сама служба API начала бы тайм-аут, я мог бы вызвать исключение OOM со всеми невыполненными вызовами, ожидающими тайм-аута.   -  person BJ Weschke    schedule 28.11.2017
comment
Опять же, зачем вам самому заставлять это делать? Обычно вы устанавливаете тайм-аут сокета (как для подключения, так и для чтения), и теперь вы делаете то же самое в своем реактивном приложении (в вашем ответе у вас установлены тайм-ауты для этих вещей).   -  person M. Deinum    schedule 28.11.2017
comment
Я не. Поправка, которую я внес в ответ, удовлетворяет требованию.   -  person BJ Weschke    schedule 29.11.2017


Ответы (1)


Благодаря комментариям @M. Deinum, чтобы направлять меня, у меня есть следующий код, который теперь работает.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.ExchangeFunctions;
import org.springframework.web.reactive.function.client.WebClient;

import reactor.core.publisher.Mono;

@SpringBootApplication
public class ContentCheckerApplication {

private static final Logger LOGGER = LoggerFactory.getLogger(ContentCheckerApplication.class);

private ExchangeFunction exchange = ExchangeFunctions.create(new ReactorClientHttpConnector());         

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(ContentCheckerApplication.class);
    // prevent SpringBoot from starting a web server
    app.setWebApplicationType(WebApplicationType.NONE);
    app.run(args);
}

@Bean
public CommandLineRunner myCommandLineRunner() {

    return args -> {
              // Change some Netty defaults
        ReactorClientHttpConnector connector = new ReactorClientHttpConnector(
                  options -> options.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
                                    .compression(true)
                                    .afterNettyContextInit(ctx -> {
                                        ctx.addHandlerLast(new ReadTimeoutHandler(1500, TimeUnit.MILLISECONDS));
                                    }));


        LinkedMultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();

        formData.add("username", args[2]);
        formData.add("password", args[3]);

        WebClient webClient = WebClient.builder().clientConnector(connector).build();

            Mono<String> tokenResult = webClient.post()
                    .uri( args[0] + "/api/token" )
                    .body( BodyInserters.fromFormData(formData))
                    .exchange()
                    .onErrorMap(ContentCheckerApplication::handleAuthTokenError)
                    .map(response -> {

                            if (HttpStatus.OK.equals(response.statusCode())) {
                                return response.headers().asHttpHeaders().getFirst("WSToken");
                            } else {
                                return "";
                            }

                    });

            LOGGER.info("Subscribing for the result and then going to sleep");
            tokenResult.subscribe(ContentCheckerApplication::handleAuthTokenResponse);

        Thread.sleep(3600000);
       };
    }

private static Throwable handleAuthTokenError(Throwable e) {
    LOGGER.error("Exception caught trying to process authentication token. ",e);
    ContentCheckerApplication.handleAuthTokenResponse("");      
    return null;        
}

private static void handleAuthTokenResponse(String newToken) {

    LOGGER.info("Got status code!");

    if (!newToken.isEmpty()) {

        LOGGER.info("Auth token is: " + newToken);

    } else {
        LOGGER.info("Unable to authenticate successfully!");
    }

    System.exit(0);
}
}
person BJ Weschke    schedule 27.11.2017
comment
Вы не должны использовать Thread.sleep в реактивном приложении ... Реактивные приложения обычно используют цикл событий и небольшое количество потоков, в конечном итоге (без каламбура) вы заблокируете свой цикл событий, остановив всю вашу реактивную систему. - person M. Deinum; 28.11.2017