Springboot:如何使用WebClient代替RestTemplate来执行非阻塞和异步调用

时间:2019-08-05 09:29:15

标签: java spring spring-boot spring-webflux

我有一个使用Springboot Resttemplate的springboot项目。我们已经从1.5.3迁移到springboot 2.0.1,我们正在努力使 其余的调用通过使用WebClient异步进行。我们曾经使用Resttemplate处理接收到的字符串,如下所示。但是WebClient仅返回 单声道或助焊剂中的数据。我怎样才能以String形式获取数据。已经尝试过block()方法,但是它执行异步调用。

@Retryable(maxAttempts = 4, value = java.net.ConnectException.class,
           backoff = @Backoff(delay = 3000, multiplier = 2))
public Mono<String> getResponse(String url) {
    return webClient.get().uri(urlForCurrent).accept(APPLICATION_JSON)
                    .retrieve()
                    .bodyToMono(String.class);
}

使用RestTemplate呈现数据流

  1. 控制器收到客户呼叫
  2. 提供者获取字符串格式的数据
  3. 提供者处理字符串
  4. 数据已提供给控制器

Controller.java

@RequestMapping(value = traffic/, method = RequestMethod.GET,
                produces = MediaType.APPLICATION_JSON_VALUE)
public String getTraffic(@RequestParam("place") String location) throws InterruptedException, ExecutionException {
    String trafficJSON = Provider.getTrafficJSON(location)
    return trafficJSON;
}

Provider.java

public String getTrafficJSON(String location) {
    String url = ----;

    ResponseEntity<String> response = dataFetcher.getResponse(url);

    /// RESPONSEBODY IS READ AS STRING AND IT NEEDS TO BE PROCESSED
    if (null != response {
        return parser.transformJSON(response.getBody(), params);
    }

    return null;
}

DataFetcher.java

@Retryable(maxAttempts = 4,
           value = java.net.ConnectException.class,
           backoff = @Backoff(delay = 3000, multiplier = 2))
public ResponseEntity<String> getResponse(String url) {
    /* ----------------------- */
    return getRestTemplate().getForEntity(urlForCurrent, String.class);
}

4 个答案:

答案 0 :(得分:4)

由于存在很多误解,所以在这里我要澄清一些事情。

Spring正式声明,它们将在未来弃用RestTemplate,因此,如果可以的话,请使用WebClient

RestTemplate API

中所述
  

注意::自5.0以来,无阻塞,反应式org.springframework.web.reactive.client.WebClient提供了RestTemplate的现代替代方案,并有效支持同步和异步以及流方案。 RestTemplate将在以后的版本中弃用,并且以后不会添加任何主要的新功能。有关更多详细信息和示例代码,请参见Spring Framework参考文档的WebClient部分。

非反应性应用

如果您的应用程序是非反应性应用程序(不向调用方客户返回磁通量或单声道),则需要使用block()(如果需要该值)。您当然可以在应用程序内部使用MonoFlux,但是最后您必须调用block()以获得返回给调用方客户端所需的具体值。

非反应式应用程序使用tomcat作为基础服务器实现,这是一个基于servlet的服务器,它将为每个请求分配1个线程,因此您将无法获得反应式应用程序带来的性能提升。

最新版本的Tomcat支持最新的servlet规范+3.0,该规范反过来又支持非阻塞I / O ,但是对于WebFlux应用程序的这种支持方式在Spring之前尚不清楚。文档。

反应性应用

另一方面,如果您有反应性应用程序,则在任何情况下都不应在应用程序中调用block()subscribe()。阻塞正是​​它所说的,它将阻塞一个线程并阻塞该线程的执行,直到可以继续执行为止,这在响应式世界中是很糟糕的。

这里的基础服务器实现是netty服务器,它是非servlet,基于事件的服务器,将为每个请求分配一个线程,该服务器本身是线程不可知的,并且任何线程可用将在任何请求期间随时处理任何事情。

我怎么知道我拥有什么应用程序?

Spring指出,如果您在类路径上同时具有spring-webspring-webflux,则应用程序将偏爱spring-web,并且默认情况下会使用基础的tomcat服务器启动非反应性应用程序。

如果需要,可以将此行为手动设置为弹簧状态。

  

在应用程序中同时添加spring-boot-starter-webspring-boot-starter-webflux模块会导致Spring Boot自动配置Spring MVC,而不是WebFlux。之所以选择这种行为,是因为许多Spring开发人员将spring-boot-starter-webflux添加到他们的Spring MVC应用程序中以使用反应式WebClient。您仍然可以通过将所选的应用程序类型设置为SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)来强制执行选择。

