Akka Actor快速开始(Java)
文章目录
Akka 是一个用于在 JVM 上构建高度并发、分布式和容错的事件驱动应用程序的工具包。 Akka 可以与 Java 和 Scala 一起使用。 本指南通过描述 Hello World 示例的 Java 版本来介绍 Akka Actor。 如果您更喜欢将 Akka 与 Scala 一起使用,请切换到 Akka Quickstart with Scala 指南。
Actor 是 Akka 中的执行单元。 Actor 模型是一种抽象,可以更轻松地编写正确的并发、并行和分布式系统。 Hello World 示例说明了 Akka 的基础知识。 在 30 分钟内,您应该能够下载并运行示例并使用本指南了解示例的构建方式。 这会让您有一个初步的了解,并希望能激发您深入探索美妙的Akka!
在尝试了这个例子之后,下一步可以通过全面的入门指南继续学习更多关于 Akka 的内容。
Akka 平台指南讨论了更多 Akka 概念和功能,Akka 作为工具包给出了全面的概述。
下载示例
Java 的 Hello World 示例是一个压缩项目,其中包含 Maven 和 Gradle 的构建文件。 您可以在 Linux、MacOS 或 Windows 上运行它。 唯一的依赖环境是要预先安装好Java 8 和 Maven 或 Gradle。
下载和解压示例:
- 下载ZIP压缩包
- 解压压缩包到合适的目录下:
在 Linux 和 OSX 系统上,打开终端并使用命令 unzip akka-quickstart-java.zip。
在 Windows 上,使用文件资源管理器等工具来提取项目。
运行示例
确保您已经安装了您选择的构建工具,然后打开一个终端窗口,然后从项目目录中键入以下内容以运行 Hello World:
mvn compile exec:exec
应会看到如下输出(一直向右滚动以查看 Actor 输出):
Scanning for projects...
[INFO]
[INFO] ------------------------< hello-akka-java:app >-------------------------
[INFO] Building app 1.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ app ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO]
[INFO] --- exec-maven-plugin:1.6.0:exec (default-cli) @ app ---
[2019-10-12 09:20:30,248] [INFO] [akka.event.slf4j.Slf4jLogger] [helloakka-akka.actor.default-dispatcher-3] [] -
Slf4jLogger started
SLF4J: A number (1) of logging calls during the initialization phase have been intercepted and are
SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system.
SLF4J: See also http://www.slf4j.org/codes.html#replay
>>> Press ENTER to exit <<<
[2019-10-12 09:20:30,288] [INFO] [com.lightbend.akka.sample.Greeter] [helloakka-akka.actor.default-dispatcher-6]
[akka://helloakka/user/greeter] - Hello Charles!
[2019-10-12 09:20:30,290] [INFO] [com.lightbend.akka.sample.GreeterBot] [helloakka-akka.actor.default-dispatcher-3]
[akka://helloakka/user/Charles] - Greeting 1 for Charles
[2019-10-12 09:20:30,291] [INFO] [com.lightbend.akka.sample.Greeter] [helloakka-akka.actor.default-dispatcher-6]
[akka://helloakka/user/greeter] - Hello Charles!
[2019-10-12 09:20:30,291] [INFO] [com.lightbend.akka.sample.GreeterBot] [helloakka-akka.actor.default-dispatcher-3]
[akka://helloakka/user/Charles] - Greeting 2 for Charles
[2019-10-12 09:20:30,291] [INFO] [com.lightbend.akka.sample.Greeter] [helloakka-akka.actor.default-dispatcher-6]
[akka://helloakka/user/greeter] - Hello Charles!
[2019-10-12 09:20:30,291] [INFO] [com.lightbend.akka.sample.GreeterBot] [helloakka-akka.actor.default-dispatcher-3]
[akka://helloakka/user/Charles] - Greeting 3 for Charles
恭喜,你刚刚运行了你的第一个 Akka 应用程序。 现在来看看幕后发生的事情。
Hello World都做了些什么
该示例由三个Actor组成:
- Greeter: 接收问候某人的命令,并用问候语回复,以确认问候已发生
- GreeterBot: 接收来自问候者的回复,并发送多条额外的问候信息,并收集回复,直到达到给定的最大消息数。
- GreeterMain: 引导所有的守护Actor。
使用 Actor 模型的好处
Akka 的以下特性使您能够以直观的方式解决困难的并发性和可扩展性挑战:
- 事件驱动模型——Actor执行工作以响应消息。 Actor 之间的通信是异步的,允许 Actor 发送消息并继续自己的工作,而不会阻塞等待回复。
- 强隔离原则 — 与 Java 中的常规对象不同,Actor 在您可以调用的方法方面没有公共 API。 相反,它的公共 API 是通过Actor处理的消息来定义的。 这可以防止 Actor 之间的任何状态共享; 观察另一个Actor状态的唯一方法是向它发送一条消息。
- 位置透明性——系统从工厂构造 Actor 并返回对实例的引用。 因为位置无关紧要,Actor 实例可以启动、停止、移动和重新启动以扩大和缩小规模以及从意外故障中恢复。
- 轻量级——每个实例仅消耗几百个字节,这实际上允许数百万个并发 Actor 存在于单个应用程序中。
让我们看看在 Hello World 示例的上下文中使用 Actor 和消息的一些最佳实践。
定义Actor和消息
每个Actor为它可以接收的消息定义了一个泛型。 Case类和Case对象是非常好的消息,因为它们是不可变的,并且支持模式匹配,当匹配它接收到的消息时,我们将利用Actor中的这一点。
在Hello World程序中Actor使用三种不通的消息:
Greet
: 发送给Greeter
Actor以示问候的命令Greeted
: 从Greeter
Actor收到确认问候已发生的答复SayHello
: 命令GreeterMain
启动问候
在定义 Actor 及其消息时,请牢记以下建议:
- 由于消息是 Actor 的公共 API,因此定义具有良好名称和丰富语义和特定领域含义的消息是一种很好的做法,即使它们只是包装您的数据类型。 这将使基于Actor的系统更易于使用、理解和调试。
- 消息应该是不可变的,因为它们在不同的线程之间共享。
- 将Actor的相关消息作为静态类放在 AbstractBehavior 的类中是一种很好的做法。 这使得更容易理解Actor期望和处理的消息类型。
- 通过静态工厂方法获取Actor的初始行为是一种很好的做法
让我们看看 Greeter
、GreeterBot
和 GreeterMain
的实现如何展示这些最佳实践。
The Greeter actor
Greeter.java
中的以下代码片段实现了 Greeter
Actor:
public class Greeter extends AbstractBehavior<Greeter.Greet> {
public static final class Greet {
public final String whom;
public final ActorRef<Greeted> replyTo;
public Greet(String whom, ActorRef<Greeted> replyTo) {
this.whom = whom;
this.replyTo = replyTo;
}
}
public static final class Greeted {
public final String whom;
public final ActorRef<Greet> from;
public Greeted(String whom, ActorRef<Greet> from) {
this.whom = whom;
this.from = from;
}
}
public static Behavior<Greet> create() {
return Behaviors.setup(Greeter::new);
}
private Greeter(ActorContext<Greet> context) {
super(context);
}
@Override
public Receive<Greet> createReceive() {
return newReceiveBuilder().onMessage(Greet.class, this::onGreet).build();
}
private Behavior<Greet> onGreet(Greet command) {
getContext().getLog().info("Hello {}!", command.whom);
command.replyTo.tell(new Greeted(command.whom, getContext().getSelf()));
return this;
}
}
这一小段码定义了两种消息类型,一种用于Actor 向某人打招呼的命令,另一种用于 Actor 用来确认它已经这样做了。 Greet 类型不仅包含要问候的人的信息,它还包含消息发送者提供的 ActorRef
,以便 Greeter
Actor 可以发回确认消息。
Actor 的行为在 newReceiveBuilder
行为工厂的帮助下被定义为 Greeter
AbstractBehavior。然后,处理下一条消息会产生一种可能与此消息不同的新行为。 可以通过修改当前实例来更新状态,因为它是可变的。 在这种情况下,我们不需要更新任何状态,因此我们在没有任何字段更新的情况下返回它,这意味着下一个行为“与当前行为相同”。
此行为处理的消息类型被声明为Greet
类。通常,一个参与者处理多个特定的消息类型,然后有一个公共接口,Actor可以处理的所有消息都可以实现该接口。
在最后一行,我们看到Greeter
Actor向另一个Actor发送消息,调用了tell
方法。这是一个异步操作,不会阻塞调用方的线程。
由于 replyTo
引用被声明为ActorRef<Greeted>
类型,编译器只允许我们发送该类型的消息,使用其他消息的话会产生编译器错误。
Actor接受的消息类型以及所有回复类型定义了该Actor所说的协议;在这种情况下,它是一个简单的请求-应答协议,但Actor可以在需要时对任意复杂的协议进行重建。该协议与实现该协议的行为捆绑在一个包装良好的范围内,即Greeter
类。
The GreeterBot actor
package $package$;
import akka.actor.typed.Behavior;
import akka.actor.typed.javadsl.*;
public class GreeterBot extends AbstractBehavior<Greeter.Greeted> {
public static Behavior<Greeter.Greeted> create(int max) {
return Behaviors.setup(context -> new GreeterBot(context, max));
}
private final int max;
private int greetingCounter;
private GreeterBot(ActorContext<Greeter.Greeted> context, int max) {
super(context);
this.max = max;
}
@Override
public Receive<Greeter.Greeted> createReceive() {
return newReceiveBuilder().onMessage(Greeter.Greeted.class, this::onGreeted).build();
}
private Behavior<Greeter.Greeted> onGreeted(Greeter.Greeted message) {
greetingCounter++;
getContext().getLog().info("Greeting {} for {}", greetingCounter, message.whom);
if (greetingCounter == max) {
return Behaviors.stopped();
} else {
message.from.tell(new Greeter.Greet(message.whom, getContext().getSelf()));
return this;
}
}
}
请注意,此Actor如何使用实例变量管理计数器。不需要synchronized或AtomicInteger等并发保护,因为Actor实例一次只处理一条消息。
The GreeterMain actor
第三个Actor生成 Greeter 和 GreeterBot 并开始交互,创建Actor,接下来将讨论 spawn
的作用。
package $package$;
import akka.actor.typed.ActorRef;
import akka.actor.typed.Behavior;
import akka.actor.typed.javadsl.*;
public class GreeterMain extends AbstractBehavior<GreeterMain.SayHello> {
public static class SayHello {
public final String name;
public SayHello(String name) {
this.name = name;
}
}
private final ActorRef<Greeter.Greet> greeter;
public static Behavior<SayHello> create() {
return Behaviors.setup(GreeterMain::new);
}
private GreeterMain(ActorContext<SayHello> context) {
super(context);
//#create-actors
greeter = context.spawn(Greeter.create(), "greeter");
//#create-actors
}
@Override
public Receive<SayHello> createReceive() {
return newReceiveBuilder().onMessage(SayHello.class, this::onSayHello).build();
}
private Behavior<SayHello> onSayHello(SayHello command) {
//#create-actors
ActorRef<Greeter.Greeted> replyTo =
getContext().spawn(GreeterBot.create(3), command.name);
greeter.tell(new Greeter.Greet(command.name, replyTo));
//#create-actors
return this;
}
}
创建Actor
到目前为止,我们已经了解了 Actor 的定义及其消息。 现在让我们更深入地了解位置透明的优势,看看如何创建 Actor 实例。
位置透明的优势
在Akka中,不能使用new
关键字创建Actor的实例。相反,您可以使用工厂生成方法创建Actor实例。Spawn不会返回actor实例,而是一个引用akka.actor.typed.ActorRef
,该引用指向actor实例。这种间接的处理方式在分布式系统中增加了很多功能和灵活性。
在Akka中,位置无关紧要。位置透明意味着ActorRef
可以在保留相同语义的同时,表示进程中或远程机器上运行的Actor实例。
如果需要,运行时可以通过在运行时更改 Actor 的位置或整个应用程序拓扑来优化系统。 这启用了故障管理的“让它崩溃”模型,其中系统可以通过崩溃有故障的 Actor 并重新启动健康的 Actor 来自我修复。
Akka ActorSystem
ActorSystem 是 Akka 的初始入口点。 通常每个应用程序只创建一个 ActorSystem。 ActorSystem 有一个名字和一个守护Actor。 您的应用程序的引导通常在守护Actor中完成。
ActorSystem中的守护Actor就是GreeterMain
.
final ActorSystem<GreeterMain.SayHello> greeterMain = ActorSystem.create(GreeterMain.create(), "helloakka");
它使用 Behaviors.setup
来引导应用程序。
package $package$;
import akka.actor.typed.ActorRef;
import akka.actor.typed.Behavior;
import akka.actor.typed.javadsl.*;
public class GreeterMain extends AbstractBehavior<GreeterMain.SayHello> {
public static class SayHello {
public final String name;
public SayHello(String name) {
this.name = name;
}
}
private final ActorRef<Greeter.Greet> greeter;
public static Behavior<SayHello> create() {
return Behaviors.setup(GreeterMain::new);
}
private GreeterMain(ActorContext<SayHello> context) {
super(context);
//#create-actors
greeter = context.spawn(Greeter.create(), "greeter");
//#create-actors
}
@Override
public Receive<SayHello> createReceive() {
return newReceiveBuilder().onMessage(SayHello.class, this::onSayHello).build();
}
private Behavior<SayHello> onSayHello(SayHello command) {
//#create-actors
ActorRef<Greeter.Greeted> replyTo =
getContext().spawn(GreeterBot.create(3), command.name);
greeter.tell(new Greeter.Greet(command.name, replyTo));
//#create-actors
return this;
}
}
生成子Actor
其他Actor通过ActorContext
中的spawn
方法创建出来。
GreeterMain
在启动时以这种方式创建一个 Greeter
Actor,并在每次收到 SayHello
消息时创建一个新的 GreeterBot
。
异步通信
Actor是响应式和消息驱动的。 Actor 在收到消息之前不会做任何事情。 Actor之间使用异步消息进行通信。 这确保了发送方不会等待接收方处理他们的消息。 相反,发送方将消息放入接收方的邮箱中,并且可以自由地做其他工作。 Actor 的邮箱本质上是一个已经排好序的消息队列。 从同一个 Actor 发送的多条消息的顺序是固定的,但可以与另一个 Actor 发送的消息交错。
您可能想知道当 Actor 不处理消息(即做实际工作)时它在做什么? 它处于挂起状态,除内存外不消耗任何资源。 再次展示了 Actor 的轻量级、高效性。
给Actor发送消息
使用ActorRef
的tell
方法将消息发送到Actor的邮箱中。举个例子,Hello World程序中的主类发送消息到Greeter
Actor,就是这样:
greeterMain.tell(new GreeterMain.SayHello("Charles"));
Greeter
Actor 还会发送一条消息以确认它已收到问候语:
command.replyTo.tell(new Greeted(command.whom, getContext().getSelf()));
我们已经了解了如何创建Actor和发送消息。 现在,让我们回顾一下整个 Main
类。
Main Class
Hello World 中的 AkkaQuickstart
对象创建了带有守护的 ActorSystem。 守护是引导您的应用程序的顶级参与者。守护通常使用包含初始引导程序的 Behaviors.setup
定义。
package $package$;
import akka.actor.typed.ActorSystem;
import java.io.IOException;
public class AkkaQuickstart {
public static void main(String[] args) {
//#actor-system
final ActorSystem<GreeterMain.SayHello> greeterMain = ActorSystem.create(GreeterMain.create(), "helloakka");
//#actor-system
//#main-send-messages
greeterMain.tell(new GreeterMain.SayHello("Charles"));
//#main-send-messages
try {
System.out.println(">>> Press ENTER to exit <<<");
System.in.read();
} catch (IOException ignored) {
} finally {
greeterMain.terminate();
}
}
}
全部示例代码
下面是创建示例应用程序的三个类 Greeter、GreeterBot、GreeterMain 和 AkkaQuickstart 的完整源代码:
Greeter.java
package $package$;
import akka.actor.typed.ActorRef;
import akka.actor.typed.Behavior;
import akka.actor.typed.javadsl.*;
import java.util.Objects;
// #greeter
public class Greeter extends AbstractBehavior<Greeter.Greet> {
public static final class Greet {
public final String whom;
public final ActorRef<Greeted> replyTo;
public Greet(String whom, ActorRef<Greeted> replyTo) {
this.whom = whom;
this.replyTo = replyTo;
}
}
public static final class Greeted {
public final String whom;
public final ActorRef<Greet> from;
public Greeted(String whom, ActorRef<Greet> from) {
this.whom = whom;
this.from = from;
}
// #greeter
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Greeted greeted = (Greeted) o;
return Objects.equals(whom, greeted.whom) &&
Objects.equals(from, greeted.from);
}
@Override
public int hashCode() {
return Objects.hash(whom, from);
}
@Override
public String toString() {
return "Greeted{" +
"whom='" + whom + '\'' +
", from=" + from +
'}';
}
// #greeter
}
public static Behavior<Greet> create() {
return Behaviors.setup(Greeter::new);
}
private Greeter(ActorContext<Greet> context) {
super(context);
}
@Override
public Receive<Greet> createReceive() {
return newReceiveBuilder().onMessage(Greet.class, this::onGreet).build();
}
private Behavior<Greet> onGreet(Greet command) {
getContext().getLog().info("Hello {}!", command.whom);
//#greeter-send-message
command.replyTo.tell(new Greeted(command.whom, getContext().getSelf()));
//#greeter-send-message
return this;
}
}
// #greeter
GreeterBot.java
package $package$;
import akka.actor.typed.Behavior;
import akka.actor.typed.javadsl.*;
public class GreeterBot extends AbstractBehavior<Greeter.Greeted> {
public static Behavior<Greeter.Greeted> create(int max) {
return Behaviors.setup(context -> new GreeterBot(context, max));
}
private final int max;
private int greetingCounter;
private GreeterBot(ActorContext<Greeter.Greeted> context, int max) {
super(context);
this.max = max;
}
@Override
public Receive<Greeter.Greeted> createReceive() {
return newReceiveBuilder().onMessage(Greeter.Greeted.class, this::onGreeted).build();
}
private Behavior<Greeter.Greeted> onGreeted(Greeter.Greeted message) {
greetingCounter++;
getContext().getLog().info("Greeting {} for {}", greetingCounter, message.whom);
if (greetingCounter == max) {
return Behaviors.stopped();
} else {
message.from.tell(new Greeter.Greet(message.whom, getContext().getSelf()));
return this;
}
}
}
GreeterMain.java
package $package$;
import akka.actor.typed.ActorRef;
import akka.actor.typed.Behavior;
import akka.actor.typed.javadsl.*;
public class GreeterMain extends AbstractBehavior<GreeterMain.SayHello> {
public static class SayHello {
public final String name;
public SayHello(String name) {
this.name = name;
}
}
private final ActorRef<Greeter.Greet> greeter;
public static Behavior<SayHello> create() {
return Behaviors.setup(GreeterMain::new);
}
private GreeterMain(ActorContext<SayHello> context) {
super(context);
//#create-actors
greeter = context.spawn(Greeter.create(), "greeter");
//#create-actors
}
@Override
public Receive<SayHello> createReceive() {
return newReceiveBuilder().onMessage(SayHello.class, this::onSayHello).build();
}
private Behavior<SayHello> onSayHello(SayHello command) {
//#create-actors
ActorRef<Greeter.Greeted> replyTo =
getContext().spawn(GreeterBot.create(3), command.name);
greeter.tell(new Greeter.Greet(command.name, replyTo));
//#create-actors
return this;
}
}
AkkaQuickstart.java
package $package$;
import akka.actor.typed.ActorSystem;
import java.io.IOException;
public class AkkaQuickstart {
public static void main(String[] args) {
//#actor-system
final ActorSystem<GreeterMain.SayHello> greeterMain = ActorSystem.create(GreeterMain.create(), "helloakka");
//#actor-system
//#main-send-messages
greeterMain.tell(new GreeterMain.SayHello("Charles"));
//#main-send-messages
try {
System.out.println(">>> Press ENTER to exit <<<");
System.in.read();
} catch (IOException ignored) {
} finally {
greeterMain.terminate();
}
}
}
作为另一个最佳实践,我们应该提供一些测试覆盖率。
测试Actor
Hello World 示例中的测试说明了 JUnit 框架的使用。 测试覆盖率不完整。 它展示了如何测试Actor代码并提供了一些基本概念。
package $package$;
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
import akka.actor.testkit.typed.javadsl.TestProbe;
import akka.actor.typed.ActorRef;
import org.junit.ClassRule;
import org.junit.Test;
//#definition
public class AkkaQuickstartTest {
@ClassRule
public static final TestKitJunitResource testKit = new TestKitJunitResource();
//#definition
//#test
@Test
public void testGreeterActorSendingOfGreeting() {
TestProbe<Greeter.Greeted> testProbe = testKit.createTestProbe();
ActorRef<Greeter.Greet> underTest = testKit.spawn(Greeter.create(), "greeter");
underTest.tell(new Greeter.Greet("Charles", testProbe.getRef()));
testProbe.expectMessage(new Greeter.Greeted("Charles", underTest));
}
//#test
}
定义测试类
public class AkkaQuickstartTest {
@ClassRule
public static final TestKitJunitResource testKit = new TestKitJunitResource();
使用 TestKitJunitResource JUnit 规则包括对 JUnit 的支持。 这会自动创建并清理一个 ActorTestKit。 要查看如何使用测试套件,请直接查看完整文档。
测试方法
此测试使用 TestProbe 来询问和验证预期行为。 让我们看一个源代码片段:
@Test
public void testGreeterActorSendingOfGreeting() {
TestProbe<Greeter.Greeted> testProbe = testKit.createTestProbe();
ActorRef<Greeter.Greet> underTest = testKit.spawn(Greeter.create(), "greeter");
underTest.tell(new Greeter.Greet("Charles", testProbe.getRef()));
testProbe.expectMessage(new Greeter.Greeted("Charles", underTest));
}
一旦我们有了对 TestProbe 的引用,我们就将它作为 Greet
消息的一部分传递给 Greeter
。 然后,我们验证 Greeter
是否响应已发生问候。
全部测试代码
以下是全部源码:
package $package$;
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
import akka.actor.testkit.typed.javadsl.TestProbe;
import akka.actor.typed.ActorRef;
import org.junit.ClassRule;
import org.junit.Test;
//#definition
public class AkkaQuickstartTest {
@ClassRule
public static final TestKitJunitResource testKit = new TestKitJunitResource();
//#definition
//#test
@Test
public void testGreeterActorSendingOfGreeting() {
TestProbe<Greeter.Greeted> testProbe = testKit.createTestProbe();
ActorRef<Greeter.Greet> underTest = testKit.spawn(Greeter.create(), "greeter");
underTest.tell(new Greeter.Greet("Charles", testProbe.getRef()));
testProbe.expectMessage(new Greeter.Greeted("Charles", underTest));
}
//#test
}
示例代码只是触及ActorTestKit
中的一些表面功能。 可以在此处找到完整的概述。