拨开荷叶行,寻梦已然成。仙女莲花里,翩翩白鹭情。
IMG-LOGO
主页 文章列表 Spring MVC中的长轮询

Spring MVC中的长轮询

白鹭 - 2021-11-22 2169 0 2

1.概述

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


在本教程中,我们将使用DeferredResult .我们先来看一个基本的实现 使用DeferredResult ,然后讨论如何处理错误和超时。最后,我们将研究如何测试所有这些。

2.使用DeferredResult

我们可以DeferredResult作为异步处理入站HTTP请求的方法。它允许释放HTTP工作线程来处理其他传入请求,并将工作分流到另一个工作线程。这样,对于需要长时间计算或任意等待时间的请求,它有助于提高服务可用性。


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

2.1。发行人

DeferredResult.的发布应用程序开始我们的长轮询示例。

最初,让我们定义一个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;

}

}

该控制器以与常规阻塞控制器相同的方式同步工作。这样,我们的HTTP线程将被完全阻止,直到通过bakeTime如果我们的服务有很多入站流量,这是不理想的。

现在,通过将工作卸载到工作线程来异步设置输出:

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;

}

在此示例中,我们现在可以释放HTTP工作线程来处理其他请求。 bakers池中的工作线程正在执行工作,并将在完成时设置结果。当工作程序调用setResult ,它将允许容器线程响应调用方客户端。

现在,我们的代码非常适合长时间轮询,并且与传统的阻塞控制器相比,我们的服务可更适合入站HTTP请求使用。但是,我们还需要注意一些边缘情况,例如错误处理和超时处理。

为了处理工作人员抛出的检查错误,我们将使用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!");

}

});

现在,工作线程可以正常处理所有引发的异常。

由于通常采用长轮询来异步和同步地处理来自下游系统的响应,因此在我们从未收到来自下游系统的响应的情况下,我们应该添加一种机制来强制执行超时。 DeferredResult API提供了一种执行此操作的机制。 DeferredResult对象的构造函数中传入一个超时参数:

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

接下來,让我们实现超时方案。为此,我们将使用onTimeout:

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

这以Runnable作为输入-当达到超时阈值时,它由容器线程调用。如果达到超时,则将其作为错误处理,并相应地使用setErrorResult .

2.2。订户

现在我们已经设置了发布应用程序,让我们编写一个订阅客户端应用程序。


编写调用此长轮询API的服务非常简单,因为它与编写用于标准阻止REST调用的客户端基本相同。唯一的实际区别是,由于长轮询的等待时间,我们要确保有适当的超时机制。在Spring MVC中,我们都可以使用RestTemplate或WebClient来实现此目的,因为它们都具有内置的超时处理功能。


首先,让我们从使用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

}

}

在这段代码中,通过ResourceAccessException ,我们能够在超时时处理错误。


接下来,让我们使用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

}

}

我们之前的有关设置Spring REST超时的文章更深入地介绍了该主题。

3.测试长轮询

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

MvcResult asyncListener = mockMvc

.perform(MockMvcRequestBuilders.get("/api/bake/cookie?bakeTime=1000"))

.andExpect(request().asyncStarted())

.andReturn();

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

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

String response = mockMvc

.perform(asyncDispatch(asyncListener))

.andReturn()

.getResponse()

.getContentAsString();



assertThat(response)

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

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


DeferredResult的超时机制asyncListener和response调用之间添加超时启用程序来稍微更改测试代码

((MockAsyncContext) asyncListener

.getRequest()

.getAsyncContext())

.getListeners()

.get(0)

.onTimeout(null);

这段代码可能看起来很奇怪,但是我们以这种方式onTimeout我们这样做是为了让[AsyncListener](https://docs.oracle.com/javaee/7/api/javax/servlet/AsyncListener.html)知道某个操作已超时。这将确保正确调用我们为控制器中的onTimeout方法实现Runnable

4.结论

在本文中,我们介绍了如何在长轮询的上下文中DeferredResult我们还讨论了如何编写用于长期轮询的订阅客户端,以及如何对其进行测试。


标签:

0 评论

发表评论

您的电子邮件地址不会被公开。 必填的字段已做标记 *