Contents

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, which hosts the context.

What you’ll need

  1. JDK version 8 or higher.
  2. Git.
  3. 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
Jun 04, 2020 5:04:55 PM io.spine.server.Server lambda$start$1
INFO: In-process server started with the name `318ea6c4-283e-4c43-b367-93310b703d31`.
[sanders] Hello World!
The client received the event: io.spine.helloworld.hello.event.Printed{"username":"sanders","text":"Hello World!"}
Jun 04, 2020 5:04:57 PM io.spine.server.Server shutdown
INFO: Shutting down the server...
Jun 04, 2020 5:04:57 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 “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 and gradlew.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;
  • tests.

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 the generated 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.5.8'
}

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:

  1. Sets the version of Java to 8.

  2. Adds generated code directories to IntelliJ IDEA module by applying the idea.gradle script plugin.

    The framework does not depend on IDEA or its Gradle plugin. We added this code because we use this IDE for development. If you use it too, you may want look into idea.gradle to configure your Spine-based projects similarly.

  3. Adds JUnit dependencies by applying the tests.gradle script plugin.

    We chose to extract this and previous scripts into separate files to simplify the code of build.gradle.

  4. Defines the sayHello task which runs the Example 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 code of these scripts.

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 the Print 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 just commands.proto like in our example.

  • events.proto — this file defines the Printed 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 at https://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:

// A command to print a text.
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 package for events of the context:

     option java_package="io.spine.helloworld.hello.event";
    
  • The outer 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:

// A text was printed.
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:

// The screen state of the user's console.
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 and 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:

  1. String — the type of the ID of the entity. Remember the type of the first field of the Print command?

  2. Output — the type of the entity state, which we reviewed in the previous section.

  3. Output.Builder — the type used to modify the state of the entity. In Protobuf for Java, each message type has a specific builder type.

To be continued…

Top