这几天在家读了一下 Akka Typed 的文档,发现还是挺有意思的。
这个体系已经是 Akka 官方推荐的默认风格,现在打开 Akka 官网的最新版 Akka 文档(2.6.0),入门教程就是 typed 。以前的 Actor 风格被称为 classic ,仍然支持。
Typed 引入了新的 Actor 类型,称为 Behavior。相对于 classic Actor, 有几个明显的变化:
* 简化了 receiver 的封装,Java API 不再要求 Receiver 是一个 PartialFunction ,只要能返回一个 Behavior 类型即可(当然,仍然可以用 Match Class 风格的消息分派封装方法)。
* 鼓励显式的将状态和行为分离,将状态只读,状态变化就构造一个新的状态,然后用新状态构造一个新的 Behavior 对象。Behavior 本身是泛型化的,对于 Java/Scala ,更有利于构造严谨的项目。
相对于 Classic Actor,Behavior 的构造风格更简明直接。
这些变化,很多都不是硬门槛,而是以前就存在于 Akka 体系中的,但是现在 typed 的设计,促使它将这些风格(大多是明显的函数式编程倾向)突出出来。
而这些风格变化,除了强化类型约束,其它对 Clojure 都是有利的。甚至可以说,Typed Behavior 的状态管理,天然就符合 Clojure 的编程风格。
另一方面,扔掉 typed 部分,至少对于 Clojure 这部分没有什么损失,因为 JVM 的擦拭法泛型,实际上在运行时,所有的类型信息都扔掉了。
我尝试对这部分代码做了简单的封装:
(defnbehavior
([state receiver]
(StateActor/create state receiver))
([receiver]
(Actor/create receiver)))
这个函数根据是否需要携带状态,分别调用了两个 Java 封装,它们负责将 Clojure 函数封装为 Behavior 对象。这部分实现稍后我们进行讨论。
构造 Typed System 的时候,我们要给它设定默认的 Behavior ,如下面这个测试:
(deftest basic-test
"tests for basic actor workflow"
(let[test-kit (ActorTestKit/create)
probe (.createTestProbe test-kit)
actor (spawn test-kit (behavior
(fnreceiver [^ActorContext context ^Map message]
(case (:category message)
:inc
(! (:sender message) (->message :number inc))
:dec
(! (:sender message) (->message :number dec))
(! (:sender message) (->message :number)))
same)))
self (.ref probe)]
(! actor {:category :inc :number 100 :sender self})
(.expectMessage probe 101)
(! actor {:category :dec :number 100 :sender self})
(.expectMessage probe 99)
(! actor {:category :save :number 100 :sender self})
(.expectMessage probe 100)
(.shutdownTestKit test-kit)))
Akka Context 提供 spawn 方法,将给定的 behavior 封装为 ActorRef ,System 和 actor 都可以使用同样的 spawn 操作。经过封装,这个过程已经非常接近 Scala 的版本。
(defnspawn
([context ^Behavior behavior]
(.spawn context behavior))
([context ^Behavior behavior ^String name]
(.spawn context behavior name)))
当然,我们也可以进一步合并 System和Behavior 的定义,形如:
(defnsystem
([receiver name]
(ActorSystem/create (behavior receiver) name))
([state receiver name]
(ActorSystem/create (behavior state receiver) name)))
同样,我们可以用这样的函数简化 spawn 操作:
(defnspa
[context receiver & options]
(let[{:keys [namestate]} options
behavior (ifstate
(behavior state receiver)
(behavior receiver))]
(ifname
(spawn context behavior name)
(spawn context behavior))))
这个 spa 不是指的 spa,而是 spawn 的简化。形如下面的测试逻辑,我们可以将这个操作简化到:
(deftest spa-test
"tests for basic actor workflow using spa"
(let[test-kit (ActorTestKit/create)
probe (.createTestProbe test-kit)
actor (spa test-kit
#(let[sender (:sender %2)
n (:number %2)]
(case (:category %2)
:inc (! sender (incn))
:dec (! sender (decn))
(! sender n))
same))
self (.ref probe)]
(! actor {:category :inc :number 100 :sender self})
(.expectMessage probe 101)
(! actor {:category :dec :number 100 :sender self})
(.expectMessage probe 99)
(! actor {:category :save :number 100 :sender self})
(.expectMessage probe 100)
(.shutdownTestKit test-kit)))
这里有一个细节,就是我们通过关键字参数 :name 和 :state 分别传入 behavior 的名字和状态。根据参数组合, spa 函数执行不同的构造逻辑。而在 Scala 中,我们可以通过 implicits 实现更精细的自动转换。
我在封装 typed 的时候,主要考虑在损失类型检查之后,尽可能的发挥 Clojure 的便利,如果需要更严谨的工程控制,直接使用 Scala 版本应该是更好的选择。
因此,无状态的 typed actor ,被我封装为:
public class Actor extends AbstractBehavior {
private IFn receiver;
private Actor(ActorContext context, IFn receiver) {
super(context);
this.receiver = receiver;
}
public static Behavior create(IFn receiver) {
return Behaviors.setup(context -> new Actor(context, receiver));
}
private Behavior onMessage(Map message) {
return (Behavior)receiver.invoke(getContext(), message);
}
@Override
public Receive createReceive() {
return newReceiveBuilder().onMessage(Map.class, this::onMessage).build();
}
}
有状态的 typed actor ,也没有很复杂:
public class StateActor extends AbstractBehavior {
private Map state;
private IFn receiver;
private StateActor(ActorContext context, Map state, IFn receiver) {
super(context);
this.state = state;
this.receiver = receiver;
}
public static Behavior create(Map state, IFn receiver) {
return Behaviors.setup(context -> new StateActor(context, state, receiver));
}
private Behavior onMessage(Object message) {
return (Behavior)receiver.invoke(getContext(), state, message);
}
@Override
public Receive createReceive() {
return newReceiveBuilder().onAnyMessage(this::onMessage).build();
}
}
对于 akka typed actor 的 receiver ,我们要求它的返回值必须是 behavior,要么是携带业务逻辑的 behavior ,要么是 same(表示没有状态变化)或 stopped 这类“系统消息”。
我也封装了一些 typed 的功能性 api,例如 Behaviors 类型的 receive 和setup 方法。但是这里并没有追求一步到位,完整的覆盖所有功能,我接下来准备尝试编写一些基于 akka typed 的例子,在使用中探索下一步的开发方向。
本文中展示的代码,我发布为 akka-typed-clojure 项目,地址在:https://github.com/MarchLiu/akka-typed-clojure 。这个项目还处于很初级的阶段,估计要写几个 Akka 应用之后,才会逐步稳定。