Getting started with Spine in Java
This guide will walk you through a minimal client-server application in Java which handles one command to print some text on behalf of the current computer user. The document goes through already written code which is quite simple. So, it won’t take long.
What we’ll do
We’ll go through the example which shows a Bounded Context called “Hello”.
The context has one ProcessManager
, which handles the Print
command
sent from the client-side code to the server-side code hosting the context.
We’ll go through the production code of the example suite, and through the code which
tests the Hello context.
What you’ll need
- JDK version 8 or higher.
- Git.
-
The source code of the Hello World example.
git clone [email protected]:spine-examples/hello.git
Run the code
To check that you’ve got everything installed, please run the following command:
./gradlew :sayHello
If you’re under Windows, it would be:
gradlew.bat :sayHello
This would build and execute the example. The process should finish with the output which looks like this:
> Task :sayHello
Dec 18, 2020 5:32:11 PM io.spine.base.Environment setCurrentType
INFO: `Environment` set to `io.spine.base.Production`.
Dec 18, 2020 5:32:12 PM io.spine.server.Server lambda$start$1
INFO: In-process server started with the name `a7c62b63-2cc6-4679-bb92-072591142275`.
[savik] Hello World!
The client received the event: io.spine.helloworld.hello.event.Printed{"username":"savik","text":"Hello World!"}
Dec 18, 2020 5:32:15 PM io.spine.server.Server shutdown
INFO: Shutting down the server...
Dec 18, 2020 5:32:15 PM io.spine.server.Server shutdown
INFO: Server shut down.
The first line tells which Gradle task we run. The following couple of lines is the server-side logging that informs us that the server was started.
The line with the Environment
tells that we’re running the application in the Production
environment.
The line with “Hello World!” text is the “meat” of this example suite.
It is what our ProcessManager
(called Console
) does in response to the Print
command received
from the Client
.
The text in between brackets is the name of the current computer user. The name was passed as
the argument of the Print
command.
We opted to show a ProcessManager
— instead of an Aggregate
— because
the console output is similar to an “External System”. Dealing with things like
that is the job of Process Managers. We also want to highlight the importance of using
this architectural pattern.
The output that follows is the logging produced by the Client
class as it receives the Printed
event from the server.
Then, the server shuts down concluding the example.
Now, let’s dive into the code.
Project structure
For the sake of simplicity, this example is organised as a single-module Gradle project. Most likely, a project for a real world application would be multi-module.
The root directory
The root of the project contains the following files:
LICENSE
— the text of the Apache v2 license under which the framework and this example are licensed.README.md
— a brief intro for the example.gradlew
andgradlew.bat
— scripts for running Gradle Wrapper.build.gradle
— the project configuration. We’ll review this file later in details.
The root directory also contains “invisible” files, names of which start with
the dot (e.g. .gitattributes
and .travis.yml
).
These files configure Git and CI systems we use. They are not directly related to the subject
of the example and this guide. If you’re interested in this level of details,
please look into the code and comments in these files.
Here are the directories of interest in the project root:
gradle
— this directory contains the code of Gradle Wrapper and two Gradle scripts used in the project configuration.generated
— this directory contains the code generated by Protobuf Compiler and Spine Model Compiler. This directory and code it contains is created automatically when a domain model changes. This directory is excluded from version control.src
— contains the handcrafted source code.
Let’s see how the source code is structured.
The src
directory
The source code directory follows standard Gradle conventions and has two sub-directories:
main
— the production code;test
— the tests for the production code.
The production code consists of two parts allocated by sub-directories:
-
proto
— contains the definition of data structures in Google Protobuf. A domain model definition starts from adding the code to this directory. Then, the Protobuf code is compiled to the languages of the project. The output of this process is placed into thegenerated
directory, with a sub-directory for each language. This example uses only Java. -
java
— this directory contains the model behavior and other server- and client-side code. A real project would have these parts in separate modules or projects. We put it all together for the sake of simplicity.
Now, let’s review the code in details, starting with how to add Spine to a Gradle project.
Adding Spine to a Gradle project
Let’s open build.gradle
from the root of the project. The simplest and recommended way for
adding Spine dependencies to a project is the Bootstrap plugin:
plugins {
id("io.spine.tools.gradle.bootstrap").version("1.7.0")
}
Once the plugin is added, we can use its features:
spine.enableJava().server()
This enables Java in the module and adds necessary dependencies and configurations.
Calling spine.enableJava().server()
adds both server- and client-side dependencies.
This way a module of a Bounded Context “A” may be a client for a Bounded Context “B”.
Client-side applications or modules should call: spine.enableJava().client()
.
Other project configuration
The rest of the build.gradle
file does the following:
-
Sets the version of Java to 8.
-
Adds JUnit dependencies by applying the
tests.gradle
script plugin (which we extracted for the sake of simplicity). -
Defines the
sayHello
task which runs theExample
application, which orchestrates the demo.
We are not reviewing these parts of the project configuration deeper because they are not
related to the use of the Spine framework. If you’re interested in more details, please look into
the mentioned .gradle
files.
Now, let’s look into the data structure of the Hello context.
Hello context data
The data types of the Hello context are defined under the src/main/proto/hello
directory with
the following files:
-
commands.proto
— this file defines thePrint
command.By convention, commands are defined in a file with the
commands
suffix in its name. It can be, for example,order_commands.proto
or justcommands.proto
like in our example. -
events.proto
— this file defines thePrinted
event.Similarly to commands, events are defined in proto files having the
events
suffix in their names.
These two files define signals used by the Hello context. There’s also data of the Console
Process Manager, which is defined in the package server
in the file console.proto
.
We arrange the sub-package server
to highlight the fact that this is server-only data. It is not
a convention used by the framework. We find the clarity of this infix useful when creating
cloud applications. So, we share it as a recommendation in this example.
Let’s review the context data definition in details.
The commands.proto
file
After the copyright header the file starts with the syntax declaration. The framework supports only this version of the Protobuf dialect:
syntax = "proto3";
Then follows the import statement for custom options used when defining data types. This import is required for all proto files of a Spine-based project.
import "spine/options.proto";
The following file-wide option defines the prefix for type names used in this file.
option (type_url_prefix) = "type.spine.io";
This prefix is needed for recognizing binary data when it’s unpacked. Most likely, all types in a system would use the same prefix.
Then we see the standard Protobuf option for defining a Java package for the generated code:
option java_package="io.spine.helloworld.hello.command";
There are three parts of interest in this package name:
-
io.spine.helloworld
— this part represents the location of our Hello World “solution” as if it were hosted on the web athttps://helloworld.spine.io
. -
hello
— this is the package of the Hello context. In this example we have only one, but a real world app would have more. Each Bounded Context goes into a dedicated package. -
command
— this part gathers commands of the context under one package. We have only one command in this example, but in real world scenarios, with dozens of commands, it is convenient to gather them under a package.
The following standard proto file option defines the name for the outer class generated by
Protobuf Compiler for this .proto
file:
option java_outer_classname = "CommandsProto";
Outer classes are used by Protobuf implementation internally.
When the java_outer_classname
option is omitted, Protobuf Compiler would calculate the Java
class name taking the name of the corresponding .proto
file.
We recommend setting the name directly to make it straight. This also avoids possible name clashes
with the handcrafted code.
The next standard option instructs the Protobuf Compiler to put each generated Java type into a separate file. This way it would be easier to analyze dependencies of the code which uses these generated types.
option java_multiple_files = true;
The command for printing a text in a console is defined this way:
message Print {
// The login name of the computer user.
string username = 1;
// The text to print.
string text = 2 [(required) = true];
}
By convention, the first field of a command is the ID of the target entity. This field is required to have a non-empty value so that the command can be dispatched to the entity. In our case, we identify a console by the login name of the computer user.
The second field is marked as (required)
using the custom option imported in
the spine/options.proto
file above. This command does not make much sense if there is no text
to print.
In Protobuf a data type is either a message
(we can send it) or an enum
.
If you’re new to this language, you may want to look at the Proto3 Language Guide.
Now, let’s see how to define events.
The events.proto
file
Events are declared similarly to commands. The header of the file has:
- The
proto3
syntax declaration. - The
hello
package declaration. -
The import statement for custom Protobuf options used by the framework:
import "spine/options.proto";
- The same
(type_url_prefix)
we use in this project. -
A separate Java package for events of the context:
option java_package="io.spine.helloworld.hello.event";
-
The outer Java class for all types in this file:
option java_outer_classname = "EventsProto";
-
The instruction to put Java types into separate files.
option java_multiple_files = true;
The sole event in this project is declared this way:
message Printed {
// The login name of the user.
string username = 1 [(required) = true];
// The printed text.
string text = 2 [(required) = true];
}
The event tells which text was printed for a user. Both of the fields are marked as (required)
because the event does not make much sense if one of them is empty.
Unlike for commands, the framework does not assume that the first event field is always populated. This is so because default routing rules for commands and events are different. When an event is produced by some entity, it remembers the ID of this producer entity. By default, the framework uses the producer ID to route events to their target entities — if they have identifiers of the same type. If the type of producer ID does not match one of the target entity, then event fields are analyzed. It is also possible to set custom routing rules.
Now, let’s see the server-side data of the Hello context.
The console.proto
file
The header of the file is similar to those we saw in commands.proto
and events.proto
.
The difference is that we use server
for the proto and Java package names to make sure the
server-only is not used by the client code.
This file defines a single data type. It is the state of the entity handling the Print
command:
message Output {
option (entity) = { kind: PROCESS_MANAGER };
// The login name of the computer user.
string username = 1;
// Text lines of the screen.
repeated string lines = 2;
}
The option (entity)
tells us that this type is going to be used by a ProcessManager
.
The first field of the type holds the ID of this entity. The framework assumes such fields as
implicitly (required)
. Then goes the declaration of the remote screen output. The value of this
field is empty until something is printed on the screen. Therefore, it is not marked (required)
.
Now, let’s see how this data is used at the server-side.
The Console
class
The class is declared this way:
final class Console extends ProcessManager<String, Output, Output.Builder> {
The generic arguments passed to ProcessManager
are:
-
String
— the type of the ID of the entity. Remember the type of the first field of thePrint
command? -
Output
— the type of the entity state, which we reviewed in the previous section. -
Output.Builder
— the type used to modify the state of the entity. In Protobuf for Java, each message type has a specific builder type.
Handling the Print
command
The command is handled by this method:
@Assign
Printed handle(Print command) {
String username = command.getUsername();
String text = command.getText();
builder().setUsername(username)
.addLines(text);
println(username, text);
return Printed.newBuilder()
.setUsername(username)
.setText(command.getText())
.vBuild();
}
Let’s review the method in details.
The @Assign
annotation tells that we assign this method to handle the command which the
method accepts as the parameter. The method returns the event message Printed
.
The following code obtains the name of the user, and the text to print from the received command,
and then applies them to the state of the Process Manager. Instances of the Console
class store
the text printed for each user.
String username = command.getUsername();
String text = command.getText();
builder().setUsername(username)
.addLines(text);
Then we print the text to System.out
so that it becomes visible on the screen:
println(username, text);
This is done by the method the Console
class declares:
private void println(String userName, String text) {
String output = format("[%s] %s", userName, text);
System.out.println(output);
}
Then, the command-handling method concludes by producing the event message:
return Printed.newBuilder()
.setUsername(username)
.setText(command.getText())
.vBuild();
The vBuild()
call validates and builds the message. This method is generated by Spine Model
Compiler. For instructions on adding validation attributes to your model please see
Validation User Guide.
After the event is generated, it is posted to the EventBus
and delivered to
subscribers automatically. You don’t need to write any code for this.
Now, let’s see how the Console
Process Manager is exposed to the outer world so that it can
receive commands.
Assembling the Hello context
Let’s open the HelloContext
class. The first thing of interest in this class is the declaration of
the name of the Bounded Context:
static final String NAME = "Hello";
This constant is used for creating the context (we will review it in a minute) and when annotating
the server-side code which belongs to the Hello context. This is done in package-info.java
using
the @BoundedContext
annotation:
@BoundedContext(HelloContext.NAME)
package io.spine.helloworld.server.hello;
The framework assumes that all entity classes belonging to this and nested packages belong to the Bounded Context with the name specified in the argument of the annotation. This arrangement is needed for routing events.
The second thing the HelloContext
does is creating a Builder for the Bounded Context:
public static BoundedContextBuilder newBuilder() {
return BoundedContext
.singleTenant(NAME)
.add(Console.class);
}
The context we create is single-tenant. It contains one entity type which we pass to the builder.
If an entity uses default routing rules for the incoming events and commands,
its type can be added to BoundedContextBuilder
directly. If custom routing rules are needed,
they are specified by a custom Repository
class. In this case, an instance of such Repository
is passed to BoundedContextBuilder
instead of the entity type managed by this Repository
.
Once we assembled the Bounded Context, let’s test it.
Testing the Hello context
Let’s open the HelloContextTest
suite. It is based on JUnit 5 and spine-testutil-server
library.
We already added JUnit dependency when defining the Gradle project.
The testImplementation
dependency for spine-testutil-server
is automatically added when
you enable Spine in your project using spine.enableJava().server()
. So, we’re good to go testing.
The class of the test suite extends the abstract base called ContextAwareTest
:
@DisplayName("Hello context should")
class HelloContextTest extends ContextAwareTest {
The base class is responsible for creation of a test fixture for a Bounded Context under
the test before each test, and for disposing the fixture after. The fixture is accessed using
the context()
method.
We pass the Hello Context for testing using its builder by implementing the abstract method
contextBuilder()
inherited from ContextAwareTest
:
@Override
protected BoundedContextBuilder contextBuilder() {
return HelloContext.newBuilder();
}
Now we can get down to the tests. The suite verifiers the outcome of the Print
command.
The test methods are gathered under the nested class called
PrintCommand
. The class holds the reference to the command as its field:
@Nested
@DisplayName("handle the `Print` command")
class PrintCommand {
private Print command;
Sending the command
The command is created and sent to the test fixture before each test method:
@BeforeEach
void sendCommand() {
command = Print.newBuilder()
.setUsername(randomString())
.setText(randomString())
.vBuild();
context().receivesCommand(command);
}
For test values we use statically imported randomString()
method of the TestValues
utility class
provided by the spine-testutil-server
library. We use the context()
method provided by
ContextAwareTest
for obtaining the reference of the test fixture of the Bounded Context
under the test.
Now we need to test that handling the command produces the event, and the handling entity updates its state.
Testing creation of the event
Testing that an event was generated is quite simple. We create the expected event and assert it with the test fixture:
@Test @DisplayName("emitting the `Printed` event")
void event() {
Printed expected = Printed.newBuilder()
.setUsername(command.getUsername())
.setText(command.getText())
.build();
context().assertEvent(expected);
}
Testing entity state update
For testing the state of the Console
Process Manager was updated, we construct the expected
state and pass it to the assertState()
method of the test fixture:
@Test @DisplayName("updating the `Console` entity")
void entity() {
Output expected = Output.newBuilder()
.setUsername(command.getUsername())
.addLines(command.getText())
.vBuild();
context().assertState(command.getUsername(), expected);
}
The first argument passed to the assertState()
method is the ID of the entity the state of which
we test. The second argument is the expected state.
Now as we’ve checked that our Bounded Context works correctly, let’s expose it in the server-side application.
The server-side application
Let’s open the Server
class of our example application suite. The static initialization of
the class configures the server environment:
static {
configureEnvironment();
}
Configuring the environment
The configureEnvironment()
method initializes the Production
environment of this example with
the settings that are normally used for testing:
private static void configureEnvironment() {
ServerEnvironment.when(Production.class)
.use(InMemoryStorageFactory.newInstance())
.use(Delivery.localAsync())
.use(InMemoryTransportFactory.newInstance());
}
A real-world application would use StorageFactory
and TransportFactory
instances that correspond
to a database, and a messaging system used by the application.
The constructor
The implementation of the Server
class wraps around the class io.spine.server.Server
provided
by the framework. This API is for exposing BoundedContext
s in a server-side application.
This is what our Server
class does in the constructor:
public Server(String serverName) {
this.server = inProcess(serverName)
.add(HelloContext.newBuilder())
.build();
}
The constructor accepts the name which is used for connecting clients. This name
is passed to the inProcess()
factory method of the io.spine.server.Server
class.
In-process gRPC communications are normally used for testing.
This example uses in-process client/server arrangement in the production code for
the sake of simplicity. A real-world application would use a Server
instance exposed
via a TCP/IP port.
Once we have the Server.Builder
instance returned by the inProcess()
method,
we add the Hello Context via its builder to the constructed Server
instance.
Start and shutdown
The remaining code of our Server
class declares start()
and shutdown()
methods that
simply delegate calls to the wrapped io.spine.server.Server
instance.
Now we have the server, but how does the client side look like?
The client code
Similarly to Server
, the Client
class of our example application wraps around
the io.spine.client.Client
API provided by the spine-client
library:
public final class Client {
private final io.spine.client.Client client;
The io.spine:spine-client
library is provided
to the example application project as a transitive dependency of
the io.spine:spine-server
library, which is added to the project when you do
spine.enableJava().server()
in your Gradle project.
Then, the Client
class declares a field for keeping subscriptions to the results of a command
execution. We’ll see how this field is used in a minute.
private @Nullable ImmutableSet<Subscription> subscriptions;
First of all, let’s see how the Client
instances are created.
The constructor
public Client(String serverName) {
this.client = inProcess(serverName)
.shutdownTimeout(2, TimeUnit.SECONDS)
.build();
}
Again, similarly to a Server
, a Client
is created using in-process connection to a named server.
The shutdownTimeout
parameter configures the amount of time allowed for completing ongoing
client-server communications.
Now, let’s review the main thing the Client
class does, sending the Print
command.
Sending the command
public void sendCommand() {
String userName = System.getProperty("user.name");
Print commandMessage =
Print.newBuilder()
.setUsername(userName)
.setText("Hello World!")
.vBuild();
this.subscriptions =
client.asGuest()
.command(commandMessage)
.observe(Printed.class, this::onPrinted)
.post();
}
The method does three things:
- Creates a command message using the login name of the current computer user.
- Subscribes to the
Printed
event which will be generated as the result of handling the command we are going to post (and only this instance of the command, not allPrint
commands). - Posts the command.
Let’s review subscribing and posting in details.
The client.asGuest()
call starts composing a client request. For the sake of simplicity,
we do this on behalf of a user who is not logged in. A real-world application would send
most of the commands using client.onBehalfOf(UserId)
.
The command(commandMessage)
call tells we want to send a command with the passed message.
Then, we subscribe to the Printed
event which would be generated as the result of handling
the command. The events obtained from the server would be passed to the onPrinted()
method
of the Client
class.
The post()
method sends the command to the server, returning the set with one Subscription
to the Printed
event. We store the returned set in the field to use later for cancelling
the subscription.
Handling the event
When the client code receives the Printed
event, it prints its data and then
cancels the subscription:
private void onPrinted(Printed event) {
printEvent(event);
cancelSubscriptions();
}
There’s not much exciting about the printing part.
private void printEvent(EventMessage e) {
String out = format(
"The client received the event: %s%s",
e.getClass()
.getName(),
toCompactJson(e)
);
System.out.println(out);
}
The only interesting thing here is the statically imported Json.toCompactJson()
call which
converts the event message to a String
. The method is available from the Json
utility class
provided by the framework. The utility does heavy lifting of the conversion which involves knowing
all Protobuf types available in the project.
Cancelling the subscription, if any, iterates through the set passing each of them to
the Client.subscriptions()
API for the cancellation:
private void cancelSubscriptions() {
if (subscriptions != null) {
subscriptions.forEach(s -> client.subscriptions().cancel(s));
subscriptions = null;
}
}
Once we finish with the cancellation, we clear the subscriptions
field.
Closing the client
The close()
method simply delegates to the method of the io.spine.client.Client
class.
We also need to tell the calling code if the client has finished its job.
This is what the isDone()
method is for:
public boolean isDone() {
return client
.subscriptions()
.isEmpty();
}
The method checks active subscriptions. If there are none, we got the event and cleared its subscription.
Now, let’s put it all together.
Orchestrating the example
Let’s review the Example
class and its main()
method. It simulates client-server communication
scenario.
public static void main(String[] args) {
String serverName = Identifier.newUuid();
Server server = new Server(serverName);
Client client = null;
try {
server.start();
client = new Client(serverName);
client.sendCommand();
while (!client.isDone()) {
sleepUninterruptibly(Duration.ofMillis(100));
}
} catch (IOException e) {
onError(e);
} finally {
if (client != null) {
client.close();
}
server.shutdown();
}
}
The first thing the method does is creating a Server
using a random name.
Then, the server is started. It is done in the try/catch/finally
block because the start()
method may throw IOException
in case of communication problems reported by gRPC.
The catch
block calls the onError()
method of the Example
class which simply prints
the exception. A real-world application would use more sophisticated exception handling.
After the Server
is started, we create a Client
using the name of the server generated before.
Then, the client sends the command, and we wait until the client finishes its job using
the statically imported method sleepUninterruptibly()
provided by Guava.
The main()
method finishes by closing the client, and shutting down the server.
Summary
Although the “business case” of this example is trivial, it shows basic steps of development of a solution based on Spine Event Engine framework.
The development starts with the discovery of the business using EventStorming or another learning approach.
Then, we select a Bounded Context and define events, commands, Value Objects, and entity states
using Protobuf. Using these .proto
files Spine Model Compiler generates the code implementing
the defined data types.
After that, we add the business logic for handling commands or events in entity classes derived from
Aggregate
, ProcessManager
, or Projection
.
Then, these entity types are assembled into a Bounded Context and tested as a whole. A test suite sends signals (i.e. commands or events) to the implementation of the Bounded Context and verifies the changes manifested as events or new entity states.
Once the Bounded Context is tested, it is added to a Server
which gathers all the Bounded Contexts
of the solution and accepts incoming requests from the client applications.
Client applications send commands to modify the business model and subscribe to messages like events that reflect the changes of the model.
Once handling of all commands and events of the selected Bounded Context is done, another context is selected for the development. The process is repeated until all the contexts are implemented.