前面我们讲到了Actor的消息传递,并看到了如何发送一条fire-n-forget消息(也就是说,消息发送给Actor后我们就不管了,不从Actor那接收响应)。
技术上来讲,消息发送给Actor就是希望能有副作用的。设计上便是如此。目标Actor可以不做响应,也可以做如下两件事情——
1. 给发送方回复一条响应(在本例中,TeacherActor会将一句名言回复给StudentActor)
2. 将响应转发给其它的目标受众Actor,后者也可以进行响应/转发/产生副作用。Router和Supervisor就是这种情况。(很快我们就会看到)

请求及响应

本文中我们只关注第一点——请求及响应周期。
Akka笔记之请求与响应 - 图1
这张图说明了我们这次要做的事情。为了简单点,图中我并没有画出ActorSystem, Dispatcher以及Mailbox。
1. DriverApp将一条InitSignal消息发送给StudentActor。
2. StudentActor响应InitSignal消息并将一条QuoteRequest消息发送到TeacherActor。
3. 正如前面所说的那样,TeacherActor会回复一个QuoteResponse。
4. StudentActor将日志打印到控制台或者logger里。
同样的,我们会写一个测试用例来验证下它。
现在我们来仔细地分析下这四个步骤:

1. DRIVERAPP将一条INITSIGNAL消息发送给STUDENTACTOR

