Long Polling in Spring MVC – 在Spring MVC中进行长时间的轮询

最后修改: 2021年 4月 22日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Long polling is a method that server applications use to hold a client connection until information becomes available. This is often used when a server must call a downstream service to get information and await a result.

长期轮询是服务器应用程序用来保持客户端连接直到信息可用的一种方法。当服务器必须调用下游服务以获得信息并等待结果时,经常使用这种方法。

In this tutorial, we’ll explore the concept of long polling in Spring MVC by using DeferredResult. We’ll start by looking at a basic implementation using DeferredResult and then discuss how we can handle errors and timeouts. Finally, we’ll look at how all this can be tested.

在本教程中,我们将通过使用DeferredResult来探索Spring MVC中的长轮询概念。我们将首先查看g在一个基本的实现 使用DeferredResult,然后讨论我们如何处理错误和超时。最后,我们将看看如何对所有这些进行测试。

2. Long Polling Using DeferredResult

2.使用DeferredResult的长时间轮询

We can use DeferredResult in Spring MVC as a way to handle inbound HTTP requests asynchronously. It allows the HTTP worker thread to be freed up to handle other incoming requests and offloads the work to another worker thread. As such, it helps with service availability for requests that require long computations or arbitrary wait times.

我们可以在Spring MVC中使用DeferredResult 作为异步处理入站HTTP请求的方式。它允许HTTP工作线程被释放出来以处理其他入站请求,并将工作卸载到其他工作线程。因此,它有助于提高需要长时间计算或任意等待时间的请求的服务可用性。

Our previous article on Spring’s DeferredResult class covers its capabilities and use cases in greater depth.

我们之前关于Spring的DeferredResult类的文章更深入地介绍了其功能和用例。

2.1. Publisher

2.1.出版商

Let’s start our long polling example by creating a publishing application that uses DeferredResult. 

让我们通过创建一个使用DeferredResult的发布应用程序,来开始我们的长轮询实例。

Initially, let’s define a Spring @RestController that makes use of DeferredResult but does not offload its work to another worker thread:

首先,让我们定义一个Spring @RestController,它利用了DeferredResult,但没有将其工作卸载到另一个工作线程:

@RestController
@RequestMapping("/api")
public class BakeryController { 
    @GetMapping("/bake/{bakedGood}")
    public DeferredResult<String> publisher(@PathVariable String bakedGood, @RequestParam Integer bakeTime) {
        DeferredResult<String> output = new DeferredResult<>();
        try {
            Thread.sleep(bakeTime);
            output.setResult(format("Bake for %s complete and order dispatched. Enjoy!", bakedGood));
        } catch (Exception e) {
            // ...
        }
        return output;
    }
}

This controller works synchronously in the same way that a regular blocking controller works. As such, our HTTP thread is completely blocked until bakeTime has passed. This is not ideal if our service has a lot of inbound traffic.

这个控制器的工作方式与普通的阻塞式控制器的工作方式一样,都是同步的。因此,我们的HTTP线程被完全阻塞,直到bakeTime过去。如果我们的服务有大量的入站流量,这并不理想。

Let’s now set the output asynchronously by offloading the work to a worker thread:

现在让我们通过将工作卸载到一个工作线程来异步设置输出:

private ExecutorService bakers = Executors.newFixedThreadPool(5);

@GetMapping("/bake/{bakedGood}")
public DeferredResult<String> publisher(@PathVariable String bakedGood, @RequestParam Integer bakeTime) {
    DeferredResult<String> output = new DeferredResult<>();
    bakers.execute(() -> {
        try {
            Thread.sleep(bakeTime);
            output.setResult(format("Bake for %s complete and order dispatched. Enjoy!", bakedGood));
        } catch (Exception e) {
            // ...
        }
    });
    return output;
}

In this example, we’re now able to free up the HTTP worker thread to handle other requests. A worker thread from our bakers pool is doing the work and will set the result upon completion. When the worker calls setResult, it will allow the container thread to respond to the calling client.

