我的应用程序正在处理大量 HTTP 请求/响应事务,其中一个服务的响应会导致下一步和后续请求,依此类推一系列步骤。
为了使代码优雅,我使用了下载和回调结构,可以简单地表示如下:
private void runFirstStep() {
String firstRequest = buildRequest();
sendHttpRequest(firstRequest, this::handleFirstStepResponse);
}
private void handleFirstStepResponse(InputStream responseBody) {
doStuffWithFirstStepResponse(responseBody);
String secondRequest = buildSecondRequest();
sendHttpRequest(secondRequest, this::handleSecondStepResponse);
}
private void handleSecondStepResponse(InputStream responseBody) {
doStuffWithSecondStepResponse(responseBody);
String thirdRequest = buildThirdRequest();
sendHttpRequest(thirdRequest, this::handleThirdStepResponse);
}
private void handleThirdStepResponse(InputStream responseBody) {
doStuffWithThirdStepResponse(responseBody);
// The flow has finished, so no further HTTP transactions.
}
尽管在我的例子中,序列长度目前达到大约 26 个步骤,全部以这种方式链接。
这工作正常,但我碰巧注意到控制台中的日志记录行,这清楚地表明每个方法只是等待链中的所有其他方法完成(当我想到这一点时,这是显而易见的) )。但这也让我认为我使用的这种模式可能有导致堆栈溢出的风险。
所以问题是:
像这样的序列(可能有几十个链接步骤)是否会面临堆栈溢出的风险,或者是否需要比这更多的滥用才能耗尽典型的堆栈?请注意,上面的简化代码结构隐藏了这样一个事实:这些方法实际上执行了很多操作(构建 XML、提取 XML、将请求/响应数据记录到日志文件),因此我们不是在讨论轻量级任务。
我是否应该使用一种不同的模式,该模式不会留下一系列方法,所有方法都耐心地等待整个流程完成?我的
sendHttpRequest
方法已经使用 JDK 11 HTTP 框架生成CompletableFuture<HttpResponse<InputStream>>
,但是我的sendHttpRequest
然后,方法只需等待该过程完成并使用结果调用指定的回调方法。是否应该创建一个新线程来处理CompletableFuture
,以便调用方法可以优雅地关闭?以及如何在不导致 JVM 关闭的情况下执行此操作(因为缓慢的 HTTP 响应将使 JVM 无法同时执行任何方法)?
作为 Stack Overflow(该网站,并非异常(exception)),我当然正在寻找涉及 Java 裸机机制的答案,而不是猜测或轶事。
更新:只是为了澄清一下,我的 sendHttpRequest
方法目前具有这种形状:
private void sendHttpRequest(String request,
Consumer<InputStream> callback) {
HttpRequest httpRequest = buildHttpRequestFromXml(request);
CompletableFuture<HttpResponse<InputStream>> completableExchange
= httpClient.
sendAsync(httpRequest, BodyHandlers.ofInputStream());
HttpResponse<InputStream> httpResponse = completableExchange.join();
InputStream responseBody = getBodyFromResponse(httpResponse);
callback.accept(responseBody);
}
重要的一点是 Java 的 HttpClient.sendAsync
方法返回 CompletablFuture
,然后 join()
对该对象进行调用以等待接收 HTTP 响应并以 HttpResponse
形式返回对象,然后用于将响应正文提供给指定的回调方法。但我的问题并不是专门关于 HTTP 请求/响应序列,而是处理任何适合等待结果和回调结构的流时的风险和最佳实践。
最佳答案
首先,如果一个方法将回调作为参数,它不应该阻塞调用线程。如果你正在阻塞,则不需要回调。(你可以从 sendHttpRequest 返回 InputStream 并用它调用下一个方法。)
您应该使用 CompletableFuture 实现完全异步。但这里您必须考虑一件事。当并行流操作和 CompletableFuture 不是专门在 Executor(线程池)上执行时,它们会使用公共(public)池。由于http下载是阻塞操作,因此您不应在公共(public)池中执行它(以免阻塞公共(public)池线程执行IO操作)。您应该创建 IO 池并将其传递给 CompletableFuture 方法,该方法在下载时以 Executor 作为参数。
至于如果继续当前的设计会发生什么;
当调用一个方法时,将创建一个堆栈帧并将其推送到调用线程的堆栈。该框架将保存该方法调用位置的返回地址、该方法采用的参数以及该方法的局部变量。如果这个参数和变量是基本类型,它们将存储在堆栈中,如果它们是对象,它们的地址将存储在堆栈中。当该方法执行完毕后,其框架将被销毁。
26 个方法调用链不应成为堆栈溢出问题。您还可以使用 -Xss 开关控制堆栈大小。每个平台的默认堆栈大小会有所不同(32 位和 64 位也会影响默认大小。)如果您正在为应用程序提供可执行命令,如果您担心这一点,您可能需要定义此值。
关于java - Java中链式方法回调会导致堆栈溢出吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54845181/