Skip to main content

Action Implementation Methods

Problem Definition

Background: Calling Java Methods from Workflow

In workflow YAML, actor methods are called by specifying them as strings:

- actor: turing
method: put
arguments: "e"

A mechanism is needed to call actual Java methods from this method: put string.

Two Implementation Methods

POJO-actor provides two implementation methods:

MethodInheritance/ImplementationUse Case
@Action annotationExtends IIActorRef<T>Operate POJOs from workflow
CallableByActionName interfaceImplements interfacePlugins, dynamic loading

Problem: Which One to Choose?

When creating a new actor, there's no clear criteria for choosing between these methods.


How to do it

Selection Criteria

RequirementRecommendation
Want to operate existing POJO from workflow@Action
Want to protect POJO state between actors (exclusive control)@Action
Want to load dynamically as pluginCallableByActionName
Actors created during workflow executionCallableByActionName
Want to use with Native Image (GraalVM)CallableByActionName

@Action Annotation Method

A method that wraps POJOs to make them operable from workflows.

// Step 1: POJO (ordinary class that doesn't know about workflows)
public class Turing {
private int currentPos = 0;
private Tape tape = new Tape();

public void put(String value) {
this.tape.set(this.currentPos, value);
}

public void move(String direction) {
if ("R".equals(direction)) currentPos++;
else if ("L".equals(direction)) currentPos--;
}
}
// Step 2: Wrap with IIActorRef
public class TuringIIAR extends IIActorRef<Turing> {

public TuringIIAR(String name, Turing pojo, IIActorSystem system) {
super(name, pojo, system);
}

@Action("put")
public ActionResult put(String args) {
String value = ActionArgs.getFirst(args);
this.object.put(value); // Call POJO method
return new ActionResult(true, "Put " + value);
}

@Action("move")
public ActionResult move(String args) {
String direction = ActionArgs.getFirst(args);
this.object.move(direction);
return new ActionResult(true, "Moved " + direction);
}
}
// Step 3: Register with system
Turing turing = new Turing();
TuringIIAR actor = new TuringIIAR("turing", turing, system);
system.addIIActor(actor);

Features:

  • Access POJO via this.object
  • Thread-safe operations possible with tell()/ask()
  • @Action methods auto-detected via reflection

CallableByActionName Method

A method that implements an interface and dispatches actions via switch statement.

public class MyPlugin implements CallableByActionName, ActorSystemAware {

private IIActorSystem system;
private Connection connection;

@Override
public void setActorSystem(IIActorSystem system) {
this.system = system;
}

@Override
public ActionResult callByActionName(String actionName, String args) {
try {
return switch (actionName) {
case "connect" -> connect(args);
case "query" -> query(args);
case "disconnect" -> disconnect();
default -> new ActionResult(false, "Unknown: " + actionName);
};
} catch (Exception e) {
return new ActionResult(false, "Error: " + e.getMessage());
}
}

private ActionResult connect(String args) { /* ... */ }
private ActionResult query(String args) { /* ... */ }
private ActionResult disconnect() { /* ... */ }
}

Features:

  • Explicit dispatch via switch statement
  • No reflection needed (Native Image compatible)
  • Can be dynamically created with loader.createChild

Under the hood

@Action Internal Behavior

When callByActionName is called on the IIActorRef base class, it processes in this order:

1. Search for @Action methods
└── Scan both subclass and wrapped object
└── Find method whose annotation value matches actionName

2. If hit, invoke
└── Call method via reflection
└── Return ActionResult

3. If no hit, fallback
└── Try subclass override
└── Try built-in actions (putJson, etc.)

This allows mixing @Action and switch statements.

Dynamic Loading with CallableByActionName

Flow when dynamically creating plugins with loader.createChild action:

- actor: loader
method: createChild
arguments: ["ROOT", "myPlugin", "com.example.MyPlugin"]
1. LoaderIIAR loads the class
└── Load class from JAR via URLClassLoader

2. Create instance
└── Class.getDeclaredConstructor().newInstance()

3. Initialize if ActorSystemAware
└── Call setActorSystem(system)

4. Register in actor tree
└── Register as child of parent actor

Exclusive Control Differences

MethodExclusive Control
@Action + tell()/ask()Access to POJO is serialized
CallableByActionNameCaller's responsibility (implement if needed)

With the @Action method, operating on POJO via lambda like tell(t -> t.put(value)) guarantees exclusive control through POJO-actor's mailbox mechanism.

With the CallableByActionName method, concurrent access to fields within the plugin must be managed by the developer.

Native Image Compatibility

MethodNative Image
@ActionReflection configuration required
CallableByActionNameWorks as-is

In GraalVM Native Image, classes using reflection must be declared in advance in a configuration file. The CallableByActionName method doesn't use reflection, so it can be compiled to Native Image without additional configuration.

Design Guideline Summary

Want to create an actor

├─ Want to wrap a POJO?
│ │
│ ├─ Yes → @Action method
│ │ - Protect POJO state
│ │ - Exclusive control with tell()/ask()
│ │
│ └─ No → CallableByActionName method

├─ Need dynamic loading?
│ │
│ └─ Yes → CallableByActionName method
│ - Create with loader.createChild
│ - Initialize with ActorSystemAware

└─ Use with Native Image?

└─ Yes → CallableByActionName method
- No reflection configuration needed