从 Classic 迁移到 Akka Typed:核心 API 差异对照与实战指南
从 Classic 迁移到 Akka Typed:核心 API 差异对照与实战指南
本指南以 Akka 官方文档 from-classic.md 为主体,面向已经熟悉 Classic Actor API 的开发者,系统梳理 Typed Actor API 在依赖、包名、行为定义、监督、生命周期、消息交互与测试等方面的差异。读完本文,你将掌握两种 API 的核心概念对应关系、Typed 中每个经典写法的替代方案,并能结合仓库源码理解 Behavior、ActorContext 等底层机制,顺利完成既有 Classic 代码的渐进式迁移。
引言:为什么会有两套 Actor API
Akka Classic 是最初的 Actor API,而 Akka Typed 是在其基础上发展出的更类型安全、更具引导性的 Actor API。两者概念大多相同,本文重点突出差异——即在 Typed 中"某件事应该怎么做"。
建议在深入差异之前,先通过 IoT 示例(Getting Started Guide)或 Actor 介绍 中的示例了解 Akka Typed 的基本形态。
需要注意的是,Akka Classic 仍然被完整支持,现有应用可以继续使用经典 API;Typed 与 Classic 也可以在同一个 ActorSystem 中共存,详见 coexisting.md。对于新项目,官方推荐使用新的 Actor API(Typed)。
Dependencies:Typed 模块的依赖命名约定
Typed 模块的依赖命名规则是:在对应 Classic 模块名后追加 -typed 后缀,仅有少量例外。例如引入 akka-cluster-typed:
// sbt
libraryDependencies += "com.typesafe.akka" %% "akka-cluster-typed" % AkkaVersion
<!-- Maven -->
<properties>
<akka.version>请替换为实际使用的 Akka 版本</akka.version>
</properties>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-cluster-typed_2.13</artifactId>
<version>${akka.version}</version>
</dependency>
// Gradle
def akkaVersion = "请替换为实际使用的 Akka 版本"
dependencies {
implementation "com.typesafe.akka:akka-cluster-typed_2.13:${akkaVersion}"
}
(AkkaVersion 占位符对应文档中的 $akka.version$,实际使用时应替换为项目采用的 Akka 版本,建议通过 akka-bom 统一管理版本。)
Artifact 名称对照表:
| Classic | Typed |
|---|---|
| akka-actor | akka-actor-typed |
| akka-cluster | akka-cluster-typed |
| akka-cluster-sharding | akka-cluster-sharding-typed |
| akka-cluster-tools | akka-cluster-typed |
| akka-distributed-data | akka-cluster-typed |
| akka-persistence | akka-persistence-typed |
| akka-stream | akka-stream-typed |
| akka-testkit | akka-actor-testkit-typed |
注意两点例外:Cluster Singleton 和 Distributed Data 被包含在 akka-cluster-typed 中;上表未列出的 Artifact 没有针对 Akka Typed 的专用 API。
Package names:包名约定
Akka Typed 的包名约定是在对应的 Classic 包名基础上追加 typed.scaladsl 或 typed.javadsl。scaladsl 与 javadsl 是区分 Scala/Java API 的惯例,这与 Akka Streams 的约定一致。
| Classic | Typed for Scala | Typed for Java |
|---|---|---|
| akka.actor | akka.actor.typed.scaladsl | akka.actor.typed.javadsl |
| akka.cluster | akka.cluster.typed | akka.cluster.typed |
| akka.cluster.sharding | akka.cluster.sharding.typed.scaladsl | akka.cluster.sharding.typed.javadsl |
| akka.persistence | akka.persistence.typed.scaladsl | akka.persistence.typed.javadsl |
Actor definition:从 Actor 类到 Behavior 与 AbstractBehavior
Classic actor 通过继承 Scala 的 akka.actor.Actor 或 Java 的 akka.actor.AbstractActor 定义;Typed actor 则通过继承 akka.actor.typed.scaladsl.AbstractBehavior(Scala)或 akka.actor.typed.javadsl.AbstractBehavior(Java)定义。此外,Typed 还支持不继承类、而用函数定义 actor 的"函数式风格",详见 style-guide.md。
下面是 Classic 的 HelloWorld actor(完整样例见 ClassicSample.scala 与 ClassicSample.java):
// Scala
import akka.actor.Actor
import akka.actor.ActorLogging
import akka.actor.Props
object HelloWorld {
final case class Greet(whom: String)
final case class Greeted(whom: String)
def props(): Props = Props(new HelloWorld)
}
class HelloWorld extends Actor with ActorLogging {
import HelloWorld._
override def receive: Receive = {
case Greet(whom) =>
log.info("Hello {}!", whom)
sender() ! Greeted(whom)
}
}
// Java
import akka.actor.AbstractActor;
import akka.actor.Props;
public class HelloWorld extends AbstractActor {
public static final class Greet {
public final String whom;
public Greet(String whom) { this.whom = whom; }
}
public static final class Greeted {
public final String whom;
public Greeted(String whom) { this.whom = whom; }
}
public static Props props() {
return Props.create(HelloWorld.class, HelloWorld::new);
}
@Override
public Receive createReceive() {
return receiveBuilder().match(Greet.class, this::onGreet).build();
}
private void onGreet(Greet command) {
log.info("Hello {}!", command.whom);
getSender().tell(new Greeted(command.whom), getSelf());
}
}
对应的 Typed HelloWorld(完整样例见 TypedSample.scala 与 TypedSample.java):
// Scala
import akka.actor.typed.ActorRef
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.AbstractBehavior
import akka.actor.typed.scaladsl.ActorContext
import akka.actor.typed.scaladsl.Behaviors
object HelloWorld {
final case class Greet(whom: String, replyTo: ActorRef[Greeted])
final case class Greeted(whom: String, from: ActorRef[Greet])
def apply(): Behavior[HelloWorld.Greet] =
Behaviors.setup(context => new HelloWorld(context))
}
class HelloWorld(context: ActorContext[HelloWorld.Greet])
extends AbstractBehaviorHelloWorld.Greet {
import HelloWorld._
override def onMessage(message: Greet): Behavior[Greet] = {
context.log.info("Hello {}!", message.whom)
message.replyTo ! Greeted(message.whom, context.self)
this
}
}
// Java
import akka.actor.typed.ActorRef;
import akka.actor.typed.Behavior;
import akka.actor.typed.javadsl.AbstractBehavior;
import akka.actor.typed.javadsl.ActorContext;
import akka.actor.typed.javadsl.Behaviors;
import akka.actor.typed.javadsl.Receive;
public class HelloWorld extends AbstractBehavior<HelloWorld.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(HelloWorld::new);
}
private HelloWorld(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;
}
}
为什么叫 Behavior 而不是 Actor?
在 Typed 中,Behavior 定义的是"如何处理收到的消息"。处理完一条消息后,可以返回一个新的 Behavior 用于处理下一条消息。这意味着 actor 以一个初始 Behavior 启动,并可能在其生命周期内多次改变 Behavior(详见下文 become 一节)。
关键区别:Behavior 带有一个类型参数,描述它可以处理的消息类型。Classic actor 没有显式声明这种信息,你可以向 Classic 的 ActorRef 发送任意类型消息——即使该 actor 根本不理解。
参考文档:Classic 定义 actor 类 对应仓库路径为 actors.md;Typed 定义见 actors.md。
actorOf 与 Props:从工厂到 Behavior 直接孵化
Classic 通过 ActorContext 或 ActorSystem 的 actorOf 方法启动 actor,参数是 akka.actor.Props——它相当于创建 actor 实例的工厂,actor 被重启时也会用它创建新实例,同时 Props 还可以指定 dispatcher 等附加属性。
Typed 中对应的方法叫 spawn,位于 akka.actor.typed.scaladsl.ActorContext / akka.actor.typed.javadsl.ActorContext。以仓库源码 ActorContext.scala 为例:
def spawnU: ActorRef[U]
差异要点:
spawn直接从一个Behavior创建 actor,不经过Props工厂,但可以接受一个可选的akka.actor.typed.Props来指定 Actor 元数据(如 dispatcher)。- "工厂"的职责在 Typed 中由
Behaviors.setup承担(对象式风格中配合继承AbstractBehavior使用);函数式风格通常不需要工厂。Behaviors.setup的签名见 Behaviors.scala:def setupT: Behavior[T],它保证每次 actor 实例(包括重启后的新实例)创建时都会执行一次工厂逻辑。 - Classic 的
actorOf的name参数可选,缺省时生成随机名字;Typed 中对应spawnAnonymous方法(ActorContext.scala)。
需要特别注意的是:Typed 的 ActorSystem 没有 spawn 方法用于创建顶层 actor。系统启动时只需给定一个用户 guardian Behavior,它就是唯一的顶层 actor;其余 actor 都作为 guardian 的子 actor 或层级中的其他子 actor 启动(详见下一节 ActorSystem](#actorsystem))。当混用 Classic 与 Typed 且持有 Classic 系统时,仍然可以从外部生成顶层 Typed actor,参见 [coexisting.md 的顶层 Typed actor 一节。
参考文档:Classic 用 Props 创建 actor(actors.md);Typed 见 actor-lifecycle.md。
ActorRef:类型参数带来的编译期约束
akka.actor.ActorRef 在 Typed 中的对应物是 akka.actor.typed.ActorRef。核心差异仍然是类型参数:Typed 的 ActorRef[T] 声明了该 actor 能处理的消息类型,向错误类型发送消息会在编译期被拦截;Classic 的 ActorRef 没有类型信息,可以发送任意消息——即使 actor 无法理解。
ActorSystem:用户 guardian 与顶层 actor 的组织方式
akka.actor.ActorSystem 在 Typed 中的对应物是 akka.actor.typed.ActorSystem。一个显著差异是:创建 Typed ActorSystem 时需要提供一个 Behavior,它会被用作顶层 actor,即用户 guardian(user guardian)。
- Classic 应用中,顶层 actor 的创建与 Cluster Sharding 等组件的初始化通常在 ActorSystem "外部"进行(例如 main 方法中调用
actorOf)。 - Typed 应用中,这些初始化工作被放入 guardian 的
Behavior中,由 guardian 负责创建子 actor 并初始化 Cluster Sharding 等 Akka 组件。
Classic ActorSystem 的 actorOf 通常用来创建若干个顶层 actor;Typed ActorSystem 没有该能力,actor 都作为 guardian 或层级中其他 actor 的子 actor 启动。这样设计部分出于一致性考虑:在 Typed 系统中,你不能从应用的任何位置向任意 actor 创建子 actor(必须通过消息交互),那么对 guardian 也应当如此。如果确实需要在 guardian 之外按需生成 actor,可以使用 SpawnProtocol(详见 actor-lifecycle.md 的 SpawnProtocol 一节)。
become:返回新 Behavior 取代热切换
Classic actor 通过 ActorContext 的 become 改变消息处理逻辑;Typed 中则是在处理完一条消息后返回一个新的 Behavior,该返回的 Behavior 会被用于处理下一条消息。
一个重要的差异:Typed 中没有与 unbecome 对应的机制。如果你想"回退"到之前的处理逻辑,必须自己显式地保存并返回"上一个" Behavior。
参考文档:Classic 的热切换(Actor HotSwap)(actors.md)。
sender:没有隐式 sender,回复目标显式放进消息
Typed 中没有 sender() / getSender()。你必须把代表"回复目标"(而不是传统意义的 sender)的 ActorRef 显式放进消息里。
设计原因:如果 Typed 保留隐式 sender,编译器无法在编译期得知 sender 的 ActorRef[T] 类型;而且把回复目标显式写进消息协议,会让协议意图更加清晰。这正是前面 HelloWorld 例子中 Greet(whom, replyTo) 包含 replyTo: ActorRef[Greeted] 字段的原因。
参考文档:Classic 的 tell 与 sender(actors.md);Typed 的 request-response 模式见 interaction-patterns.md。
parent:没有隐式 parent,显式传入父 actor 引用
Typed 中没有 parent / getParent。父 actor 的 ActorRef 需要作为参数在构造 Behavior 时显式传入。
设计原因:如果不为 Behavior 增加额外的类型参数,编译器同样无法在编译期得知 parent 的 ActorRef[T] 类型;此外,显式传入 parent 也有利于测试——测试时可以替换为 probe 或将其 stub 掉。
Supervision:默认"停止"取代默认"重启"
Classic 与 Typed 在监督策略上有一个非常重要的默认差异:
- Typed:默认情况下,如果抛出异常且未定义任何监督策略,actor 会被停止。
- Classic:默认情况下,actor 会被重启。
Classic 中,子 actor 的监督策略通过在父 actor 中重写 supervisorStrategy 方法定义;Typed 中则通过 Behaviors.supervise 包装子 actor 的 Behavior 来定义,其签名见 Behaviors.scala:
def superviseT: Supervise[T]
典型用法:
Behaviors.supervise(childBehavior)
.onFailureIllegalStateException
Classic 的 BackoffSupervisor 在 Typed 中由 SupervisorStrategy.restartWithBackoff 以普通 SupervisorStrategy 的形式支持。以 SupervisorStrategy.scala 源码为准,其语义为:
minBackoff与maxBackoff之间的指数退避,例如minBackoff为 3 秒、maxBackoff为 30 秒时,启动尝试将依次延迟 3、6、12、24、30、30 秒;- 额外的随机延迟由
randomFactor控制(如0.2表示最多额外 20% 延迟),用于避免所有失败 actor 同时冲击后端资源,传入0可关闭; - 退避期间收到的消息会被丢弃;
- 如果在
(minBackoff + maxBackoff) / 2时间内没有新的异常,指数退避计时会被重置(可用withResetBackoffAfter覆盖); - 可通过
withMaxRestarts限制最大重启次数。
SupervisorStrategy.Escalate 在 Typed 中不被支持,但可以通过"让失败向上层冒泡"的方式达到类似效果,详见 fault-tolerance.md 的 Bubble failures up through the hierarchy 一节。
参考文档:Classic 容错(fault-tolerance.md)与 Typed 容错。
Lifecycle hooks:信号(Signal)取代生命周期钩子方法
Classic actor 提供可重写的 preStart、preRestart、postRestart、postStop 四个生命周期钩子。
Typed 以对应的 PreRestart 与 PostStop **信号消息(signal)**支持这些能力。Typed 没有 PreStart 和 PostRestart 信号,因为这类初始化动作可以直接在 Behaviors.setup 中或 AbstractBehavior 的构造函数里完成。
另一个容易踩坑的差异:Classic 中 postStop 钩子在 actor 被重启时也会被调用;Typed 中则不会,重启时只会发出 PreRestart 信号。如果需要在重启和停止两种情况下都做资源清理,就必须同时处理 PreRestart 和 PostStop 两个信号。
参考文档:Classic start hook(actors.md);Typed 见 fault-tolerance.md 的 PreRestart 信号。
watch:死亡监视与 watchWith
watch 与 Terminated 消息在两种 API 中基本相同,但 Typed 增加了一些能力:
- 在 Typed 中,
Terminated是一个信号(signal),因为它与Behavior声明的消息类型不同,属于另一套类型体系。 - Typed 的
ActorContext.watchWith可以在被监视 actor 终止时发送一条自定义消息,而不是发出Terminated信号。签名见 ActorContext.scala:def watchWithU: Unit。 - 监视子 actor 时,可以通过
ChildFailed信号(Terminated的子类)区分子 actor 是主动终止还是因失败而终止。
参考文档:Classic 死亡监视(actors.md);Typed 见 actor-lifecycle.md 的 Watching Actors。
Stopping:Behaviors.stopped 与消失的 PoisonPill
- Classic 通过
ActorContext或ActorSystem的stop方法停止 actor。Typed 中 actor 通过返回Behaviors.stopped停止自己(Behaviors.scala);ActorContext也有stop方法,但只能用于停止直接子 actor,不能停止任意 actor。 - Typed 不支持
PoisonPill。如果需要请求某个 actor 停止,应当定义一条该 actor 能理解的消息,让它在收到消息后返回Behaviors.stopped。
参考文档:Classic 停止 actor(actors.md);Typed 见 actor-lifecycle.md 的 Stopping Actors。
ActorSelection:用 Receptionist 取代路径查找
Typed 不支持 ActorSelection。需要通过某种 key 查找 actor 时,应该使用 Receptionist 进行注册与发现,详见 actor-discovery.md。
ActorSelection 的用途是在没有目标 ActorRef 时按路径发送消息;在 Typed 中,Group Router 可以承担类似职责。
参考文档:Classic ActorSelection(actors.md)。
ask:请求-响应模式的两条路径
Classic 的 ask 模式返回 Future(Scala)或 CompletionStage(Java)。
Typed 中同样存在 ask,位于 akka.actor.typed.scaladsl.AskPattern / akka.actor.typed.javadsl.AskPattern,适合请求方本身不是 actor 的场景。
当请求方本身是 actor 时,更好的做法是使用 Typed ActorContext 的 ask 方法:它的优势在于不必把在不同线程上运行的 Future/CompletionStage 回调混入 actor 代码,从而避免经典的线程模型陷阱。
参考文档:Classic ask(actors.md);Typed 的 ask 交互模式见 interaction-patterns.md。
pipeTo:pipeToSelf 取代外部管道
pipeTo 通常与 ask 配合使用。Typed 的 ActorContext.ask 已经消除了对 pipeTo 的需求。不过,当与其他返回 Future/CompletionStage 的 API 交互时,仍然需要把异步结果作为消息发送给 actor——为此 Typed 提供了 ActorContext 的 pipeToSelf 方法。
ActorContext:actor 的"内视角"
ActorContext 与 actor 实例始终是 1:1 关系:actor 启动后 context 即存在(即使你不访问它),actor 停止并被 GC 后 context 也随之消失;多个嵌套的 setup 块访问的是同一个 context,不会产生问题。
可以把 ActorContext 理解为:ActorRef 是 actor 对"外部"的化身,而 ActorContext 是 actor 对"内部"的化身。它提供与 actor 实例绑定的操作,例如生成子 actor、输出日志等——这些操作只应由 actor 当前的行为使用。
actor 的定义是:一个计算实体,在响应消息时可以:
- 向其他 actor 发送消息;
- 创建新 actor;
- 改变自身状态;
- 指定处理下一条消息时使用的行为。
在 Akka 中,这归结为一个运行中的 actor 拥有:用于处理下一条消息的当前行为、生成子 actor 的途径、以及可选的状态。
- Classic API 将其直接建模为
Actor/AbstractActor类;但运行中的 actor 实际上是"actor 类实例 + actor context(内含当前Receive与self)"这一对组合。 - 新 API 既支持用基于类的
AbstractBehavior(整个生命周期保持同一实例,状态建模为可变字段)来建模,也支持更函数式(FP)的风格:行为与状态分离,actor 响应消息时通常返回"新行为 + 新状态"的组合。此时运行中的 actor 本质上是"actor context + 当前行为"的一对组合。
从当前实现看,新的 Typed API 实际上是构建在 Classic API 之上的——spawn 一个 typed actor,底层总会 spawn 一个 classic actor。
ActorContext.children:不要用 context 做子 actor 记账
两种 API 的 ActorContext 都提供 children / child(Java 为 getChildren / getChild)方法获取已启动子 actor 的 ActorRef。
但这些方法返回的 ActorRef 类型未知(不同子 actor 可能使用不同类型),因此当目的是"向子 actor 发送消息"时,这并不是一种有用的查找方式。
推荐做法:使用应用自身的集合(bookkeeping)记录子 actor,例如 Map<a href="https://link.gitcode.com/i/70e543d053d00a51d959a5c89f538728" target="_blank">String, ActorRef[Child.Command]](Java:Map<String, ActorRef<Child.Command>>)。仓库中的完整示例见 [TypedSample.scala 与 TypedSample.java:
// Scala:用 Map 记账子 actor,配合 watchWith 自动清理
object Parent {
sealed trait Command
case class DelegateToChild(name: String, message: Child.Command) extends Command
private case class ChildTerminated(name: String) extends Command
def apply(): Behavior[Command] = {
def updated(children: Map[String, ActorRef[Child.Command]]): Behavior[Command] = {
Behaviors.receive { (context, command) =>
command match {
case DelegateToChild(name, childCommand) =>
children.get(name) match {
case Some(ref) =>
ref ! childCommand
Behaviors.same
case None =>
val ref = context.spawn(Child(), name)
context.watchWith(ref, ChildTerminated(name))
ref ! childCommand
updated(children + (name -> ref))
}
case ChildTerminated(name) =>
updated(children - name)
}
}
}
updated(Map.empty)
}
}
// Java:HashMap 记账 + watchWith 自动清理
public class Parent extends AbstractBehavior<Parent.Command> {
public interface Command {}
public static class DelegateToChild implements Command {
public final String name;
public final Child.Command message;
public DelegateToChild(String name, Child.Command message) {
this.name = name;
this.message = message;
}
}
private static class ChildTerminated implements Command {
final String name;
ChildTerminated(String name) { this.name = name; }
}
public static Behavior<Command> create() {
return Behaviors.setup(Parent::new);
}
private Map<String, ActorRef<Child.Command>> children = new HashMap<>();
private Parent(ActorContext<Command> context) {
super(context);
}
@Override
public Receive<Command> createReceive() {
return newReceiveBuilder()
.onMessage(DelegateToChild.class, this::onDelegateToChild)
.onMessage(ChildTerminated.class, this::onChildTerminated)
.build();
}
private Behavior<Command> onDelegateToChild(DelegateToChild command) {
ActorRef<Child.Command> ref = children.get(command.name);
if (ref == null) {
ref = getContext().spawn(Child.create(), command.name);
getContext().watchWith(ref, new ChildTerminated(command.name));
children.put(command.name, ref);
}
ref.tell(command.message);
return this;
}
private Behavior<Command> onChildTerminated(ChildTerminated command) {
children.remove(command.name);
return this;
}
}
要点:
- 子 actor 终止时要记得从
Map中移除对应条目;此时用watchWith很合适——可以把Map的 key 带进终止消息里,这样记账用的标识符不必与 actor 名字相同。 - 从
ActorContext取子 actor 在少数场景仍有用:检查某个子 actor 名字是否被占用、停止子 actor、以及当子 actor 类型已知时对ActorRef做unsafeUpcast(可视为"足够安全")。
Remote deployment:不支持远程部署
Typed 不支持远程部署(在远端节点上启动 actor)。官方明确不鼓励该特性,因为它经常导致节点间强耦合和不理想的故障处理——例如父 actor 所在节点崩溃时,所有远程部署的子 actor 也会随之被终止;有时这是期望行为,但很多情况下使用者并未意识到这一点。类似效果可以通过 watch 等手段实现。
Routers:简化版 Router 与 Receptionist 注册
Typed 提供 Router,但相比 Classic Router 大大简化:
- Group Router 的目的地注册在
Receptionist中,因此天然具备 Cluster 感知能力,且比 Classic group router 更动态(可以随时增减路由目标)。 - Pool Router 在 Typed 中只支持本地 actor 目的地,因为 Typed 不支持远程部署(见上一节)。
参考文档:Classic Routing(routing.md);Typed 见 routers.md。
FSM:用 Behavior 天然表达有限状态机
Classic 为构建有限状态机(FSM)提供了专门支持;Akka Typed 不需要专门的 FSM 支持——因为用 Behavior 表达 FSM 是自然而直接的:每个状态就是一个 Behavior,状态转移就是返回新的 Behavior(这与 become 一节的机制完全一致)。
参考文档:Classic FSM(fsm.md);Typed 见 fsm.md。
Timers:Behaviors.withTimers 取代 with Timers
Classic 中通过混入 with Timers(Scala)或继承 AbstractActorWithTimers(Java)获得延迟/周期调度消息的能力。Typed 中通过 Behaviors.withTimers 获得类似能力,其签名见 Behaviors.scala:
def withTimersT: Behavior[T]
参考文档:Classic actor timers(actors.md);Typed 见 interaction-patterns.md 的 Scheduling messages to self。
Stash:Behaviors.withStash 取代 with Stash
Classic 中通过混入 with Stash(Scala)或继承 AbstractActorWithStash(Java)暂存消息。Typed 中通过 Behaviors.withStash 获得类似能力,其签名见 Behaviors.scala:
def withStashT(factory: StashBuffer[T] => Behavior[T]): Behavior[T]
注意 withStash 需要显式指定 capacity(暂存容量上限),这也是 Typed 引导式设计的一个体现。
参考文档:Classic stash(actors.md);Typed 见 stash.md。
PersistentActor:EventSourcedBehavior 取代 PersistentActor
Classic PersistentActor 在 Typed 中的对应物是 akka.persistence.typed.scaladsl.EventSourcedBehavior(Scala)或 akka.persistence.typed.javadsl.EventSourcedBehavior(Java)。
Typed API 更具引导性,更有利于实践 Event Sourcing 最佳实践,并且与 Cluster Sharding 有更紧密的集成。
参考文档:Classic Persistence(persistence.md);Typed 见 persistence.md。
异步测试:Test Kit 大体相似
两种 API 的异步测试 Test Kit 较为相似,迁移成本低。
参考文档:Classic 异步集成测试(testing.md);Typed 见 testing-async.md。
同步测试:BehaviorTestKit 与确定性单元测试
Classic 与 Typed 的同步测试 Test Kit 是不同的。
Typed 的一个关键优势是:Behavior 可以不包装进 actor 而独立测试。因此测试可以完全同步运行,无需担心超时和偶发失败。
BehaviorTestKit 提供了一种确定性单元测试 Behavior 的便捷方式,但也存在一些需要了解的局限性;Classic actor 的同步测试同样有类似的局限。
参考文档:Classic 同步测试(testing.md);Typed 见 testing-sync.md 的 Synchronous Behavior Testing。
小结:迁移时的核心心法
| 关注点 | Classic 写法 | Typed 对应写法 |
|---|---|---|
| actor 定义 | 继承 Actor/AbstractActor |
继承 AbstractBehavior 或函数式 Behavior |
| 创建 actor | actorOf(props) |
context.spawn(behavior, name) / spawnAnonymous |
| 热切换 | become / unbecome |
返回新 Behavior(无 unbecome) |
| 回复 | sender() ! msg |
在消息中显式携带 ActorRef 回复目标 |
| 监督 | 重写 supervisorStrategy |
Behaviors.supervise(...).onFailure(...) |
| 生命周期 | preStart/postStop 等 |
PreRestart/PostStop 信号 + Behaviors.setup |
| 停止 | stop / PoisonPill |
返回 Behaviors.stopped(不支持 PoisonPill) |
| 查找 actor | ActorSelection |
Receptionist / Group Router |
| ask | 外部 ask + pipeTo |
AskPattern / ActorContext.ask / pipeToSelf |
| 定时 | with Timers |
Behaviors.withTimers |
| 暂存 | with Stash |
Behaviors.withStash(capacity) |
| 持久化 | PersistentActor |
EventSourcedBehavior |
两条最值得记住的心法:"一切行为变化都是返回新 Behavior"(它统一了 become、FSM、停止与状态机);"所有类型信息都要显式进入类型系统"(它统一了 sender、parent、ActorRef 的类型参数设计)。在此基础上,配合 coexisting.md 中的共存策略,就可以在同一个 ActorSystem 中渐进式地把既有 Classic 代码迁移到 Typed。