The “Spring WebFlux Framework”

那么如何实现WebClient

@Retryable(maxAttempts = 4,
       value = java.net.ConnectException.class,
       backoff = @Backoff(delay = 3000, multiplier = 2))
public ResponseEntity<String> getResponse(String url) {
    return webClient.get()
            .uri(url)
            .exchange()
            .flatMap(response -> response.toEntity(String.class))
            .block();
}

这是最简单,侵入性最小的实现。当然,您可能需要在@Bean中构建适当的Web客户端,并将其自动连接到其类中。

答案 1 :(得分:1)

第一步是使用baseUrl构建WebClient对象;

WebClient webClient = WebClient.builder()
    .baseUrl("http://localhost:8080/api") //baseUrl
    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .build();

然后选择方法,并将路径与请求变量或主体有效载荷一起附加。

ResponseSpec responseSpec = webClient
    .get()
    .uri(uriBuilder -> uriBuilder.path("/findById") //additional path
        .queryParam("id", id).build())
    .retrieve()
    .onStatus(HttpStatus::is4xxClientError, response -> Mono.error(new CustomRuntimeException("Error")));

block()的{​​{1}}功能等待响应。如果您希望将响应作为字符串,则可以使用Google的gson库将其转换。

bodyToMono

如果您不想知道api调用的状态,则可以执行以下操作。

Object response = responseSpec.bodyToMono(Object.class).block();
Gson gson = new Gson();
String str = gson.toJson(response);

答案 2 :(得分:1)

首先要了解的是,如果您需要致电.block(),那么您最好还是坚持使用RestTemplate,使用WebClient不会给您带来任何好处。

如果您想从使用WebClient中受益,就需要开始以被动的方式进行思考。反应性过程实际上只是一系列步骤,每个步骤的输入就是该步骤之前的输出。收到请求时,您的代码将创建步骤序列,并立即返回释放http线程。然后,当上一步的输入可用时,该框架将使用工作线程池执行每个步骤。

这样做的好处是,以巨大的代价来接受竞争性请求,而不必重新考虑编写代码的方式,所付出的代价很小。您的应用程序只需要一个非常小的http线程池和另一个非常小的工作线程池。

当您的控制器方法返回MonoFlux时,您已正确使用此方法,因此无需调用block()

最简单的形式是这样的

@GetMapping(value = "endpoint", produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
@ResponseStatus(OK)
public Mono<String> controllerMethod() {

    final UriComponentsBuilder builder =
            UriComponentsBuilder.fromHttpUrl("http://base.url/" + "endpoint")
                    .queryParam("param1", "value");

    return webClient
            .get()
            .uri(builder.build().encode().toUri())
            .accept(APPLICATION_JSON_UTF8)
            .retrieve()
            .bodyToMono(String.class)
            .retry(4)
            .doOnError(e -> LOG.error("Boom!", e))
            .map(s -> {

                // This is your transformation step. 
                // Map is synchronous so will run in the thread that processed the response. 
                // Alternatively use flatMap (asynchronous) if the step will be long running. 
                // For example, if it needs to make a call out to the database to do the transformation.

                return s.toLowerCase();
            });
}

转向反应式思维是一个很大的范式转变,但值得付出努力。坚持下去,一旦您可以全神贯注地在整个应用程序中完全没有阻塞代码,这实际上并不那么困难。建立步骤并返回它们。然后,让框架管理步骤的执行。

如果其中任何一个不清楚,很乐意提供更多指导。

记住玩得开心:)

答案 3 :(得分:0)

据我所知,您的 Spring Boot Web 应用程序中需要非阻塞/异步和 Web servlet/同步 api 调用。我有完全相同的问题。答案很简单。您需要同时拥有 spring-boot-starter-web 和 spring-boot-starter-webflux 依赖项。 然后,当您要进行异步/非阻塞调用时,请确保按如下方式调用 toFuture() 方法。

configWebClient.post().uri("/alarm/check")
            .body(BodyInserters.fromFormData(formData))
            .exchangeToMono(res -> {
                if (res.statusCode().equals(HttpStatus.OK)) {
                    return res.bodyToMono(Boolean.class);
                } else {
                    return res.createException().flatMap(Mono::error);
                }
            }).toFuture();