spawn

Java Getting Started

First we need to install Spawn CLI tool to create a new Java project.

curl -sSL https://github.com/eigr/spawn/releases/download/latest/install.sh | sh

Now you will need to fill in the data for groupId, artifactId, version, and package. Let’s call our maven artifact spawn-java-demo. The output of this command will be similar to the output below

spawn new java spawn-java-demo --group-id=io.eigr.spawn --artifact-id=spawn-java-demo --version=1.0.0 --package=io.eigr.spawn.java.demo

Now it is necessary to download the dependencies via Maven:

cd spawn-java-demo && mvn install

So far it’s all pretty boring and not really Spawn related, so it’s time to start playing for real. The first thing we’re going to do is define a place to put our protobuf files.

touch src/main/proto/actors/domain.proto

And let’s populate this file with the following content:

syntax = "proto3";

package domain;
// Due to the dynamic nature of spawn we are required to define the java package 
// as being the same as the protobuf package name.
option java_package = "domain";
// Generating the java classes in multiple files is mandatory for the process to work correctly.
option java_multiple_files = true;

message State {
   repeated string languages = 1;
}

message Request {
   string language = 1;
}

message Reply {
   string response = 1;
}

service JoeActor {
   rpc SetLanguage(Request) returns (Reply);
}

NOTE: Due to the dynamic nature of spawn we are required to define the java package as being the same as the protobuf package name. Also generating the java classes in multiple files is mandatory for the process to work correctly.

We must compile this file using the protoc utility. In the root of the project type the following command:

mvn protobuf:compile

Now in the spawn-java-demo folder we will create our first Java file containing the code of our Actor.

touch src/main/java/io/eigr/spawn/java/demo/Joe.java

Populate this file with the following content:

package io.eigr.spawn.java.demo;

import io.eigr.spawn.api.actors.ActorContext;
import io.eigr.spawn.api.actors.StatefulActor;
import io.eigr.spawn.api.actors.Value;
import io.eigr.spawn.api.actors.behaviors.ActorBehavior;
import io.eigr.spawn.api.actors.behaviors.BehaviorCtx;
import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior;
import io.eigr.spawn.api.actors.ActionBindings;
import domain.Reply;
import domain.Request;
import domain.State;

import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.*;

public final class JoeActor implements StatefulActor<State> {

    @Override
    public ActorBehavior configure(BehaviorCtx context) {
        return new NamedActorBehavior(
                name("JoeActor"),
                channel("test.channel"),
                action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage))
        );
    }

    private Value setLanguage(ActorContext<State> context, Request msg) {
        if (context.getState().isPresent()) {
            //Do something with previous state
        }

        return Value.at()
                .response(Reply.newBuilder()
                        .setResponse(String.format("Hi %s. Hello From Java", msg.getLanguage()))
                        .build())
                .state(updateState(msg.getLanguage()))
                .reply();
    }

    private State updateState(String language) {
        return State.newBuilder()
                .addLanguages(language)
                .build();
    }
}

Dissecting the code

Class Declaration

package io.eigr.spawn.java.demo;

import io.eigr.spawn.api.actors.StatefulActor;
import domain.State;

public final class JoeActor implements StatefulActor<State> {
 // ...
}

The JoeActor class implements StatefulActor<State> interface. StatefulActor is a generic interface provided by the Spawn API, which takes a type parameter for the state. In this case, the state type is domain.State defined in above protobuf file.

Configure Actor Behavior

public final class JoeActor implements StatefulActor<State> {
   @Override
   public ActorBehavior configure(BehaviorCtx context) {
      return new NamedActorBehavior(
              name("JoeActor"),
              channel("test.channel"),
              action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage))
      );
   }
}

This configure method is overridden from StatefulActor and is used to configure the actor’s behavior.

Handle request

public final class JoeActor implements StatefulActor<State> {
   //
   private Value setLanguage(ActorContext<State> context, Request msg) {
      if (context.getState().isPresent()) {
         // Do something with the previous state
      }

      return Value.at()
              .response(Reply.newBuilder()
                      .setResponse(String.format("Hi %s. Hello From Java", msg.getLanguage()))
                      .build())
              .state(updateState(msg.getLanguage()))
              .reply();
   }
}

This method setLanguage is called when the SetLanguage action is invoked. It takes an ActorContext<State> and a Request message as parameters.

Ok now with our Actor properly defined, we just need to start the SDK correctly. Create another file called App.java to serve as your application’s entrypoint and fill it with the following content:

package io.eigr.spawn.java.demo;

import io.eigr.spawn.api.Spawn;

public class App {
   public static void main(String[] args) throws Exception {
      Spawn spawnSystem = new SpawnSystem()
              .create("spawn-system")
              .withActor(Joe.class)
              .build();

      spawnSystem.start();
   }
}

Or passing transport options like:

package io.eigr.spawn.java.demo;

import io.eigr.spawn.api.Spawn;
import io.eigr.spawn.api.TransportOpts;

public class App {
   public static void main(String[] args) throws Exception {
      TransportOpts opts = TransportOpts.builder()
              .port(8091)
              .proxyPort(9003)
              .executor(Executors.newVirtualThreadPerTaskExecutor()) // If you use java above 19 and use the --enable-preview flag when running the jvm
              .build();

      Spawn spawnSystem = new SpawnSystem()
              .create("spawn-system")
              .withActor(Joe.class)
              .withTransportOptions(opts)
              .build();

      spawnSystem.start();
   }
}

Then:

mvn compile && mvn package && java -jar target/spawn-java-demo-1.0-SNAPSHOT.jar 

But of course you will need to locally run the Elixir proxy which will actually provide all the functionality for your Java application.

spawn dev run -p src/main/proto -s spawn-system -W

Spawn is based on kubernetes and containers, so you will need to generate a docker container for your application. There are many ways to do this, one of them is by adding Maven’s jib plugin. Add the following lines to your plugin’s section in pom.xml file:

<plugin>
    <groupId>com.google.cloud.tools</groupId>
    <artifactId>jib-maven-plugin</artifactId>
    <version>3.3.2</version>
    <configuration>
        <to>
            <image>your-repo-here/spawn-java-demo</image>
        </to>
    </configuration>
</plugin>

finally you will be able to create your container by running the following command in the root of your project:

mvn compile jib:build

Next: Actors

Previous: SDKs