前面的章节我们讨论过,Clojure Actor 的主要意义在于可以用 clojure 的 multimethod 编写 actor 的消息响应逻辑,并且可以向各个 handler 注入 clojure 代码。那么如何管理 actor 中的状态呢?
如果一个项目本身就有比较多的 Java 逻辑,那么再多实现几个带有自定义字段的 Java 类,例如就简单的派生 ClojureActor 类型,都是不难实现的。但是如果我们只是想快速的写一段 clojure 代码,这样做并不经济。
最后我决定为 Clojure Actor 加入一个字段解决这个问题,这个字段应该易于clojure 代码使用,能够修改数据状态,那么我想到的是,增加一个 agent 对象。
Agent 是 clojure 中几个内置的“引用”类型之一。它允许我们用 send/send-off 向其发送函数,并将函数的返回值作为自己的新状态保存,这个过程通过一个任务队列确保并发安全。Agent 也允许我们用 @ ——即 deref 函数 —— 获取状态。
我们扩展一下 ClojureActor 的定义,加入一个 Agent 字段:
public class ClojureActor extends AbstractActor {
static private String actor_namespace = "liu.mars.actor";
static {
CR.require(actor_namespace);
}
private Agent state;
...
为了简化代码,我们有一些用 Clojure 封装的状态操作,这些代码通过上例中的 require 操作引入,例如构造一个空状态的 new-state :
(nsliu.mars.actor)
(defnnew-state
[]
(agent{}))
(defnderef-state
[state]
@state)
(defnget-state
[state k]
(get@state k))
(defnget-state-in [state path]
(get-in @state path))
CR (Clojure Runtime)工具类来自我的工具库 jaskell [MarchLiu/jaskell] 。用来简化一些琐碎的操作。
其它几个函数,主要针对一些在 Java 里面直接写不方便,但是可能会用到的操作,便于将来使用。
一开始,我把这几个操作封装成了工具方法,但是在写测试的时候我发现,如果想要覆盖 agent 丰富的功能,要封装的 Java 代码太多,但是收益并不大,我预期这个类型还是主要用于 clojure 用户,那么只要能够在 clojure 代码中引用到这个 agent 即可,这只需要一个 get 方法:
...
public Agent getState() {
return state;
}
}
相应的,构造 actor 的 props 函数也做了扩展:
public static Props props(MultiFn fn){
return Props.create(ClojureActor.class, () -> {
var result = new ClojureActor(fn);
result.state = (Agent) CR.invoke(actor_namespace, "new-state");
return result;
});
}
public static Props propsWithInit(IFn initiator, MultiFn fn){
return Props.create(ClojureActor.class, () -> {
var result = new ClojureActor(fn);
result.state = (Agent) CR.invoke(actor_namespace, "new-state");
initiator.invoke(result);
return result;
});
}
public static Props propsWithStateInit(IFn initiator, Agent state, MultiFn fn){
return Props.create(ClojureActor.class, () -> {
var result = new ClojureActor(fn);
result.state = (Agent) state;
initiator.invoke(result);
return result;
});
}
这三个静态构造函数分别对应:
- 默认构造时只需要给出消息响应逻辑,程序调用 new-state 构造一个空字典作为初始状态 `(agent {})`
- 用户可以在 initiator 里给出初始化逻辑
- 如果我们需要更复杂的设定,例如指定 agent 的校验函数,那么我们可以在 clojure 里构造好 agent,通过 propsWithStateInit 函数指定给构造逻辑。
通过下面这段测试代码,我们可以看到用 clojure 维护数据状态的过程。虽然每次都要 (.getState this) 确实不够简练,但是我们在生产环境里,编写复杂的程序逻辑时,通过 doto 、let 或箭头宏 -> 、 ->> 等技巧,完全可以把它写的轻巧一些。
(nsliu.mars.state-actor-test
(:require [clojure.test :refer :all])
(:import (akka.actor ActorSystem)
(liu.mars ClojureActor)
(akka.testkit.javadsl TestKit)
(java.util.function Supplier Function)))
(defmultireceiver "receive matchers for basic workflow"
(fn[_ message]
(:order message)))
(defmethodreceiver :get [this message]
(println(str"receive a get message " message " from " this))
(.tell (.getSender this) (get@(.getState this) (:key message)) (.getSelf this)))
(defmethodreceiver :get-in [this message]
(println(str"receive a get in message " message " from " this))
(.tell (.getSender this) (get-in @(.getState this) (:path message)) (.getSelf this)))
(defmethodreceiver :post [this message]
(let[fun (:function message)]
(println(str"receive a post order " fun " : " (classfun)))
(send(.getState this) fun)))
(testing "tests for clojure state actor by creator"
(let[system (ActorSystem/create "test")
test-kit (TestKit. system)
await#(.awaitCond test-kit (reify Supplier (get[this] (.msgAvailable test-kit))))
self (.getRef test-kit)
text-message "a text message save in state"
runtime-message "this data post in runtime"
initiator (fn[actor]
(dotoactor
(.setPreStart #(println(str% " is going to start")))
(.setPostStop #(println(str% " stopped"))))
(send(.getState actor) #(assoc% :data text-message)))
actor (.actorOf system (ClojureActor/propsWithInit initiator receiver))]
(try
(.tell actor {:order :get :key :data} self)
(await)
(.expectMsgPF test-kit "check get messsage" (reify Function
(apply[this message]
(is (=text-message message)))))
(.tell actor {:order :post :function #(assoc% :post-data runtime-message)} self)
(.tell actor {:order :get-in :path [:post-data]} self)
(await)
(.expectMsgPF test-kit "check get in" (reify Function
(apply[this message]
(is (=runtime-message message)))))
(.stop system actor)
(finally
(TestKit/shutdownActorSystem system)))))
这部分代码已经上传到 [MarchLiu/akka-clojure] 并发布为 `[liu.mars/akka-clojure "0.1.1"] ` 。