从 Classic 迁移到 Akka Typed:核心 API 差异对照与实战指南

原创2026-09-24 02:21:2043 阅读
文章标签:后端并发编程异步编程

从 Classic 迁移到 Akka Typed:核心 API 差异对照与实战指南

本指南以 Akka 官方文档 from-classic.md 为主体,面向已经熟悉 Classic Actor API 的开发者,系统梳理 Typed Actor API 在依赖、包名、行为定义、监督、生命周期、消息交互与测试等方面的差异。读完本文,你将掌握两种 API 的核心概念对应关系、Typed 中每个经典写法的替代方案,并能结合仓库源码理解 BehaviorActorContext 等底层机制,顺利完成既有 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.scaladsltyped.javadslscaladsljavadsl 是区分 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 类到 BehaviorAbstractBehavior

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.scalaClassicSample.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.scalaTypedSample.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 通过 ActorContextActorSystemactorOf 方法启动 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.scaladef setupT: Behavior[T],它保证每次 actor 实例(包括重启后的新实例)创建时都会执行一次工厂逻辑。
  • Classic 的 actorOfname 参数可选,缺省时生成随机名字;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 创建 actoractors.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 ActorSystemactorOf 通常用来创建若干个顶层 actor;Typed ActorSystem 没有该能力,actor 都作为 guardian 或层级中其他 actor 的子 actor 启动。这样设计部分出于一致性考虑:在 Typed 系统中,你不能从应用的任何位置向任意 actor 创建子 actor(必须通过消息交互),那么对 guardian 也应当如此。如果确实需要在 guardian 之外按需生成 actor,可以使用 SpawnProtocol(详见 actor-lifecycle.md 的 SpawnProtocol 一节)。

become:返回新 Behavior 取代热切换

Classic actor 通过 ActorContextbecome 改变消息处理逻辑;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 与 senderactors.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 源码为准,其语义为:

  • minBackoffmaxBackoff 之间的指数退避,例如 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 提供可重写的 preStartpreRestartpostRestartpostStop 四个生命周期钩子。

Typed 以对应的 PreRestartPostStop **信号消息(signal)**支持这些能力。Typed 没有 PreStartPostRestart 信号,因为这类初始化动作可以直接在 Behaviors.setup 中或 AbstractBehavior 的构造函数里完成。

另一个容易踩坑的差异:Classic 中 postStop 钩子在 actor 被重启时也会被调用;Typed 中则不会,重启时只会发出 PreRestart 信号。如果需要在重启和停止两种情况下都做资源清理,就必须同时处理 PreRestartPostStop 两个信号。

参考文档:Classic start hookactors.md);Typed 见 fault-tolerance.md 的 PreRestart 信号

watch:死亡监视与 watchWith

watchTerminated 消息在两种 API 中基本相同,但 Typed 增加了一些能力:

  • 在 Typed 中,Terminated 是一个信号(signal),因为它与 Behavior 声明的消息类型不同,属于另一套类型体系。
  • Typed 的 ActorContext.watchWith 可以在被监视 actor 终止时发送一条自定义消息,而不是发出 Terminated 信号。签名见 ActorContext.scaladef watchWithU: Unit
  • 监视子 actor 时,可以通过 ChildFailed 信号(Terminated 的子类)区分子 actor 是主动终止还是因失败而终止

参考文档:Classic 死亡监视actors.md);Typed 见 actor-lifecycle.md 的 Watching Actors

Stopping:Behaviors.stopped 与消失的 PoisonPill

  • Classic 通过 ActorContextActorSystemstop 方法停止 actor。Typed 中 actor 通过返回 Behaviors.stopped 停止自己Behaviors.scala);ActorContext 也有 stop 方法,但只能用于停止直接子 actor,不能停止任意 actor。
  • Typed 不支持 PoisonPill。如果需要请求某个 actor 停止,应当定义一条该 actor 能理解的消息,让它在收到消息后返回 Behaviors.stopped

参考文档:Classic 停止 actoractors.md);Typed 见 actor-lifecycle.md 的 Stopping Actors

ActorSelection:用 Receptionist 取代路径查找

Typed 不支持 ActorSelection。需要通过某种 key 查找 actor 时,应该使用 Receptionist 进行注册与发现,详见 actor-discovery.md

ActorSelection 的用途是在没有目标 ActorRef 时按路径发送消息;在 Typed 中,Group Router 可以承担类似职责。

参考文档:Classic ActorSelectionactors.md)。

ask:请求-响应模式的两条路径

Classic 的 ask 模式返回 Future(Scala)或 CompletionStage(Java)。

Typed 中同样存在 ask,位于 akka.actor.typed.scaladsl.AskPattern / akka.actor.typed.javadsl.AskPattern适合请求方本身不是 actor 的场景

当请求方本身是 actor 时,更好的做法是使用 Typed ActorContextask 方法:它的优势在于不必把在不同线程上运行的 Future/CompletionStage 回调混入 actor 代码,从而避免经典的线程模型陷阱。

参考文档:Classic askactors.md);Typed 的 ask 交互模式见 interaction-patterns.md

pipeTo:pipeToSelf 取代外部管道

pipeTo 通常与 ask 配合使用。Typed 的 ActorContext.ask 已经消除了对 pipeTo 的需求。不过,当与其他返回 Future/CompletionStage 的 API 交互时,仍然需要把异步结果作为消息发送给 actor——为此 Typed 提供了 ActorContextpipeToSelf 方法。

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(内含当前 Receiveself)"这一对组合。
  • 新 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 类型已知时对 ActorRefunsafeUpcast(可视为"足够安全")。

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 Routingrouting.md);Typed 见 routers.md

FSM:用 Behavior 天然表达有限状态机

Classic 为构建有限状态机(FSM)提供了专门支持;Akka Typed 不需要专门的 FSM 支持——因为用 Behavior 表达 FSM 是自然而直接的:每个状态就是一个 Behavior,状态转移就是返回新的 Behavior(这与 become 一节的机制完全一致)。

参考文档:Classic FSMfsm.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 timersactors.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 stashactors.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 Persistencepersistence.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。

登录后查看全文
akka-core