文档

§处理异步结果

§使控制器异步

在内部,Play 框架从下到上都是异步的。Play 以异步、非阻塞的方式处理每个请求。

默认配置针对异步控制器进行了调整。换句话说,应用程序代码应避免在控制器中阻塞,即控制器代码等待操作完成。此类阻塞操作的常见示例包括 JDBC 调用、流式 API、HTTP 请求和长时间计算。

虽然可以通过增加默认执行上下文中的线程数量来允许更多并发请求由阻塞控制器处理,但遵循将控制器保持为异步的推荐方法可以更轻松地进行扩展,并在负载下保持系统响应。

§创建非阻塞操作

由于 Play 的工作方式,操作代码必须尽可能快,即非阻塞。那么,如果我们还无法计算结果,应该从操作中返回什么呢?我们应该返回结果的承诺

Java 8 及更高版本提供了一个名为 CompletionStage 的通用承诺 API。CompletionStage<Result> 最终将用类型为 Result 的值兑现。通过使用 CompletionStage<Result> 而不是普通的 Result,我们能够快速从操作中返回而不会阻塞任何内容。Play 将在承诺兑现后立即提供结果。

§如何创建 CompletionStage<Result>

要创建 CompletionStage<Result>,我们首先需要另一个承诺:提供计算结果所需实际值的承诺

CompletionStage<Double> promiseOfPIValue = computePIAsynchronously();
// Runs in same thread
CompletionStage<Result> promiseOfResult =
    promiseOfPIValue.thenApply(pi -> ok("PI value computed: " + pi));

Play 异步 API 方法为您提供 CompletionStage。当您使用 play.libs.WS API 调用外部 Web 服务时,或者当您使用 Pekko 调度异步任务或使用 play.libs.Pekko 与 Actor 通信时,就会出现这种情况。

在这种情况下,使用CompletionStage.thenApply将在与先前任务相同的调用线程中执行完成阶段。当您只有少量无阻塞的 CPU 绑定逻辑时,这很好。

异步执行代码块并获取CompletionStage 的一种简单方法是使用CompletableFuture.supplyAsync() 方法。

// creates new task
CompletionStage<Integer> promiseOfInt =
    CompletableFuture.supplyAsync(() -> intensiveComputation());

使用supplyAsync 会创建一个新任务,该任务将被放置在 fork join 池中,并且可能从不同的线程调用 - 尽管这里它使用的是默认执行器,在实践中您将显式指定执行器。

只有来自CompletionStage 的“*Async”方法提供异步执行。

§使用 ClassLoaderExecutionContext

Action 中使用 Java CompletionStage 时,您必须显式地将类加载器执行上下文作为执行器提供,以确保类加载器保持在范围内。

您可以通过依赖注入提供 play.libs.concurrent.ClassLoaderExecutionContext 实例。

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import javax.inject.Inject;
import play.libs.concurrent.ClassLoaderExecutionContext;
import play.mvc.*;

public class MyController extends Controller {

  private ClassLoaderExecutionContext clExecutionContext;

  @Inject
  public MyController(ClassLoaderExecutionContext ec) {
    this.clExecutionContext = ec;
  }

  public CompletionStage<Result> index() {
    // Use a different task with explicit EC
    return calculateResponse()
        .thenApplyAsync(
            answer -> {
              return ok("answer was " + answer).flashing("info", "Response updated!");
            },
            clExecutionContext.current());
  }

  private static CompletionStage<String> calculateResponse() {
    return CompletableFuture.completedFuture("42");
  }
}

有关使用ClassLoaderExecutionContext 的更多信息,请参阅 类加载器

§使用 CustomExecutionContext 和 ClassLoaderExecution

使用CompletionStageClassLoaderExecutionContext 只是其中的一部分!此时您仍然在 Play 的默认 ExecutionContext 上。如果您正在调用阻塞 API(例如 JDBC),那么您仍然需要让您的 ExecutionStage 使用不同的执行器运行,以将其从 Play 的渲染线程池中移开。您可以通过创建一个 play.libs.concurrent.CustomExecutionContext 的子类来实现,该子类包含对 自定义调度器 的引用。

添加以下导入

import play.libs.concurrent.ClassLoaderExecution;

import javax.inject.Inject;
import java.util.concurrent.Executor;
import java.util.concurrent.CompletionStage;
import static java.util.concurrent.CompletableFuture.supplyAsync;

定义自定义执行上下文

public class MyExecutionContext extends CustomExecutionContext {

  @Inject
  public MyExecutionContext(ActorSystem actorSystem) {
    // uses a custom thread pool defined in application.conf
    super(actorSystem, "my.dispatcher");
  }
}

您需要在application.conf 中定义一个自定义调度器,这可以通过 Pekko 调度器配置 完成。

一旦您有了自定义调度器,请添加显式执行器并使用 ClassLoaderExecution.fromThread 将其包装起来。

public class Application extends Controller {

  private MyExecutionContext myExecutionContext;

  @Inject
  public Application(MyExecutionContext myExecutionContext) {
    this.myExecutionContext = myExecutionContext;
  }

  public CompletionStage<Result> index() {
    // Wrap an existing thread pool, using the context from the current thread
    Executor myEc = ClassLoaderExecution.fromThread(myExecutionContext);
    return supplyAsync(() -> intensiveComputation(), myEc)
        .thenApplyAsync(i -> ok("Got result: " + i), myEc);
  }

  public int intensiveComputation() {
    return 2;
  }
}

您不能通过将同步 IO 包装在CompletionStage 中来神奇地将其转换为异步。如果您无法更改应用程序的架构以避免阻塞操作,那么在某个时刻该操作将必须执行,并且该线程将被阻塞。因此,除了将操作包含在CompletionStage 中之外,还需要将其配置为在已配置有足够线程来处理预期并发性的单独执行上下文中运行。有关更多信息,请参阅 了解 Play 线程池,并下载显示数据库集成的 Play 示例模板

§操作默认异步

Play 操作 默认是异步的。例如,在下面的控制器代码中,返回的 Result 在内部被封装在一个 promise 中

public Result index(Http.Request request) {
  return ok("Got request " + request + "!");
}

注意: 无论操作代码返回 Result 还是 CompletionStage<Result>,这两种返回的对象在内部以相同的方式处理。只有一种 Action,它是异步的,而不是两种(同步和异步)。返回 CompletionStage 是一种编写非阻塞代码的技术。

§处理超时

正确处理超时通常很有用,以避免在出现问题时让 Web 浏览器阻塞并等待。您可以使用 play.libs.concurrent.Futures.timeout 方法将 CompletionStage 包装在一个非阻塞超时中。

class MyClass {

  private final Futures futures;
  private final Executor customExecutor = ForkJoinPool.commonPool();

  @Inject
  public MyClass(Futures futures) {
    this.futures = futures;
  }

  CompletionStage<Double> callWithOneSecondTimeout() {
    return futures.timeout(computePIAsynchronously(), Duration.ofSeconds(1));
  }

  public CompletionStage<String> delayedResult() {
    long start = System.currentTimeMillis();
    return futures.delayed(
        () ->
            CompletableFuture.supplyAsync(
                () -> {
                  long end = System.currentTimeMillis();
                  long seconds = end - start;
                  return "rendered after " + seconds + " seconds";
                },
                customExecutor),
        Duration.of(3, SECONDS));
  }
}

注意: 超时与取消不同 - 即使在超时的情况下,给定的 future 仍然会完成,即使该完成的值没有返回。

下一步: 流式 HTTP 响应


发现此文档中的错误?此页面的源代码可以在 这里 找到。在阅读 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区开始对话。