在这个例子中,我们现在可以腾出HTTP工作线程来处理其他请求。我们的bakerspool中的一个工作线程正在进行这项工作,并将在完成后设置结果。当工作者调用setResult时,它将允许容器线程响应调用的客户端。

Our code is now a good candidate for long polling and will allow our service to be more available to inbound HTTP requests than with a traditional blocking controller. However, we also need to take care of edge cases such as error handling and timeout handling.

我们的代码现在是一个很好的长轮询候选者,与传统的阻塞式控制器相比,它将允许我们的服务对入站的HTTP请求有更多的可用性。然而,我们也需要照顾到边缘情况,如错误处理和超时处理。

To handle checked errors thrown by our worker, we’ll use the setErrorResult method provided by DeferredResult:

为了处理工人抛出的检查错误,我们将使用DeferredResult提供的setErrorResult方法:

bakers.execute(() -> {
    try {
        Thread.sleep(bakeTime);
        output.setResult(format("Bake for %s complete and order dispatched. Enjoy!", bakedGood));
     } catch (Exception e) {
        output.setErrorResult("Something went wrong with your order!");
     }
});

The worker thread is now able to gracefully handle any exception thrown.

工作线程现在能够优雅地处理任何抛出的异常。

Since long polling is often implemented to handle responses from downstream systems both asynchronously and synchronously, we should add a mechanism to enforce a timeout in the case that we never receive a response from the downstream system. The DeferredResult API provides a mechanism for doing this. First, we pass in a timeout parameter in the constructor of our DeferredResult object:

由于长时间的轮询通常是为了处理来自下游系统的异步和同步响应,我们应该添加一种机制,以便在我们从未收到来自下游系统的响应的情况下强制执行超时。DeferredResult API提供了一种机制来实现这一点。首先,我们在DeferredResult对象的构造函数中传入一个超时参数。

DeferredResult<String> output = new DeferredResult<>(5000L);

Next, let’s implement the timeout scenario. For this, we’ll use onTimeout:

接下来,让我们来实现超时的情况。为此,我们将使用onTimeout:

output.onTimeout(() -> output.setErrorResult("the bakery is not responding in allowed time"));

This takes in a Runnable as input — it’s invoked by the container thread when the timeout threshold is reached. If the timeout is reached, then we handle this as an error and use setErrorResult accordingly.

这需要一个Runnable作为输入–当达到超时阈值时,它将被容器线程调用。如果达到超时,那么我们将其作为错误处理,并相应地使用setErrorResult

2.2. Subscriber

2.2.订阅者

Now that we have our publishing application set up, let’s write a subscribing client application.

现在我们已经建立了我们的发布应用程序,让我们来写一个订阅的客户端应用程序。

Writing a service that calls this long polling API is fairly straightforward, as it’s essentially the same as writing a client for standard blocking REST calls. The only real difference is that we want to ensure we have a timeout mechanism in place due to the wait time of long polling. In Spring MVC, we can use RestTemplate or WebClient to achieve this, as both have built-in timeout handling.

编写一个调用这个长轮询API的服务是相当简单的,因为它基本上与编写一个标准阻塞REST调用的客户端相同。唯一真正的区别是,由于长轮询的等待时间,我们要确保我们有一个超时机制。在Spring MVC中,我们可以使用RestTemplateWebClient来实现这一点,因为两者都有内置的超时处理。

First, let’s start with an example using RestTemplate. Let’s create an instance of RestTemplate using RestTemplateBuilder so that we can set the timeout duration:

首先,让我们从一个使用RestTemplate的例子开始。让我们使用RestTemplateBuilder创建一个RestTemplate的实例,这样我们就可以设置超时时间。