Akka笔记之请求与响应 - 图2
现在你应该能猜到DriverApp到底是干什么的了。它只做了4件事情:
1. 初始化ActorSystem

  1. //Initialize the ActorSystem
  2. val system = ActorSystem("UniversityMessageSystem”)
  1. 创建TeacherActor
  1. //create the teacher actor
  2. val teacherRef = system.actorOf(Props[TeacherActor], "teacherActor”)
  1. 创建StudentActor
  1. //create the Student Actor - pass the teacher actorref as a constructor parameter to StudentActor
  2. val studentRef = system.actorOf(Props(new StudentActor(teacherRef)), "studentActor")

你会注意到我把TeacherActor的一个ActorRef的引用作为构造函数的参数传给了StudentActor,这样StudentActor才能够通过ActorRef来将消息发送给TeacherActor。当然还有别的方法(比如通过Props来传递),不过这么做对后续即将讲到的Supervisor和Router来说会方便一点。很快我们会看到子Actor也能实现这个功能,不过那个方法用在这里并不适合——学生来生成老师,这看起来不太对劲吧?
最后,

  1. DriverApp将InitSignal消息发送给了StudentActor,这样StudentActor会开始将QuoteRequest消息发送给TeacherActor。
  1. //send a message to the Student Actor
  2. studentRef ! InitSignal

DriverClass讲的已经够多了。后面的Thread.sleep和ActorSystem.shutdown就是等了几秒,以便消息发送完成,然后再最终将ActorSystem关掉。
DRIVERAPP.SCALA

  1. package me.rerun.akkanotes.messaging.requestresponse
  2. import akka.actor.ActorSystem
  3. import akka.actor.Props
  4. import me.rerun.akkanotes.messaging.protocols.StudentProtocol._
  5. import akka.actor.ActorRef
  6. object DriverApp extends App {
  7. //Initialize the ActorSystem
  8. val system = ActorSystem("UniversityMessageSystem")
  9. //construct the teacher actor
  10. val teacherRef = system.actorOf(Props[TeacherActor], "teacherActor")
  11. //construct the Student Actor - pass the teacher actorref as a constructor parameter to StudentActor
  12. val studentRef = system.actorOf(Props(new StudentActor(teacherRef)), "studentActor")
  13. //send a message to the Student Actor
  14. studentRef ! InitSignal
  15. //Let's wait for a couple of seconds before we shut down the system
  16. Thread.sleep(2000)
  17. //Shut down the ActorSystem.
  18. system.shutdown()
  19. }

2. STUDENTACTOR响应INITSIGNAL消息并将QUOTEREQUEST消息发送给TEACHERACTOR

以及

4. STUDENTACTOR接收到TEACHERACTOR回复的QuoteResponse然后将日志打印到控制台/logger上来

为什么我把第2和第4点放到一起来讲?因为它太简单了,如果分开讲的话我怕你嫌我啰嗦。
Akka笔记之请求与响应 - 图3
那么,第2步——StudentActor接收到DriverApp发过来的InitSingal消息并将QuoteRequest发送给TeacherActor。

  1. def receive = {
  2. case InitSignal=> {
  3. teacherActorRef!QuoteRequest
  4. }
  5. ...
  6. ...

搞定!
第4步——StudentActor将TeacherActor发过来的消息打印出来。
Akka笔记之请求与响应 - 图4
说到做到:

  1. case QuoteResponse(quoteString) => {
  2. log.info ("Received QuoteResponse from Teacher")
  3. log.info(s"Printing from Student Actor $quoteString")
  4. }

我猜你肯定觉得这很像是伪代码。
那么,完整的StudentActor应该是这样的:
STUDENTACTOR.SCALA

  1. package me.rerun.akkanotes.messaging.requestresponse
  2. import akka.actor.Actor
  3. import akka.actor.ActorLogging
  4. import me.rerun.akkanotes.messaging.protocols.TeacherProtocol._
  5. import me.rerun.akkanotes.messaging.protocols.StudentProtocol._
  6. import akka.actor.Props
  7. import akka.actor.ActorRef
  8. class StudentActor (teacherActorRef:ActorRef) extends Actor with ActorLogging {
  9. def receive = {
  10. case InitSignal=> {
  11. teacherActorRef!QuoteRequest
  12. }
  13. case QuoteResponse(quoteString) => {
  14. log.info ("Received QuoteResponse from Teacher")
  15. log.info(s"Printing from Student Actor $quoteString")
  16. }
  17. }
  18. }

3. TeacherActor回复QuoteResponse

这和我们在前面的fire-n-forget那篇)中看到的代码是类似的。
TeacherActor接收到QuoteRequest消息然后回复一个QuoteResponse。
TEACHERACTOR.SCALA

  1. package me.rerun.akkanotes.messaging.requestresponse
  2. import scala.util.Random
  3. import akka.actor.Actor
  4. import akka.actor.ActorLogging
  5. import akka.actor.actorRef2Scala
  6. import me.rerun.akkanotes.messaging.protocols.TeacherProtocol._
  7. class TeacherActor extends Actor with ActorLogging {
  8. val quotes = List(
  9. "Moderation is for cowards",
  10. "Anything worth doing is worth overdoing",
  11. "The trouble is you think you have time",
  12. "You never gonna know if you never even try")
  13. def receive = {
  14. case QuoteRequest => {
  15. import util.Random
  16. //Get a random Quote from the list and construct a response
  17. val quoteResponse = QuoteResponse(quotes(Random.nextInt(quotes.size)))
  18. //respond back to the Student who is the original sender of QuoteRequest
  19. sender ! quoteResponse
  20. }
  21. }
  22. }

测试用例

现在,我们的测试用例会来模拟下DriverApp。由于StudentActor只是打印了个日志消息,我们没法对QuoteResponse本身进行断言,那么我们就看下EventStream中是不是有这条日志消息就好了(就像上回做的那样)
那么,我们的测试用例看起来会是这样的:

  1. "A student" must {
  2. "log a QuoteResponse eventually when an InitSignal is sent to it" in {
  3. import me.rerun.akkanotes.messaging.protocols.StudentProtocol._
  4. val teacherRef = system.actorOf(Props[TeacherActor], "teacherActor")
  5. val studentRef = system.actorOf(Props(new StudentActor(teacherRef)), "studentActor")
  6. EventFilter.info (start="Printing from Student Actor", occurrences=1).intercept{
  7. studentRef!InitSignal
  8. }
  9. }
  10. }