Java-反应流教程-二-

121 阅读14分钟

Java 反应流教程(二)

原文:Reactive Streams in Java

协议:CC BY-NC-SA 4.0

九、Akka HTTP 和 Akka 流

当考虑使用哪个库或框架来创建利用 Akka 流的 web 应用程序时,有许多东西可供选择,Play Framework、Apache Camel 或 Akka HTTP 等等。对于这一章,我们将集中在使用 Akka HTTP。Akka HTTP 服务器是在 Akka 流之上实现的,并大量使用它。

Akka HTTP 一直致力于提供构建集成层的工具,而不是应用核心。因此,它认为自己是一套库,而不是一个框架。

-http docs 的一个子节点

Akka HTTP 采用了一种非个人化的方法,更喜欢被看作是一组库而不是一个框架。虽然这可能会使开始变得更加困难,但它允许开发人员有更多的灵活性,并对正在发生的一切有一个清晰的视图。幕后没有什么“魔法”能让它工作。

Akka HTTP 支持以下内容:

  • HTTP : Akka HTTP 实现了包括持久连接和客户端连接池的 HTTP/1.1。

  • Java 提供的工具支持 HTTPS。

  • WebSocket : Akka HTTP 在服务器端和客户端都实现了 WebSocket。

  • HTTP/2 : Akka HTTP 提供服务器端 HTTP/2 支持。

  • Multipart : Akka HTTP 已经对 multipart/*有效载荷进行了建模。它提供流式多部分解析器和呈现器,例如用于解析文件上传,并提供类型化模型来访问这种有效载荷的细节。

  • 服务器发送事件(SSE) :通过提供或消费(基于 Akka 流的)事件流的编组来支持。

  • JSON:Java 中基于 Jackson 的模型支持与 JSON 之间的编组。

  • Gzip 和 Deflate 内容编码。

它还有一个测试库来帮助测试。

对于我们的示例项目,我们将使用 Akka HTTP 以及 Akka 流 和 WebSockets 来创建一个带有假存储库的实时聊天机器人 web 服务器。

入门指南

虽然你可以使用 SBT (Scala 的构建工具)、Maven 或许多其他构建工具,但这里我们使用的是 Gradle。

首先创建一个名为“build.gradle”的构建文件,包含以下内容:

  1. 指定插件。

  2. 设置应用程序的名称。

  3. 使用静态 void main 方法设置主类以运行。

  4. 将 Java 版本设置为 10。

  5. 设置要使用的 Akka HTTP 和 Akka 版本的变量。

  6. 指定此项目所需的所有依赖项,包括用于测试的 akka-http-testkit、akka-stream-testkit、junit 和 assertj。

apply plugin: 'java' //1
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'application'

group = 'com.github.adamldavis'
applicationName = 'akka-http-java' //2
version = '0.0.1-SNAPSHOT'
mainClassName = 'com.github.adamldavis.akkahttp.WebApp' //3
// requires Gradle 4.7+
sourceCompatibility = 1.10 //4
targetCompatibility = 1.10

repositories {
    mavenCentral()
}
ext {
    akkaHttpVersion = '10.1.5' //5
    akkaVersion = '2.5.12'
}

dependencies {
  compile "com.typesafe.akka:akka-http_2.12:$akkaHttpVersion" //6
  compile "com.typesafe.akka:akka-http-jackson_2.12:$akkaHttpVersion"
  compile "com.typesafe.akka:akka-stream_2.12:$akkaVersion"

  testCompile "com.typesafe.akka:akka-http-testkit_2.12:$akkaHttpVersion"
  testCompile "com.typesafe.akka:akka-stream-testkit_2.12:$akkaVersion"
  testCompile 'junit:junit:4.12'
  testCompile "org.assertj:assertj-core:3.11.1"
}

然后创建一个名为 WebApp 的类,并从以下导入开始:

import akka.NotUsed;
import akka.actor.ActorSystem;
import akka.http.javadsl.ConnectHttp;
import akka.http.javadsl.Http;
import akka.http.javadsl.ServerBinding;
import akka.http.javadsl.model.*;
import akka.http.javadsl.server.*;
import akka.stream.ActorMaterializer;
import akka.stream.javadsl.Flow;
import akka.stream.javadsl.Source;
import akka.util.ByteString;

接下来,使该类扩展 AllDirectives 以启用 Java DSL,并添加如下所示的 main 方法:

  1. 为此应用程序创建 ActorSystem。

  2. 使用这个系统,创建一个 Http 实例,它是 Akka HTTP 服务器。

  3. 为了访问所有指令,我们需要一个定义路由的实例。

  4. 启动服务器,将其绑定到 localhost 上的端口 5010,并使用前面代码中定义的 routeFlow。

  5. 最后,我们添加一个 shutdown 钩子来解除服务器的绑定并关闭 ActorSystem。

public static void main(String[] args) {
  ActorSystem system = ActorSystem.create("routes");//1
  final Http http = Http.get(system); //2
  final ActorMaterializer materializer =
        ActorMaterializer.create(system);
  var app = new WebApp(); //3
  final Flow<HttpRequest, HttpResponse, NotUsed>
        routeFlow = app.joinedRoutes()
        .flow(system, materializer);
  final CompletionStage<ServerBinding> binding =
        http.bindAndHandle(routeFlow,
                ConnectHttp.toHost("localhost", 5010),
                materializer); //4
  System.out.println("Server online at http://localhost:5010/\nUse Ctrl+C to stop");
  // add shutdown Hook to terminate system:
  Runtime.getRuntime().addShutdownHook(new Thread(() -> { //5
        System.out.println("Shutting down...");
        binding.thenCompose(ServerBinding::unbind)
               .thenAccept(unbound -> system.terminate());
  }));
}

要运行应用程序,只需在命令行中使用命令“gradle run”。

路线

可以使用服务器 DSL 定义路由,使用简单的名称,如“route”、“path”和“get”。在您的路由中匹配的第一个路径将导致您的处理程序为该路由运行。如果没有匹配的路由,默认情况下将返回 HTTP 状态为 404(未找到)的响应。

例如,下面的方法定义了一个匹配“/hello”的路由:

private Route createHelloRoute() {
  return route(
        path("hello", () ->
                get(() ->
                  complete(HttpEntities.create(
                  ContentTypes.TEXT_HTML_UTF8,
                  "<h1>Say hello to akka-http</h1>"))
        )));
}

这条路线只是返回一个简单的 HTML 实体,如前面的代码所示。我们通过使用 ContentType 和字符串调用 HttpEntities.create 来创建 HttpEntity。“complete”方法表示响应由给定的参数完成,并被重载以接受许多不同的值,如 String、StatusCode、HttpEntity 或 HttpResponse。它还有一个变量,带有 Iterable 类型的附加参数来指定响应的头。这里我们使用的是完整的(HttpEntity)类型。

HttpEntities.create 方法也被重载以接受字符串、字节字符串、字节数组、路径、文件或 Akka 流源。

我们可以通过运行我们的应用程序,然后使用“curl localhost:5010/hello”命令来测试路由。我们应该得到以下输出:

<h1>Say hello to akka-http</h1>

可以使用允许组合路由的重载“route”方法将路由组合成一条路由。例如:

private Route joinedRoutes() {
  return route(createHelloRoute(),
        createRandomRoute(),
        createWebsocketRoute());
}

这里我们提供了一条结合了我们定义的三条路线的路线。

因为 Akka HTTP 是建立在 Akka 流之上的,所以我们可以向任何路由提供无限的字节流。Akka HTTP 将使用 HTTP 的内置速率限制规范,在内存使用不变的情况下提供一个流。以下方法为路径“/random”上的请求提供了一个随机数流:

  1. 这里我们使用 Stream.generate 生成一个无限字节流,然后使用 Source.fromIterator 将其转换为 Source。

  2. 使用 ByteString 将每个数字转换成一个字节块。

private Route createRandomRoute() {
  final Random rnd = new Random();
  Source<Integer, NotUsed> numbers = //1
  Source.fromIterator(() ->
    Stream.generate(rnd::nextInt).iterator());
  return route(
    path("random", () ->
      get(() ->
        complete(
        HttpEntities.create(
                ContentTypes.TEXT_PLAIN_UTF8,
                numbers.map(x ->
                  ByteString.fromString(x + "\n")))) //2
        )));
}

我们可以在应用程序运行时使用命令“curl-limit-rate 1k 127 . 0 . 0 . 1:5010/random”来测试这个路由(将下载速率限制在 1 千字节/秒)。

求转发到

最后,我们可以使用“handleWebSocketMessages”创建一个 WebSocket 处理路由,如下所示:

public Route createWebsocketRoute() {
  return path("greeter", () ->
    handleWebSocketMessages(
        WebSocketExample.greeter())
  );
}

WebSocketExample 中的“greeter”方法定义了一个处理程序,该处理程序将传入的消息视为一个名称,并以对该名称的问候作为响应:

public static
Flow<Message, Message, NotUsed> greeter() {
  return Flow.<Message>create()
    .collect(new JavaPartialFunction<>() {
      @Override
      public Message apply(Message msg,
                boolean isCheck) {
      if (isCheck) {
        if (msg.isText()) return null;
        else throw noMatch();
      } else {
        return handleTextMessage(
                msg.asTextMessage());
      }
    }});
}
public static TextMessage
        handleTextMessage(TextMessage msg) {
  if (msg.isStrict()) {
        return TextMessage.create("Hello " +
                msg.getStrictText());
  } else {
        return TextMessage.create(Source.single(
        "Hello ").concat(msg.getStreamedText()));
  }
}

关于 JavaPartialFunction,需要知道的重要一点是,它可以用 isCheck 作为 true 或 false 多次调用。如果 isCheck 为 true,它只是检查您的 JavaPartialFunction 是否处理给定的类型,这就是为什么如果消息不是 TextMessage 类型(isText 返回 false),我们会“抛出 noMatch()”。

由于复杂的 WebSocket 协议,测试 WebSockets 更加复杂。接下来,我们将构建一个聊天应用程序来演示 WebSockets。

我们的领域

对于这个示例应用程序,我们将构建一个简单的聊天服务器。核心域模型是如下的 ChatMessage:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ChatMessage {
  final String username;
  final String message;

  @JsonCreator
  public ChatMessage(
        @JsonProperty("username") String username,
        @JsonProperty("message") String message) {
        this.username = username;
        this.message = message;
  }
  // toString, equals, and hashCode omitted for
  // brevity
  public String getUsername() { return username; }
  public String getMessage() { return message; }
}

这个 ChatMessage 对象是不可变的,只保存用户名和消息的值。

我们将使用 Jackson 进行与 JSON 的相互转换,因此我们有一些注释来允许这种情况发生。

我们的仓库

出于演示的目的,我们的存储库不会实际保存,而只是模拟一个长时间运行的操作,并打印出保存的消息。其代码如下:

import java.util.concurrent.*;

public class MessageRepository {
  public CompletionStage<ChatMessage> save(
        ChatMessage message) {
  return CompletableFuture.supplyAsync(() -> {
    try { Thread.sleep(500); }
    catch (InterruptedException e)
        { e.printStackTrace(); }
    System.out.println("saving message: " + message);
    return message; });
  }
}

它使用 Java 的 CompletableFuture 来执行一个异步动作,并在该动作中休眠半秒钟。在实际的应用程序中,我们希望将聊天消息保存到某种数据库中,这可能需要一些时间来阻塞。

聊天服务器

聊天服务器的主要入口点将是 chat server 类。

它从以下导入开始:

akka.NotUsed;
akka.actor.ActorSystem;
akka.http.javadsl.model.ws.Message;
akka.http.javadsl.model.ws.TextMessage;
akka.japi.JavaPartialFunction;
akka.stream.*;
akka.stream.javadsl.*;
com.fasterxml.jackson.databind.ObjectMapper;
org.reactivestreams.Publisher;
java.util.concurrent.*;

为了简洁起见,我们将跳过这些字段,因为它们可以从构造函数中派生出来。ChatServer 构造函数进行了一些非常重要的初始化,我们将使用这些初始化在客户端之间传播 ChatMessages:

  1. 这里我们使用 Java 的内置运行时类初始化一个 int 属性 parallelism。我们将它设置为可用处理器的数量,因为这将允许我们在并行处理中利用每个处理器。

  2. 创建 ActorMaterializer。

  3. 为了简单起见,我们在这里使用 Java 10 的“var ”,因为完整类型非常长。在接收器上使用静态方法“asPublisher”会创建一个接收器,该接收器也可以充当 org . react vestreams . publisher。默认情况下,它只允许一个订阅者,因此使用 WITH_FANOUT 可允许多个订阅者。我们必须调用 preMaterialize 来访问 Publisher 和 Sink 的实际实例。

  4. 因为我们希望多个客户端将 ChatMessages 推入一个接收器,所以我们必须使用 MergeHub。与上一步非常相似,您必须用一个实体化器运行 MergeHub 来访问 Sink 实例。

public ChatServer(ActorSystem actorSystem) {
 parallelism =
        Runtime.getRuntime().availableProcessors(); //1
 this.actorSystem = actorSystem;
 materializer = ActorMaterializer.create(
        actorSystem); //2
 var asPublisher = Sink.<ChatMessage>asPublisher(
        AsPublisher.WITH_FANOUT); //3
 var publisherSinkPair =
        asPublisher.preMaterialize(materializer);
 publisher = publisherSinkPair.first();
 sink = publisherSinkPair.second();
 mergeHub = MergeHub.of(ChatMessage.class,
        BUFFER_SIZE).to(sink); //4
 mergeSink = mergeHub.run(materializer);
}

MergeHub 和 Publisher

虽然这看起来很复杂,但我们在这里使用 MergeHub 和 asPublisher 所做的只是允许多个流使用同一个 Sink,从而推送到 Publisher 的一个实例。

通过这种方式,我们可以将每个新的 WebSocket 连接发送到一个接收器中,并订阅一个中央发布者,我们将在接下来看到这一点。

WebSocket 流

对于我们的聊天服务器应用程序,我们需要创建一个主流程。我们用下面的代码(为简洁起见,省略了一些)类似于前面的定义(添加了一个图形):

  1. 创造流动。类型声明描述了流接收消息并输出 ChatMessage,并且不使用补充数据类型。我们添加了一个给定大小的缓冲区 BUFFER_SIZE,它可以是我们系统的内存所能处理的最大值。在 JavaPartialFunction 中,调用我们将在后面定义的 storeMessageFromContent。

  2. 使用 mapAsync 展开 CompletionStage 。该调用允许使用并行度数量的并发线程并行运行数据库保存。

  3. 使用 GraphDSL 创建 FlowShape。此图将使用前面的 savingFlow 保存所有 ChatMessages 并将其放入 mergeSink,但使用 ChatServer 发布者的输出,以便每个客户端都可以获得每个 ChatMessage。

  4. 创建 toMessage FlowShape,它将 ChatMessage 转换为 JSON,然后将其包装在 TextMessage 中。

  5. 通过将 mergeSink 添加到图表的构建器来创建“sinkInlet”。同样以类似的方式创建“publisherOutput”和“saveFlow”。

  6. 将 saveFlow 的输出连接到 sinkInlet。

  7. 将 publisherOutput 输出连接到 toMessage 的入口。

  8. 使用保存流的入口和消息流的出口定义流图。

public Flow<Message, Message, NotUsed> flow() {

Flow<Message, ChatMessage, NotUsed> savingFlow =
  Flow.<Message>create() //1
  .buffer(BUFFER_SIZE, OverflowStrategy.backpressure())
  .collect(new
        JavaPartialFunction<Message,
        CompletionStage<ChatMessage>>() {
  @Override
  public CompletionStage<ChatMessage>
                apply(Message msg, boolean isCheck) {
    if (msg.isText()) {
      TextMessage textMessage = msg.asTextMessage();
      return storeMessageFromContent(
                CompletableFuture.completedFuture(
                textMessage.getStrictText()));
    } else if (isCheck)
      throw noMatch();
    return CompletableFuture.completedStage(
                new ChatMessage(null, null));
    }

  })
  .mapAsync(parallelism, stage -> stage) // 2
  .filter(m -> m.username != null);
final Graph<FlowShape<Message,Message>, NotUsed>graph = //3
  GraphDSL.create(builder -> {
    final FlowShape<ChatMessage, Message>
                toMessage = //4
                builder.add(Flow.of(ChatMessage.class)
                .map(jsonMapper::writeValueAsString)
                .async()
                .map(TextMessage::create));
    Inlet<ChatMessage> sinkInlet =
        builder.add(mergeSink).in(); //5
    Outlet<ChatMessage> publisherOutput = builder
        .add(Source.fromPublisher(publisher)).out();
    FlowShape<Message, ChatMessage> saveFlow =
        builder.add(savingFlow);
    builder.from(saveFlow.out()).toInlet(sinkInlet);//6
    builder.from(publisherOutput)
        .toInlet(toMessage.in()); // 7
    return new FlowShape<>(saveFlow.in(),
        toMessage.out()); // 8
  });
return Flow.fromGraph(graph);
}

诸如“storeMessageFromContent”之类的帮助器方法(和字段)定义如下:

  1. 方法 parseContent 返回一个流,该流使用 Jackson 的 ObjectMapper,jsonMapper,将字符串转换为 ChatMessage 的实例,我们将在后面定义。

  2. storeChatMessages 方法返回一个使用 mapAsyncUnordered 和 messageRepository 上的 save 方法的接收器(允许以任何顺序并行保存)。

  3. 这一行将流具体化为一个只保留最后一个元素输入的接收器。这是可行的,因为它只给出了一个元素。

  4. 方法 storeMessageFromContent 通过从给定的 CompletionStage 创建源开始。

  5. 然后,使用 via(Flow)将该字符串转换为 ChatMessage。

  6. 最后,它使用 whenComplete 打印出保存的每条消息,并处理任何错误。虽然这里我们只是打印堆栈跟踪,但在生产系统中,您应该使用日志记录或其他方法来从错误中恢复。

  7. 创建一个 singleton MessageRepository 和 ObjectMapper,用于将 ChatMessages 与 JSON 相互转换。

private Flow<String, ChatMessage, NotUsed> parseContent() { //1
  return Flow.of(String.class)
        .map(line -> jsonMapper.readValue(line,
                ChatMessage.class));
}
private Sink<ChatMessage, CompletionStage<ChatMessage>> storeChatMessages() {
  return Flow.of(ChatMessage.class)
        .mapAsyncUnordered(parallelism,
                messageRepository::save) //2
        .toMat(Sink.last(), Keep.right()); //3
}
CompletionStage<ChatMessage> storeMessageFromContent(
                CompletionStage<String> content) {
  return Source.fromCompletionStage(content) //4
                .via(parseContent())
                .runWith(storeChatMessages(),
                         materializer) //5
                .whenComplete((message, ex) -> { //6
                  if (message != null) System.out
                    .println("Saved message: "+message);
                  else { ex.printStackTrace(); }
                });
}
final MessageRepository messageRepository =
        new MessageRepository();
final ObjectMapper jsonMapper =
        new ObjectMapper(); //7

我们还更新了 WebApp 中的“createWebsocketRoute”方法,以使用我们的新流程:

return path("chatws", () ->
        handleWebSocketMessages(chatServer.flow())
);

网络客户端

为了让最终用户使用我们的 WebSocket,我们必须有某种前端。为此,我们在“src/main/resources/akkahttp”下创建一个“index.html”文件,其内容如下:

  1. 创建 WebSocket 连接。

  2. 在我们的“submitChat”函数中,用用户名和消息构造一个名为“msg”的对象。

  3. 将 msg 对象作为 JSON 格式的字符串发送。

  4. 将消息输入元素留空,以告知用户消息已发送,并允许输入新的消息。

  5. 定义 WebSocket 的 onmessage 事件处理程序,它将聊天消息追加到页面中。

  6. 最后,我们为用户的输入创建表单。

<!DOCTYPE html>
<html>
<head>
<title>Hello Akka HTTP!</title>
<script>
var webSocket =
  new WebSocket("ws://localhost:5010/chatws"); //1
function submitChat() {
  var msg = { // 2
    username: document.getElementById("u").value,
    message: document.getElementById("m").value
  };
  webSocket.send(JSON.stringify(msg)); //3
  document.getElementById("m").value = ""; //4
}
webSocket.onmessage = function (event) { //5
  console.log(event.data);
  var content = document.getElementById("content");
  content.innerHTML = content.innerHTML
        + '<br>' +   event.data;
}

</script>
</head>
<body>
 <form> <!--6-->
  Username:<input type="text" id="u"
        name="username"><br>
  Message: <input type="text" id="m"
        name="message"><br>
  <input type="button" value="Submit"
        onclick="submitChat()">
 </form>
<div id="content"></div>
</body>
</html>

虽然这是一个非常简单的接口,但它仅仅是为了演示强大的后端。有了这个简单的聊天服务器,我们可以同时处理成千上万的用户。

在真实的应用程序中,您可以改进界面,添加错误处理和其他功能,如搜索、聊天室和安全性。

我们还需要更新路由来服务这个文件。用以下内容更新 createHelloRoute 方法:

  1. 使用 getResourceAsStream 从类路径中读取文件。

  2. 使用 Java 的 InputStream 的 readAllBytes 方法从文件中读取所有字节。

  3. 将字节数组转换为 Akka HTTP 的字节字符串。

final Source<String,NotUsed> file =
        Source.single("/akkahttp/index.html");
return route(
  path("hello", () ->
    get(() ->
      complete(
          HttpEntities
            .create(ContentTypes.TEXT_HTML_UTF8,
              file.map(f ->
                WebApp.class.getResourceAsStream(f)) //1
                  .map(stream -> stream.readAllBytes()) //2
                  .map(bytes -> ByteString.fromArray(bytes))))//3
        )));

您可以通过运行 WebApp 并在几个浏览器中访问“http://localhost:5010/hello”来测试应用程序。

测试

除了我们的标准 Akka HTTP 和 Akka 流 导入之外,我们还添加了以下导入:

akka.testkit.javadsl.TestKit;
akka.util.ByteString;
com.github.adamldavis.akkahttp.*;
org.junit.*;
java.util.*;
java.util.concurrent.*;
static org.assertj.core.api.Assertions.assertThat;

我们的 ChatServerTest 类的核心是下面的安装和拆卸:

  1. 在每次测试之前,我们做以下工作:创建 ActorSystem。

  2. 创建聊天服务器。

  3. 创建一个 ActorMaterializer,我们将用于测试。

  4. 每次测试后,我们使用 Akka TestKit 关闭 TestKit ActorSystem。

ChatServer chatServer;
ActorSystem actorSystem;
ActorMaterializer materializer;
@Before
public void setup() {
 actorSystem = ActorSystem.create("test-system"); //1
 chatServer = new ChatServer(actorSystem);//2
 materializer = ActorMaterializer.create(actorSystem);//3
}
@After
public void tearDown() {
 TestKit.shutdownActorSystem(actorSystem);//4
}

然后,我们定义一个类似下面的测试,简单地确保 ChatMessage 作为 JSON 编码的 TextMessage 被复制到流的输出:

  1. 创建一个 ConcurrentLinkedDeque(命名列表)来保存消息,以避免任何多线程问题(这可能是多余的)。

  2. 调用 flow()来获取我们想要测试的 WebSocket 流。

  3. 用 JSON 编码的聊天消息创建一个文本消息。虽然我们在这里只创建了一个,但是在其他测试中,我们可以使用 Source.range 创建许多,然后像下面这样映射:Source.range(1,100)。map(I-> text message . create(JSON msg(I)))。

  4. 创建 testSink,将每条消息添加到我们之前定义的列表中。

  5. 使用源、接收器和实体化器调用 flow.runWith。这是测试流开始的地方。

  6. 我们必须调用 toCompletableFuture()。超时进入我们的 CompletionStage,以便用测试结果重新连接当前线程。否则,它将永远运行下去,因为底层发布者(由 MergeHub 和 Sink.asPublisher 支持)没有定义停止点。

  7. 断言输出 TextMessage 按照预期编码成 JSON。

@Test
public void flow_should_copy_messages() throws ExecutionException, InterruptedException {
 final Collection<Message> list = new
        ConcurrentLinkedDeque<>(); //1
 Flow<Message, Message, NotUsed> flow = chatServer.flow(); //2
 assertThat(flow).isNotNull();
 List<Message> messages =
   Arrays.asList(TextMessage.create(jsonMsg(0))); //3
 Graph<SourceShape<Message>, ?> testSource =
        Source.from(messages);
 Graph<SinkShape<Message>, CompletionStage<Done>>
        testSink = Sink.foreach(list::add); //4
 CompletionStage<Done> results = flow.runWith(testSource,
        testSink, materializer).second(); //5
 try {
  results.toCompletableFuture().get(2, TimeUnit.SECONDS); //6
 } catch (TimeoutException te) {
  System.out.println("caught expected: " +
        te.getMessage());
 }

 Iterator<Message> iterator = list.iterator();
 assertThat(list.size()).isEqualTo(1);

 assertThat(iterator.next()
        .asTextMessage().getStrictText())
        .isEqualTo("{\"username\":\"foo\",”+
                “\"message\":\"bar0\"}"); //7
}
static final String jsonMsg(int i) {
 return "{\"username\": \"foo\", \"message\": \"bar"
        + i + "\"}";
}

GitHub 上的完整代码有更多的测试,但是这应该给你一个如何测试一个基于 Akka HTTP 的项目的好主意。

十、总结

有许多方法可以比较不同的编程库,其中许多是主观的。问十个不同的程序员,你可能会得到十个不同的答案。

你可能会比较图书馆的易用性,社区的规模,工作的受欢迎程度,灵活性,性能,或者一些更高的概念,如完整性或内聚性,或者许多其他方面。如果您确实关注性能,请记住有无数种方法可以比较性能,任何差异都可能是由于程序员对这些库的理解有限造成的。出于本书的目的,我们将简短地看一下每个库的独特优势。

RxJava

RxJava 的优势在于它是更大的 Rx 项目的一部分。例如,如果开发人员熟悉 RxJS,迁移到 RxJava 可能会容易得多。它似乎也是唯一一个使用流行的现有开源库来构建 Android 应用程序的反应式流库。

Reactor

Project Reactor 是更大的 Spring Framework 库套件的一部分。正因如此,对于已经在使用 Spring 的人来说可能更熟悉,它与 Spring Data 之类的其他项目有很好的集成。使用 Spring WebFlux,我们可以非常容易地创建一个非阻塞的异步应用程序,并使用一个后台 MongoDB、Redis 或 Cassandra 数据库。

Akka 流

Akka 流 的优势在于它是更大的 Akka 项目的一部分。它在 Scala 语言中也有很好的支持。因此,熟悉 Scala 或 Akka 的开发人员可能会发现使用起来容易得多。它还具有图形的独特概念。有了图和相关的 DSL,程序员可以用流构建大型复杂的图,这在其他反应流库中可能很难做到。

结论

这些库中的任何一个都是构建反应式、异步、非阻塞、容错应用程序的绝佳选择,选择使用哪一个在很大程度上取决于项目和团队。