Java 反应流教程(二)
九、Akka HTTP 和 Akka 流
当考虑使用哪个库或框架来创建利用 Akka 流的 web 应用程序时,有许多东西可供选择,Play Framework、Apache Camel 或 Akka HTTP 等等。对于这一章,我们将集中在使用 Akka HTTP。Akka HTTP 服务器是在 Akka 流之上实现的,并大量使用它。
Akka HTTP 一直致力于提供构建集成层的工具,而不是应用核心。因此,它认为自己是一套库,而不是一个框架。
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”的构建文件,包含以下内容:
-
指定插件。
-
设置应用程序的名称。
-
使用静态 void main 方法设置主类以运行。
-
将 Java 版本设置为 10。
-
设置要使用的 Akka HTTP 和 Akka 版本的变量。
-
指定此项目所需的所有依赖项,包括用于测试的 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 方法:
-
为此应用程序创建 ActorSystem。
-
使用这个系统,创建一个 Http 实例,它是 Akka HTTP 服务器。
-
为了访问所有指令,我们需要一个定义路由的实例。
-
启动服务器,将其绑定到 localhost 上的端口 5010,并使用前面代码中定义的 routeFlow。
-
最后,我们添加一个 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”上的请求提供了一个随机数流:
-
这里我们使用 Stream.generate 生成一个无限字节流,然后使用 Source.fromIterator 将其转换为 Source。
-
使用 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:
-
这里我们使用 Java 的内置运行时类初始化一个 int 属性 parallelism。我们将它设置为可用处理器的数量,因为这将允许我们在并行处理中利用每个处理器。
-
创建 ActorMaterializer。
-
为了简单起见,我们在这里使用 Java 10 的“var ”,因为完整类型非常长。在接收器上使用静态方法“asPublisher”会创建一个接收器,该接收器也可以充当 org . react vestreams . publisher。默认情况下,它只允许一个订阅者,因此使用 WITH_FANOUT 可允许多个订阅者。我们必须调用 preMaterialize 来访问 Publisher 和 Sink 的实际实例。
-
因为我们希望多个客户端将 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 流
对于我们的聊天服务器应用程序,我们需要创建一个主流程。我们用下面的代码(为简洁起见,省略了一些)类似于前面的定义(添加了一个图形):
-
创造流动。类型声明描述了流接收消息并输出 ChatMessage,并且不使用补充数据类型。我们添加了一个给定大小的缓冲区 BUFFER_SIZE,它可以是我们系统的内存所能处理的最大值。在 JavaPartialFunction 中,调用我们将在后面定义的 storeMessageFromContent。
-
使用 mapAsync 展开 CompletionStage 。该调用允许使用并行度数量的并发线程并行运行数据库保存。
-
使用 GraphDSL 创建 FlowShape。此图将使用前面的 savingFlow 保存所有 ChatMessages 并将其放入 mergeSink,但使用 ChatServer 发布者的输出,以便每个客户端都可以获得每个 ChatMessage。
-
创建 toMessage FlowShape,它将 ChatMessage 转换为 JSON,然后将其包装在 TextMessage 中。
-
通过将 mergeSink 添加到图表的构建器来创建“sinkInlet”。同样以类似的方式创建“publisherOutput”和“saveFlow”。
-
将 saveFlow 的输出连接到 sinkInlet。
-
将 publisherOutput 输出连接到 toMessage 的入口。
-
使用保存流的入口和消息流的出口定义流图。
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”之类的帮助器方法(和字段)定义如下:
-
方法 parseContent 返回一个流,该流使用 Jackson 的 ObjectMapper,jsonMapper,将字符串转换为 ChatMessage 的实例,我们将在后面定义。
-
storeChatMessages 方法返回一个使用 mapAsyncUnordered 和 messageRepository 上的 save 方法的接收器(允许以任何顺序并行保存)。
-
这一行将流具体化为一个只保留最后一个元素输入的接收器。这是可行的,因为它只给出了一个元素。
-
方法 storeMessageFromContent 通过从给定的 CompletionStage 创建源开始。
-
然后,使用 via(Flow)将该字符串转换为 ChatMessage。
-
最后,它使用 whenComplete 打印出保存的每条消息,并处理任何错误。虽然这里我们只是打印堆栈跟踪,但在生产系统中,您应该使用日志记录或其他方法来从错误中恢复。
-
创建一个 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”文件,其内容如下:
-
创建 WebSocket 连接。
-
在我们的“submitChat”函数中,用用户名和消息构造一个名为“msg”的对象。
-
将 msg 对象作为 JSON 格式的字符串发送。
-
将消息输入元素留空,以告知用户消息已发送,并允许输入新的消息。
-
定义 WebSocket 的 onmessage 事件处理程序,它将聊天消息追加到页面中。
-
最后,我们为用户的输入创建表单。
<!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 方法:
-
使用 getResourceAsStream 从类路径中读取文件。
-
使用 Java 的 InputStream 的 readAllBytes 方法从文件中读取所有字节。
-
将字节数组转换为 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 类的核心是下面的安装和拆卸:
-
在每次测试之前,我们做以下工作:创建 ActorSystem。
-
创建聊天服务器。
-
创建一个 ActorMaterializer,我们将用于测试。
-
每次测试后,我们使用 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 被复制到流的输出:
-
创建一个 ConcurrentLinkedDeque(命名列表)来保存消息,以避免任何多线程问题(这可能是多余的)。
-
调用 flow()来获取我们想要测试的 WebSocket 流。
-
用 JSON 编码的聊天消息创建一个文本消息。虽然我们在这里只创建了一个,但是在其他测试中,我们可以使用 Source.range 创建许多,然后像下面这样映射:Source.range(1,100)。map(I-> text message . create(JSON msg(I)))。
-
创建 testSink,将每条消息添加到我们之前定义的列表中。
-
使用源、接收器和实体化器调用 flow.runWith。这是测试流开始的地方。
-
我们必须调用 toCompletableFuture()。超时进入我们的 CompletionStage,以便用测试结果重新连接当前线程。否则,它将永远运行下去,因为底层发布者(由 MergeHub 和 Sink.asPublisher 支持)没有定义停止点。
-
断言输出 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,程序员可以用流构建大型复杂的图,这在其他反应流库中可能很难做到。
结论
这些库中的任何一个都是构建反应式、异步、非阻塞、容错应用程序的绝佳选择,选择使用哪一个在很大程度上取决于项目和团队。