In this example, we'll use Dapr to test the actor pattern capabilities such as concurrency, state, life-cycle management for actor activation/deactivation, timers, and reminders to wake-up actors.
Visit this link for more information about the Actor pattern.
This example contains the follow classes:
- DemoActor: The interface for the actor. Exposes the different actor features.
- DemoActorImpl: The implementation for the DemoActor interface. Handles the logic behind the different actor features.
- DemoActorService: A Spring Boot application service that registers the actor into the Dapr actor runtime.
- DemoActorClient: This class will create and execute actors and its capabilities by using Dapr.
- Dapr CLI.
- Java JDK 11 (or greater):
- Apache Maven version 3.x.
Clone this repository:
git clone https://github.com/dapr/java-sdk.git
cd java-sdk
Then build the Maven project:
# make sure you are in the `java-sdk` directory.
mvn install
Get into the examples directory.
cd examples
Run dapr init
to initialize Dapr in Self-Hosted Mode if it's not already initialized.
The first Java class is DemoActorService
. It's job is to register an implementation of DemoActor
in the Dapr's Actor runtime. In the DemoActorService.java
file, you will find the DemoActorService
class and the main
method. See the code snippet below:
public class DemoActorService {
public static void main(String[] args) throws Exception {
///...
// Register the Actor class.
ActorRuntime.getInstance().registerActor(DemoActorImpl.class);
// Start Dapr's callback endpoint.
DaprApplication.start(port);
}
}
This application uses ActorRuntime.getInstance().registerActor()
in order to register DemoActorImpl
as an actor in the Dapr Actor runtime. Internally, it is using DefaultObjectSerializer
for two properties: objectSerializer
is for Dapr's sent and received objects, and stateSerializer
is for objects to be persisted.
DaprApplication.start()
method will run the Spring Boot DaprApplication, which registers the Dapr Spring Boot controller DaprController. This controller contains all Actor methods implemented as endpoints. The Dapr's sidecar will call into the controller.
See DemoActorImpl for details on the implementation of an actor:
public class DemoActorImpl extends AbstractActor implements DemoActor, Remindable<Integer> {
//...
public DemoActorImpl(ActorRuntimeContext runtimeContext, ActorId id) {
super(runtimeContext, id);
//...
}
@Override
public void registerTimer(String state) {
//...
}
@Override
public void registerReminder(int index) {
//...
}
@Override
public String say(String something) {
//...
}
@Override
public Mono<Integer> incrementAndGet(int delta) {
//...
}
@Override
public void clock(String message) {
//...
}
@Override
public Class<Integer> getStateType() {
return Integer.class;
}
@Override
public Mono<Void> receiveReminder(String reminderName, Integer state, Duration dueTime, Duration period) {
//...
}
}
An actor inherits from AbstractActor
and implements the constructor to pass through ActorRuntimeContext
and ActorId
. By default, the actor's name will be the same as the class' name. Optionally, it can be annotated with ActorType
and override the actor's name. The actor's methods can be synchronously or use Project Reactor's Mono return type. Finally, state management is done via methods in super.getActorStateManager()
. The DemoActor
interface is used by the Actor runtime and also client. See how the DemoActor
interface can be annotated as a Dapr Actor.
import io.dapr.actors.ActorMethod;
/**
* Example of implementation of an Actor.
*/
@ActorType(name = "DemoActor")
public interface DemoActor {
void registerTimer(String state);
void registerReminder(int index);
@ActorMethod(name = "echo_message")
String say(String something);
void clock(String message);
@ActorMethod(returns = Integer.class)
Mono<Integer> incrementAndGet(int delta);
}
The @ActorType
annotation indicates the Dapr Java SDK that this interface is an Actor Type, allowing a name for the type to be defined.
The @ActorMethod
annotation can be applied to an interface method to specify configuration for that method. In this example, the say
method, is renamed to echo_message
- this can be used when invoking an actor method implemented in a different programming language (like C# or Python) and the method name does not match Java's naming conventions.
Some methods can return a Mono
object. In these cases, the @ActorMethod
annotation is used to hint the Dapr Java SDK of the type encapsulated in the Mono
object. You can read more about Java generic type erasure here.
Now, execute the following script in order to run DemoActorService:
dapr run --resources-path ./components/actors --app-id demoactorservice --app-port 3000 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.actors.DemoActorService -p 3000
The actor client is a simple java class with a main method that uses the Dapr Actor capabilities in order to create the actors and execute the different methods based on the Actor pattern.
The DemoActorClient.java
file contains the DemoActorClient
class. See the code snippet below:
public class DemoActorClient {
private static final int NUM_ACTORS = 3;
public static void main(String[] args) throws InterruptedException {
try (ActorClient client = new ActorClient()) {
ActorProxyBuilder<DemoActor> builder = new ActorProxyBuilder(DemoActor.class, client);
///...
for (int i = 0; i < NUM_ACTORS; i++) {
DemoActor actor = builder.build(ActorId.createRandom());
// Start a thread per actor.
Thread thread = new Thread(() -> callActorForever(actorId.toString(), actor));
thread.start();
threads.add(thread);
}
///...
}
}
private static final void callActorForever(int index, String actorId, DemoActor actor) {
// First, register reminder.
actor.registerReminder(index);
// Second register timer.
actor.registerTimer("ping! {" + index + "} ");
// Now, we run until thread is interrupted.
while (!Thread.currentThread().isInterrupted()) {
// Invoke actor method to increment counter by 1, then build message.
int messageNumber = actor.incrementAndGet(1).block();
String message = String.format("Message #%d received from actor at index %d with ID %s", messageNumber,
index, actorId);
// Invoke the 'say' method in actor.
String result = actor.say(message);
System.out.println(String.format("Reply %s received from actor at index %d with ID %s ", result,
index, actorId));
try {
// Waits for up to 1 second.
Thread.sleep((long) (1000 * Math.random()));
} catch (InterruptedException e) {
// We have been interrupted, so we set the interrupted flag to exit gracefully.
Thread.currentThread().interrupt();
}
}
}
}
First, the client defines how many actors it is going to create. The main method declares a ActorClient
and ActorProxyBuilder
to create instances of the DemoActor
interface, which are implemented automatically by the SDK and make remote calls to the equivalent methods in Actor runtime. ActorClient
is reusable for different actor types and should be instantiated only once in your code. ActorClient
also implements AutoCloseable
, which means it holds resources that need to be closed. In this example, we use the "try-resource" feature in Java.
Then, the code executes the callActorForever
private method once per actor. Initially, it will invoke registerReminder()
, which sets the due time and period for the reminder. Then, incrementAndGet()
increments a counter, persists it and sends it back as response. Finally, say
method will print a message containing the received string along with the formatted server time.
Use the following command to execute the DemoActorClient:
dapr run --resources-path ./components/actors --app-id demoactorclient -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.actors.DemoActorClient
Once running, the demoactorservice
logs will start displaying the different steps:
First, we can see actors being activated and the say
method being invoked:
== APP == 2023-05-23 11:04:47,348 {HH:mm:ss.SSS} [http-nio-3000-exec-5] INFO io.dapr.actors.ActorTrace - Actor:a855706e-f477-4530-9bff-d7b1cd2988f8 Activating ...
== APP == 2023-05-23 11:04:47,348 {HH:mm:ss.SSS} [http-nio-3000-exec-6] INFO io.dapr.actors.ActorTrace - Actor:4720f646-baaa-4fae-86dd-aec2fc2ead6e Activating ...
== APP == 2023-05-23 11:04:47,348 {HH:mm:ss.SSS} [http-nio-3000-exec-7] INFO io.dapr.actors.ActorTrace - Actor:d54592a5-5b5b-4925-8974-6cf309fbdbbf Activating ...
== APP == 2023-05-23 11:04:47,348 {HH:mm:ss.SSS} [http-nio-3000-exec-5] INFO io.dapr.actors.ActorTrace - Actor:a855706e-f477-4530-9bff-d7b1cd2988f8 Activated
== APP == 2023-05-23 11:04:47,348 {HH:mm:ss.SSS} [http-nio-3000-exec-7] INFO io.dapr.actors.ActorTrace - Actor:d54592a5-5b5b-4925-8974-6cf309fbdbbf Activated
== APP == 2023-05-23 11:04:47,348 {HH:mm:ss.SSS} [http-nio-3000-exec-6] INFO io.dapr.actors.ActorTrace - Actor:4720f646-baaa-4fae-86dd-aec2fc2ead6e Activated
== APP == Server say method for actor d54592a5-5b5b-4925-8974-6cf309fbdbbf: Message #2 received from actor at index 1 with ID d54592a5-5b5b-4925-8974-6cf309fbdbbf @ 2023-05-23 11:04:48.459
== APP == Server say method for actor 4720f646-baaa-4fae-86dd-aec2fc2ead6e: Message #4 received from actor at index 2 with ID 4720f646-baaa-4fae-86dd-aec2fc2ead6e @ 2023-05-23 11:04:48.695
== APP == Server say method for actor d54592a5-5b5b-4925-8974-6cf309fbdbbf: Message #3 received from actor at index 1 with ID d54592a5-5b5b-4925-8974-6cf309fbdbbf @ 2023-05-23 11:04:48.708
Then we can see reminders and timers in action:
== APP == Server timer triggered with state ping! {0} for actor a855706e-f477-4530-9bff-d7b1cd2988f8@ 2023-05-23 11:04:49.021
== APP == Server timer triggered with state ping! {1} for actor d54592a5-5b5b-4925-8974-6cf309fbdbbf@ 2023-05-23 11:04:49.021
== APP == Reminder myremind with state {2} triggered for actor 4720f646-baaa-4fae-86dd-aec2fc2ead6e @ 2023-05-23 11:04:52.012
== APP == Reminder myremind with state {1} triggered for actor d54592a5-5b5b-4925-8974-6cf309fbdbbf @ 2023-05-23 11:04:52.012
== APP == Reminder myremind with state {0} triggered for actor a855706e-f477-4530-9bff-d7b1cd2988f8 @ 2023-05-23 11:04:52.012
Finally, the console for demoactorclient
got the service responses:
== APP == Reply 2023-05-23 11:04:49.288 received from actor at index 0 with ID a855706e-f477-4530-9bff-d7b1cd2988f8
== APP == Reply 2023-05-23 11:04:49.408 received from actor at index 0 with ID a855706e-f477-4530-9bff-d7b1cd2988f8
== APP == Reply 2023-05-23 11:04:49.515 received from actor at index 1 with ID d54592a5-5b5b-4925-8974-6cf309fbdbbf
== APP == Reply 2023-05-23 11:04:49.740 received from actor at index 0 with ID a855706e-f477-4530-9bff-d7b1cd2988f8
== APP == Reply 2023-05-23 11:04:49.863 received from actor at index 2 with ID 4720f646-baaa-4fae-86dd-aec2fc2ead6e
For more details on Dapr SpringBoot integration, please refer to Dapr Spring Boot Application implementation.
Currently, these are the limitations in the Java SDK for Dapr:
- Actor interface cannot have overloaded methods (methods with same name, but different signature).
- Actor methods can only have zero or one parameter.