public String callBakeWithRestTemplate(RestTemplateBuilder restTemplateBuilder) {
    RestTemplate restTemplate = restTemplateBuilder
      .setConnectTimeout(Duration.ofSeconds(10))
      .setReadTimeout(Duration.ofSeconds(10))
      .build();

    try {
        return restTemplate.getForObject("/api/bake/cookie?bakeTime=1000", String.class);
    } catch (ResourceAccessException e) {
        // handle timeout
    }
}

In this code, by catching the ResourceAccessException from our long polling call, we’re able to handle the error upon timeout.

在这段代码中,通过从我们的长轮询调用中捕获ResourceAccessException,我们能够在超时时处理这个错误。

Next, let’s create an example using WebClient to achieve the same result:

接下来,让我们使用WebClient创建一个例子来实现同样的结果。

public String callBakeWithWebClient() {
    WebClient webClient = WebClient.create();
    try {
        return webClient.get()
          .uri("/api/bake/cookie?bakeTime=1000")
          .retrieve()
          .bodyToFlux(String.class)
          .timeout(Duration.ofSeconds(10))
          .blockFirst();
    } catch (ReadTimeoutException e) {
        // handle timeout
    }
}

Our previous article on setting Spring REST timeouts covers this topic in greater depth.

我们之前关于设置Spring REST超时的文章更深入地介绍了这个话题。

3. Testing Long Polling

3.测试长距离投票

Now that we have our application up and running, let’s discuss how we can test it. We can start by using MockMvc to test calls to our controller class:

现在我们的应用程序已经启动并运行,让我们来讨论如何测试它。我们可以首先使用MockMvc来测试对我们控制器类的调用:

MvcResult asyncListener = mockMvc
  .perform(MockMvcRequestBuilders.get("/api/bake/cookie?bakeTime=1000"))
  .andExpect(request().asyncStarted())
  .andReturn();

Here, we’re calling our DeferredResult endpoint and asserting that the request has started an asynchronous call. From here, the test will await the completion of the asynchronous result, meaning that we do not need to add any waiting logic in our test.

在这里,我们正在调用我们的DeferredResult端点,并断言该请求已经开始一个异步调用。从这里开始,测试将等待异步结果的完成,这意味着我们不需要在测试中添加任何等待逻辑。

Next, we want to assert when the asynchronous call has returned and that it matches the value that we’re expecting:

接下来,我们要断言异步调用何时返回,并且与我们所期望的值相匹配:

String response = mockMvc
  .perform(asyncDispatch(asyncListener))
  .andReturn()
  .getResponse()
  .getContentAsString();

assertThat(response)
  .isEqualTo("Bake for cookie complete and order dispatched. Enjoy!");

By using asyncDispatch(), we can get the response of the asynchronous call and assert its value.

通过使用asyncDispatch(),我们可以获得异步调用的响应并断言其值。

To test the timeout mechanism of our DeferredResult, we need to alter the test code slightly by adding a timeout enabler between the asyncListener and the response calls:

为了测试我们的DeferredResult的超时机制,我们需要稍微改变测试代码,在asyncListenerresponse调用之间添加一个超时启用器。

((MockAsyncContext) asyncListener
  .getRequest()
  .getAsyncContext())
  .getListeners()
  .get(0)
  .onTimeout(null);

This code might look strange, but there’s a specific reason we call onTimeout in this way. We do this to let the AsyncListener know that an operation has timed out. This will ensure that the Runnable class that we’ve implemented for our onTimeout method in our controller is called correctly.

这段代码可能看起来很奇怪,但我们以这种方式调用onTimeout是有特殊原因的。我们这样做是为了让AsyncListener知道一个操作已经超时了。这将确保我们为控制器中的onTimeout方法实现的Runnable类被正确调用。

4. Conclusion

4.结论

In this article, we covered how to use DeferredResult in the context of long polling. We also discussed how we can write subscribing clients for long polling, and how it can be tested. The source code is available over on GitHub.

在这篇文章中,我们介绍了如何在长轮询的背景下使用DeferredResult。我们还讨论了如何编写用于长轮询的订阅客户端,以及如何测试它。源代码可在GitHub上获得over。