In the previous lesson we set a property in a HelloWorld
data object and committed it, then, when handling the event, we retrieved the object’s property and displayed it. That means that we acted as both a Producer and a Consumer of the object. Since the application implemented both roles we did not have to worry about communication.
Obviously, this is not a common scenario. Normally you would have a service-mesh in which some microservices act as Producers and others act as Consumers, communicating over some kind of a communication channel and protocol. Frequently a microservice acts as a producer of some types of data and a consumer of others, but the idea is the same.
In this lesson we keep with the HelloWorld
application, but we will have two of them – HelloWorldProducer
and HelloWorldConsumer
. The first produces and commits the HelloWorld
data object, while the second acts upon receiving an object event and displays the greeting set by the producer, as in the previous lesson. We will also learn how to connect the two applications over the network (spoiler: this will not require even a single line of code).
We will have two projects – hello-world-producer
and hello-world-consumer
. Needless to say, both shall include the Spiderwiz dependency as in lesson 1 (we will not mention it again in this tutorial). Each of the applications has its own Main
class. Let’s see first HelloWorldProducerMain.java
:
package org.spiderwiz.tutorial.lesson2.producer; import java.util.List; import org.spiderwiz.core.DataObject; import org.spiderwiz.core.Main; import org.spiderwiz.tutorial.objectLib.HelloWorld; /** * Provides the entry point of the application. Initializes and executes the Spiderwiz framework. */ public class HelloWorldProducerMain extends Main{ private static final String ROOT_DIRECTORY = ""; private static final String CONF_FILENAME = "hello-world-producer.conf"; private static final String APP_NAME = "Hello World Producer"; private static final String APP_VERSION = "Z1.01"; // Version Z1.01: Initial version /** * Class constructor with constant parameters. */ public HelloWorldProducerMain() { super(ROOT_DIRECTORY, CONF_FILENAME, APP_NAME, APP_VERSION); } /** * Application entry point. Instantiate the class, initialize the instance, then call a command-line hook that would shut down * the application when "exit" is typed. * * @param args the command line arguments. Not used in this application. */ public static void main(String[] args) { HelloWorldProducerMain main = new HelloWorldProducerMain(); if (main.init()) main.commandLineHook(); } /** * @return the list of produced objects, in this case HelloWorld is the only one. */ @Override protected String[] getProducedObjects() { return new String[]{HelloWorld.ObjectCode}; } /** * @return the list of consumed objects, in this case we do not consume any so we return an empty list. */ @Override protected String[] getConsumedObjects() { return new String[]{}; } /** * Add HelloWorld class to the object factory list of this application. * @param factoryList */ @Override protected void populateObjectFactory(List<Class<? extends DataObject>> factoryList) { super.populateObjectFactory(factoryList); factoryList.add(HelloWorld.class); } /** * Create a HelloWorld object, set its field and commit it. */ @Override protected void postStart() { try { HelloWorld helloWorld = createTopLevelObject(HelloWorld.class, null); helloWorld.setSayHello("Hello World"); helloWorld.commit(); return; } catch (NoSuchFieldException | IllegalAccessException ex) { ex.printStackTrace(); } } }
Beyond the obvious
changes in APP_NAME
and CONF_FILENAME
, you can see (line 40)
that the application is still a producer of HelloWorld
objects. However in getConsumedObjects()
(line 48) we return an empty list because the application consumes nothing. The
rest of the code is identical to the code of the previous lesson.
The sharp-eyed among you
may notice another small change. While in the previous lesson HelloWorld
class was defined in the same
package as HelloWorldMain
, here we
import it from org.spiderwiz.tutorial.objectLib
(line 6). The reason is that from now on we will have multiple projects sharing
the same data object classes
therefore we have created a class library that can be shared by other projects
– producers and consumers alike. We store HeloWorld
in that library so that it can be used also by the other project that we are
going to build in this lesson – hello-world-consumer
.
Before we start with it let’s first look at the new HelloWorld
class, as it differs a bit from the previous lesson.
package org.spiderwiz.tutorial.objectLib; import org.spiderwiz.annotation.WizField; import org.spiderwiz.core.DataObject; /** * Implements HelloWorld data object. */ public class HelloWorld extends DataObject{ /** * Mandatory public static field for all data objects. */ public final static String ObjectCode = "HLWRLD"; @WizField private String sayHello; public String getSayHello() { return sayHello; } public void setSayHello(String sayHello) { this.sayHello = sayHello; } /** * @return null as this is a root object. */ @Override protected String getParentCode() { return null; } /** * @return true as this object is disposable. */ @Override protected boolean isDisposable() { return false; } }
There are two changes
from the HelloWorld
class of the
previous lesson. First, we do not override onEvent()
.
The reason is that only the consumer needs to implement this method, while
classes defined in objectLib
library
are used by the producer and the consume alike.
The other change is the value returned by isDisposable() (38). This was modified from true
to false
. This is because we are going to have two applications that communicate, and obviously we cannot guarantee that they will start simultaneously. We also do not want to mess up with synchronization of the “hello world” greeting until the two applications connect. The solution is the Spiderwiz way. Right after the producer starts and initializes, it creates a HelloWorld
object and commits it. From now on, since the object is not disposable, it is conceptually “in the space”. Once the consumer starts and connects to the producer, it will encounter the event that is fired by the object.
In order to handle HelloWorld
events, hello-world-consumer
defines (in its own package) a new class – HelloWorldConsumer
that extends HelloWorld
. Here it is:
package org.spiderwiz.tutorial.lesson2.consumer; import org.spiderwiz.tutorial.objectLib.HelloWorld; /** * Extends HelloWorld and implement consumer code in onEvent(). */ public class HelloWorldConsumer extends HelloWorld { /** * Do the consumer work. * @return true to indicate that the event has been handled. */ @Override protected boolean onEvent() { System.out.println(getSayHello()); return true; } }
The extension adds onEvent() to the base HelloWorld
class. The method does exactly what it did in lesson 1.
We still need to see how the hello-world-consumer
project implements HelloWorldConsumerMain
. Here it goes:
package org.spiderwiz.tutorial.lesson2.consumer; import java.util.List; import org.spiderwiz.core.DataObject; import org.spiderwiz.core.Main; import org.spiderwiz.tutorial.objectLib.HelloWorld; /** * Provides the entry point of the application. Initializes and executes the Spiderwiz framework. */ public class HelloWorldConsumerMain extends Main{ private static final String ROOT_DIRECTORY = ""; private static final String CONF_FILENAME = "hello-world-consumer.conf"; private static final String APP_NAME = "Hello World Consumer"; private static final String APP_VERSION = "Z1.01"; // Version Z1.01: Initial version /** * Class constructor with constant parameters. */ public HelloWorldConsumerMain() { super(ROOT_DIRECTORY, CONF_FILENAME, APP_NAME, APP_VERSION); } /** * Application entry point. Instantiate the class, initialize the instance, then call a command-line hook that would shut down * the application when "exit" is typed. * * @param args the command line arguments. Not used in this application. */ public static void main(String[] args) { HelloWorldConsumerMain main = new HelloWorldConsumerMain(); if (main.init()) main.commandLineHook(); } /** * @return an empty list as we produce nothing. */ @Override protected String[] getProducedObjects() { return new String[]{}; } /** * @return the list of consumed objects, in this case HelloWorld is the only one. */ @Override protected String[] getConsumedObjects() { return new String[]{HelloWorld.ObjectCode}; } /** * Add HelloWorldConsumer class to the object factory list of this application. * @param factoryList */ @Override protected void populateObjectFactory(List<Class<? extends DataObject>> factoryList) { super.populateObjectFactory(factoryList); factoryList.add(HelloWorldConsumer.class); } }
As expected, here getProducedObjects()
(line 40) and getConsumedObjects()
(line 48) reverse their roles compared to HelloWorldProducerMain
– we produce nothing and consume HelloWorld
.
There is one more crucial
discrepancy. In populateObjectFactory()
(line 57) we register the HelloWorldConsumer
extension class rather than HelloWorld
.
This ensures that whenever a HelloWorld
object is received it will be handled by an instance of HelloWorldConsumer
and its onEvent()
method will be activated.
We are done with the code. Now the big question is how do the two applications communicate. The answer is: through the magic of Spiderwiz – two lines that we add to the configuration files – one per application. Let’s see that.
With the current Spiderwiz version we have two ways to connect the applications – TCP/IP sockets and WebSockets. Other methods can be implemented as plugins, as we will see later in this tutorial. In this lesson we will use TCP/IP (WebSockets are used in the next lesson). In Java, an implementation of TCP/IP connection involves two classes – Socket and ServerSocket. We do not need to use them here since they are already built into the Spiderwiz framework, but we need to configure their use.
Let’s assume the hello-world-producer
project is the
server. A TCP/IP server requires the allocation of a port number – ours will be
31415. Here is hello-world-producer.conf
:
[log folder]/tests/HelloWorldProducer/Logs [producer server-1]port=31415
The first line is a
definition of a log folder as in lesson 1. The second line is what interests us
now. We set up a communication server by using the producer server-
n property, when n is any number between 1 and 99 that is unique across all producer servers defined in the
configuration file. We can define as many as 99 producer servers and 99 consumer
servers for a single application. There is no material difference between producer servers and consumer servers. Both can be used for
both produced objects and consumed objects. The choice is mainly
cosmetic for the clarity of the network topology – applications that mostly
produce objects will be normally set as producers, while applications that
mostly consume objects will be normally set as consumers. It is very important,
however, that a consumer client always connects to a producer server and vice
versa because the hand shaking mechanism depends on this relationship.
Our application defines producer server-1
property. A TCP/IP
server is the default for Spiderwiz servers, so all we need to do is to specify
the port number served by this server, 31415 in our case.
It is left for us to set hello-world-consumer.conf
and then we
will be ready to run:
[log folder]/tests/HelloWorldConsumer/Logs [consumer-1]ip=localhost;port=31415
Here we configure a TCP/IP client. Similarly to servers, we can have up to 99 producer
clients and 99 consumer
clients for a single application, using the properties producer-
n and consumer-
n. Note that the value of n has no meaning except the unique identification of the property across the configuration file. Specifically there is no need to match the number of the client to the number of the server. They find each other by the IP address and the port number. However, as mentioned above, consumer
clients connect to producer
servers and vice versa.
Our consumer application
defines consumer-1
property. Again,
TCP/IP is the default for Spiderwiz clients, so we just need to specify a
server address and a port number. We use localhost
and 31415.
Everything is ready. The following table shows what happens when we run and stop the two applications few times in some occasional order:
Operation | Producer Console | Consumer Console |
---|---|---|
Run Producer | Hello World Producer ver. Z1.01 (core version Z2.30) has been initiated successfully | |
Run Consumer | A connection from 127.0.0.1 has been established | Hello World Consumer ver. Z1.01 (core version Z2.30) has been initiated successfully |
Exit Consumer | A connection from 127.0.0.1 has been dropped. Reason: Channel closed by other peer | exit |
Exit Producer | exit | |
Run Consumer | Hello World Consumer ver. Z1.01 (core version Z2.30) has been initiated successfully |
|
Run producer | Hello World Producer ver. Z1.01 (core version Z2.30) has been initiated successfully | |
After about one minute | A connection from 127.0.0.1 has been established | A connection from localhost:31415 has been established |
These logs are quite straightforward but there are few points to notice.
First, you see that hello-world-consumer
prints Hello World
regardless of whether the
producer or the consumer was launched first. You can also see that this is
prompted every time a connection between them is established, even if this
happens more than once during application lifetime.
You can also see the client-server mechanism applied by Spiderwiz. If a server is available when the client is launched, connection is established immediately. If a server is not available when a client is trying to connect to the designated port, connection fails and the client repeats the attempt every set time interval until success. By default this time interval is 1 minute, but it can be configured using the reconnection seconds
property in the application configuration file.
Before concluding this lesson let’s revisit the log files that were mentioned in the previous lesson. Besides the main log that was described there, classified by date and hour of the day, you will now see a new subfolder named Consumers
under /tests/HelloWorldProducer/Logs
and a new subfolder named Producers
under /tests/HelloWorldConsumer/Logs
. The former will contain a subfolder named “Hello World Consumer.127.0.0.1”
and the latter will contain a subfolder named “Hello World Producer.localhost”
. In each of these there will be a folder by the date and a file by the hour of the day. The following table compares the contents of these two files:
/tests/HelloWorldProducer/Logs/Consumers/Hello World Consumer.127.0.0.1/20-06-06/pm03.txt | /tests/HelloWorldConsumer/Logs/Producers/Hello World Producer.localhost/20-06-06/pm03.txt |
---|---|
15:27:10:580 Received a request from Hello World Consumer running at 127.0.0.1 to reset objects: HLWRLD | 15:27:13:498 $HLWRLD counter has been reset to zero. Previous value was 0 |
These log lines are produced when the two applications connect and the HelloWorld
object is sent from the Producer to the Consumer. The log of HelloWorldProducer
shows that it received a request from a consumer called Hello World Consumer
running on IP 127.0.0.1
to reset the objects whose ObjectCode
was HLWRLD
, and the log of HelloWorldConsumer
shows that it received the first object whose ObjectCode
was HLWRLD
from a producer called Hello World Producer
running on localhost
. This is indeed not much information, but this logging sub-system plays a major role when it comes to full-fledged data logging. Spiderwiz can be configured to log every message that passes between applications and then these log files can become pretty fat.
In the next lesson we will learn more about communication and the Spiderwiz network topology with a WebSocket based application hub.