parent
9a54ae92d5
commit
081e6bd8ba
5 changed files with 227 additions and 9 deletions
|
|
@ -298,15 +298,16 @@ public class IntroTest {
|
||||||
ctx.watch(gabbler);
|
ctx.watch(gabbler);
|
||||||
chatRoom.tell(new ChatRoom.GetSession("ol’ Gabbler", gabbler));
|
chatRoom.tell(new ChatRoom.GetSession("ol’ Gabbler", gabbler));
|
||||||
|
|
||||||
return Behaviors.receive(Void.class)
|
return Behaviors.<Void>receiveSignal(
|
||||||
.onSignal(Terminated.class, (c, sig) -> Behaviors.stopped())
|
(c, sig) -> {
|
||||||
.build();
|
if (sig instanceof Terminated) return Behaviors.stopped();
|
||||||
|
else return Behaviors.unhandled();
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
final ActorSystem<Void> system =
|
final ActorSystem<Void> system =
|
||||||
ActorSystem.create(main, "ChatRoomDemo");
|
ActorSystem.create(main, "ChatRoomDemo");
|
||||||
|
|
||||||
system.getWhenTerminated().toCompletableFuture().get();
|
|
||||||
//#chatroom-main
|
//#chatroom-main
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@
|
||||||
package jdocs.akka.typed;
|
package jdocs.akka.typed;
|
||||||
|
|
||||||
//#imports
|
//#imports
|
||||||
|
import akka.NotUsed;
|
||||||
import akka.actor.typed.ActorRef;
|
import akka.actor.typed.ActorRef;
|
||||||
|
import akka.actor.typed.ActorSystem;
|
||||||
import akka.actor.typed.Behavior;
|
import akka.actor.typed.Behavior;
|
||||||
|
import akka.actor.typed.Terminated;
|
||||||
import akka.actor.typed.javadsl.Behaviors;
|
import akka.actor.typed.javadsl.Behaviors;
|
||||||
import akka.actor.typed.javadsl.Behaviors.Receive;
|
import akka.actor.typed.javadsl.Behaviors.Receive;
|
||||||
import akka.actor.typed.javadsl.ActorContext;
|
import akka.actor.typed.javadsl.ActorContext;
|
||||||
|
|
@ -138,4 +141,54 @@ public class MutableIntroTest {
|
||||||
}
|
}
|
||||||
//#chatroom-actor
|
//#chatroom-actor
|
||||||
|
|
||||||
|
|
||||||
|
//#chatroom-gabbler
|
||||||
|
public static abstract class Gabbler {
|
||||||
|
private Gabbler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Behavior<ChatRoom.SessionEvent> behavior() {
|
||||||
|
return Behaviors.receive(ChatRoom.SessionEvent.class)
|
||||||
|
.onMessage(ChatRoom.SessionDenied.class, (ctx, msg) -> {
|
||||||
|
System.out.println("cannot start chat room session: " + msg.reason);
|
||||||
|
return Behaviors.stopped();
|
||||||
|
})
|
||||||
|
.onMessage(ChatRoom.SessionGranted.class, (ctx, msg) -> {
|
||||||
|
msg.handle.tell(new ChatRoom.PostMessage("Hello World!"));
|
||||||
|
return Behaviors.same();
|
||||||
|
})
|
||||||
|
.onMessage(ChatRoom.MessagePosted.class, (ctx, msg) -> {
|
||||||
|
System.out.println("message has been posted by '" +
|
||||||
|
msg.screenName +"': " + msg.message);
|
||||||
|
return Behaviors.stopped();
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
//#chatroom-gabbler
|
||||||
|
|
||||||
|
public static void runChatRoom() throws Exception {
|
||||||
|
|
||||||
|
//#chatroom-main
|
||||||
|
Behavior<Void> main = Behaviors.setup(ctx -> {
|
||||||
|
ActorRef<ChatRoom.RoomCommand> chatRoom =
|
||||||
|
ctx.spawn(ChatRoom.behavior(), "chatRoom");
|
||||||
|
ActorRef<ChatRoom.SessionEvent> gabbler =
|
||||||
|
ctx.spawn(Gabbler.behavior(), "gabbler");
|
||||||
|
ctx.watch(gabbler);
|
||||||
|
chatRoom.tell(new ChatRoom.GetSession("ol’ Gabbler", gabbler));
|
||||||
|
|
||||||
|
return Behaviors.<Void>receiveSignal(
|
||||||
|
(c, sig) -> {
|
||||||
|
if (sig instanceof Terminated) return Behaviors.stopped();
|
||||||
|
else return Behaviors.unhandled();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final ActorSystem<Void> system =
|
||||||
|
ActorSystem.create(main, "ChatRoomDemo");
|
||||||
|
//#chatroom-main
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,6 @@ class IntroSpec extends ScalaTestWithActorTestKit with WordSpecLike {
|
||||||
}
|
}
|
||||||
|
|
||||||
val system = ActorSystem(main, "ChatRoomDemo")
|
val system = ActorSystem(main, "ChatRoomDemo")
|
||||||
Await.result(system.whenTerminated, 3.seconds)
|
|
||||||
//#chatroom-main
|
//#chatroom-main
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,6 @@ class MutableIntroSpec extends ScalaTestWithActorTestKit with WordSpecLike {
|
||||||
|
|
||||||
val system = ActorSystem(main, "ChatRoomDemo")
|
val system = ActorSystem(main, "ChatRoomDemo")
|
||||||
system ! "go"
|
system ! "go"
|
||||||
Await.result(system.whenTerminated, 1.second)
|
|
||||||
//#chatroom-main
|
//#chatroom-main
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,8 @@ The console output may look like this:
|
||||||
|
|
||||||
## A More Complex Example
|
## A More Complex Example
|
||||||
|
|
||||||
|
### Functional Style
|
||||||
|
|
||||||
The next example is more realistic and demonstrates some important patterns:
|
The next example is more realistic and demonstrates some important patterns:
|
||||||
|
|
||||||
* Using a sealed trait and case class/objects to represent multiple messages an actor can receive
|
* Using a sealed trait and case class/objects to represent multiple messages an actor can receive
|
||||||
|
|
@ -210,7 +212,7 @@ former simply speaks more languages than the latter. The opposite would be
|
||||||
problematic, so passing an @scala[`ActorRef[PublishSessionMessage]`]@java[`ActorRef<PublishSessionMessage>`] where
|
problematic, so passing an @scala[`ActorRef[PublishSessionMessage]`]@java[`ActorRef<PublishSessionMessage>`] where
|
||||||
@scala[`ActorRef[RoomCommand]`]@java[`ActorRef<RoomCommand>`] is required will lead to a type error.
|
@scala[`ActorRef[RoomCommand]`]@java[`ActorRef<RoomCommand>`] is required will lead to a type error.
|
||||||
|
|
||||||
### Trying it out
|
#### Trying it out
|
||||||
|
|
||||||
In order to see this chat room in action we need to write a client Actor that can use it:
|
In order to see this chat room in action we need to write a client Actor that can use it:
|
||||||
|
|
||||||
|
|
@ -268,7 +270,171 @@ called `ctx.watch` for it. This allows us to shut down the Actor system: when
|
||||||
the main Actor terminates there is nothing more to do.
|
the main Actor terminates there is nothing more to do.
|
||||||
|
|
||||||
Therefore after creating the Actor system with the `main` Actor’s
|
Therefore after creating the Actor system with the `main` Actor’s
|
||||||
`Behavior` the only thing we do is await its termination.
|
`Behavior` we can let the `main` method return, the `ActorSystem` will continue running and
|
||||||
|
the JVM alive until the root actor stops.
|
||||||
|
|
||||||
|
|
||||||
|
### Object-oriented style
|
||||||
|
|
||||||
|
The samples shown so far are all based on a functional programming style
|
||||||
|
where you pass a function to a factory which then constructs a behavior, for stateful
|
||||||
|
actors this means passing immutable state around as parameters and switching to a new behavior
|
||||||
|
whenever you need to act on a changed state. An alternative way to express the same is a more
|
||||||
|
object oriented style where a concrete class for the actor behavior is defined and mutable
|
||||||
|
state is kept inside of it as fields.
|
||||||
|
|
||||||
|
Some reasons why you may want to do this are:
|
||||||
|
|
||||||
|
@@@ div {.group-java}
|
||||||
|
|
||||||
|
* Java lambdas can only close over final or effectively final fields, making it
|
||||||
|
impractical to use this style in behaviors that mutate their fields
|
||||||
|
* some state is not immutable, e.g. immutable collections are not widely used in Java
|
||||||
|
* it could be more familiar and easier to migrate existing untyped actors to this style
|
||||||
|
* mutable state can sometimes have better performance, e.g. mutable collections and
|
||||||
|
avoiding allocating new instance for next behavior (be sure to benchmark if this is your
|
||||||
|
motivation)
|
||||||
|
|
||||||
|
@@@
|
||||||
|
|
||||||
|
@@@ div {.group-scala}
|
||||||
|
|
||||||
|
* some state is not immutable
|
||||||
|
* it could be more familiar and easier to migrate existing untyped actors to this style
|
||||||
|
* mutable state can sometimes have better performance, e.g. mutable collections and
|
||||||
|
avoiding allocating new instance for next behavior (be sure to benchmark if this is your
|
||||||
|
motivation)
|
||||||
|
|
||||||
|
@@@
|
||||||
|
|
||||||
|
#### MutableBehavior API
|
||||||
|
|
||||||
|
Defining a class based actor behavior starts with extending
|
||||||
|
@scala[`akka.actor.typed.scaladsl.MutableBehavior[T]`]
|
||||||
|
@java[`akka.actor.typed.javadsl.MutableBehavior<T>`] where `T` is the type of messages
|
||||||
|
the behavior will accept.
|
||||||
|
|
||||||
|
Let's repeat the chat room sample from @ref:[A more complex example above](#a-more-complex-example) but implemented
|
||||||
|
using `MutableBehavior`. The protocol for interacting with the actor looks the same:
|
||||||
|
|
||||||
|
Scala
|
||||||
|
: @@snip [MutableIntroSpec.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/MutableIntroSpec.scala) { #chatroom-protocol }
|
||||||
|
|
||||||
|
Java
|
||||||
|
: @@snip [MutableIntroTest.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/MutableIntroTest.java) { #chatroom-protocol }
|
||||||
|
|
||||||
|
Initially the client Actors only get access to an @scala[`ActorRef[GetSession]`]@java[`ActorRef<GetSession>`]
|
||||||
|
which allows them to make the first step. Once a client’s session has been
|
||||||
|
established it gets a `SessionGranted` message that contains a `handle` to
|
||||||
|
unlock the next protocol step, posting messages. The `PostMessage`
|
||||||
|
command will need to be sent to this particular address that represents the
|
||||||
|
session that has been added to the chat room. The other aspect of a session is
|
||||||
|
that the client has revealed its own address, via the `replyTo` argument, so that subsequent
|
||||||
|
`MessagePosted` events can be sent to it.
|
||||||
|
|
||||||
|
This illustrates how Actors can express more than just the equivalent of method
|
||||||
|
calls on Java objects. The declared message types and their contents describe a
|
||||||
|
full protocol that can involve multiple Actors and that can evolve over
|
||||||
|
multiple steps. Here's the `MutableBehavior` implementation of the chat room protocol:
|
||||||
|
|
||||||
|
Scala
|
||||||
|
: @@snip [MutableIntroSpec.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/MutableIntroSpec.scala) { #chatroom-behavior }
|
||||||
|
|
||||||
|
Java
|
||||||
|
: @@snip [MutableIntroTest.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/MutableIntroTest.java) { #chatroom-behavior }
|
||||||
|
|
||||||
|
The state is managed through fields in the class, just like with a regular object oriented class.
|
||||||
|
As the state is mutable, we never return a different behavior from the message logic, but can return
|
||||||
|
the `MutableBehavior` instance itself (`this`) as a behavior to use for processing the next message coming in.
|
||||||
|
We could also return `Behavior.same` to achieve the same.
|
||||||
|
|
||||||
|
It is also possible to return a new different `MutableBehavior`, for example to represent a different state in a
|
||||||
|
finite state machine (FSM), or use one of the functional behavior factories to combine the object oriented
|
||||||
|
with the functional style for different parts of the lifecycle of the same Actor behavior.
|
||||||
|
|
||||||
|
When a new `GetSession` command comes in we add that client to the
|
||||||
|
list of current sessions. Then we also need to create the session’s
|
||||||
|
`ActorRef` that will be used to post messages. In this case we want to
|
||||||
|
create a very simple Actor that repackages the `PostMessage`
|
||||||
|
command into a `PublishSessionMessage` command which also includes the
|
||||||
|
screen name.
|
||||||
|
|
||||||
|
To implement the logic where we spawn a child for the session we need access
|
||||||
|
to the `ActorContext`. This is injected as a constructor parameter upon creation
|
||||||
|
of the behavior, note how we combine the `MutableBehavior` with `Behaviors.setup`
|
||||||
|
to do this in the `behavior` method.
|
||||||
|
|
||||||
|
The behavior that we declare here can handle both subtypes of `RoomCommand`.
|
||||||
|
`GetSession` has been explained already and the
|
||||||
|
`PublishSessionMessage` commands coming from the session Actors will
|
||||||
|
trigger the dissemination of the contained chat room message to all connected
|
||||||
|
clients. But we do not want to give the ability to send
|
||||||
|
`PublishSessionMessage` commands to arbitrary clients, we reserve that
|
||||||
|
right to the internal session actors we create—otherwise clients could pose as completely
|
||||||
|
different screen names (imagine the `GetSession` protocol to include
|
||||||
|
authentication information to further secure this). Therefore `PublishSessionMessage`
|
||||||
|
has `private` visibility and can't be created outside the `ChatRoom` @scala[object]@java[class].
|
||||||
|
|
||||||
|
If we did not care about securing the correspondence between a session and a
|
||||||
|
screen name then we could change the protocol such that `PostMessage` is
|
||||||
|
removed and all clients just get an @scala[`ActorRef[PublishSessionMessage]`]@java[`ActorRef<PublishSessionMessage>`] to
|
||||||
|
send to. In this case no session actor would be needed and we could use
|
||||||
|
@scala[`ctx.self`]@java[`ctx.getSelf()`]. The type-checks work out in that case because
|
||||||
|
@scala[`ActorRef[-T]`]@java[`ActorRef<T>`] is contravariant in its type parameter, meaning that we
|
||||||
|
can use a @scala[`ActorRef[RoomCommand]`]@java[`ActorRef<RoomCommand>`] wherever an
|
||||||
|
@scala[`ActorRef[PublishSessionMessage]`]@java[`ActorRef<PublishSessionMessage>`] is needed—this makes sense because the
|
||||||
|
former simply speaks more languages than the latter. The opposite would be
|
||||||
|
problematic, so passing an @scala[`ActorRef[PublishSessionMessage]`]@java[`ActorRef<PublishSessionMessage>`] where
|
||||||
|
@scala[`ActorRef[RoomCommand]`]@java[`ActorRef<RoomCommand>`] is required will lead to a type error.
|
||||||
|
|
||||||
|
#### Trying it out
|
||||||
|
|
||||||
|
In order to see this chat room in action we need to write a client Actor that can use it, for this
|
||||||
|
stateless actor it doesn't make much sense to use the `MutableBehavior` so let's just reuse the
|
||||||
|
functional style gabbler from the sample above:
|
||||||
|
|
||||||
|
Scala
|
||||||
|
: @@snip [MutableIntroSpec.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/MutableIntroSpec.scala) { #chatroom-gabbler }
|
||||||
|
|
||||||
|
Java
|
||||||
|
: @@snip [MutableIntroTest.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/MutableIntroTest.java) { #chatroom-gabbler }
|
||||||
|
|
||||||
|
Now to try things out we must start both a chat room and a gabbler and of
|
||||||
|
course we do this inside an Actor system. Since there can be only one guardian
|
||||||
|
supervisor we could either start the chat room from the gabbler (which we don’t
|
||||||
|
want—it complicates its logic) or the gabbler from the chat room (which is
|
||||||
|
nonsensical) or we start both of them from a third Actor—our only sensible
|
||||||
|
choice:
|
||||||
|
|
||||||
|
|
||||||
|
Scala
|
||||||
|
: @@snip [MutableIntroSpec.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/MutableIntroSpec.scala) { #chatroom-main }
|
||||||
|
|
||||||
|
Java
|
||||||
|
: @@snip [MutableIntroTest.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/MutableIntroTest.java) { #chatroom-main }
|
||||||
|
|
||||||
|
In good tradition we call the `main` Actor what it is, it directly
|
||||||
|
corresponds to the `main` method in a traditional Java application. This
|
||||||
|
Actor will perform its job on its own accord, we do not need to send messages
|
||||||
|
from the outside, so we declare it to be of type @scala[`NotUsed`]@java[`Void`]. Actors receive not
|
||||||
|
only external messages, they also are notified of certain system events,
|
||||||
|
so-called Signals. In order to get access to those we choose to implement this
|
||||||
|
particular one using the `receive` behavior decorator. The
|
||||||
|
provided `onSignal` function will be invoked for signals (subclasses of `Signal`)
|
||||||
|
or the `onMessage` function for user messages.
|
||||||
|
|
||||||
|
This particular `main` Actor is created using `Behaviors.setup`, which is like a factory for a behavior.
|
||||||
|
Creation of the behavior instance is deferred until the actor is started, as opposed to `Behaviors.receive`
|
||||||
|
that creates the behavior instance immediately before the actor is running. The factory function in
|
||||||
|
`setup` is passed the `ActorContext` as parameter and that can for example be used for spawning child actors.
|
||||||
|
This `main` Actor creates the chat room and the gabbler and the session between them is initiated, and when the
|
||||||
|
gabbler is finished we will receive the `Terminated` event due to having
|
||||||
|
called `ctx.watch` for it. This allows us to shut down the Actor system: when
|
||||||
|
the main Actor terminates there is nothing more to do.
|
||||||
|
|
||||||
|
Therefore after creating the Actor system with the `main` Actor’s
|
||||||
|
`Behavior` we can let the `main` method return, the `ActorSystem` will continue running and
|
||||||
|
the JVM alive until the root actor stops.
|
||||||
|
|
||||||
|
|
||||||
## Relation to Akka (untyped) Actors
|
## Relation to Akka (untyped) Actors
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue