
This application note explores how KOS objects can communicate with each other using KOS listeners.
You should have completed the First One application note, as it builds the framework used in this and other app notes.
It’s common in software design for objects to communicate with each other. The most obvious way is through direct method calls. However, this approach can lead to an overly coupled solution that becomes difficult to maintain.
An alternative is to use messages. KOS provides an excellent mechanism to both send and receive messages using its ListenerList class.
With this technique, senders are unaware of which objects are receiving, and receivers are unaware of which objects are sending. Producers simply say "I send this type of message", while consumers say "I want to receive this type of message". This loose coupling between objects leads to easier development and maintenance.
Message
A message is simply a unit of communication that a source sends to one or more recipients. A message may or may not contain a data payload. Messages are also referred to as events. |
In this section, we’ll add code to our app note framework developed in the First One page.
We need to do three things:
Define what message is to be sent and what (if any) payload does it have
Define the receiver class or classes (the "listeners")
Define the sender class
Source code for this and most other Java reference pages is available on GitHub. |
We are going to create two messages:
one without a payload : somethingHappened()
, and
one with a payload : somethingHappenedWithData(SomeData someData)
.
Both are defined inside a single listener interface:
MessageListener
interfacepackage com.example.listeners;
public interface MessageListener {
void somethingHappened();
void somethingHappenedWithData(SomeData someData);
}
Here’s our payload, which is a simple data bean:
SomeData
payload beanpackage com.example.listeners;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SomeData {
private int id;
private String firstName;
private String lastName;
}
For our example, we are going to set up two receivers, Listener1
and Listener2
. Here’s the first listener:
Listener1
classpackage com.example.listeners;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Listener1 implements MessageListener { (1)
@Override
public void somethingHappened() { (2)
log.info("> Listener1.somethingHappened()");
}
@Override
public void somethingHappenedWithData(SomeData someData) { (3)
log.info("> Listener1.somethingHappenedWithData()");
log.info("> {}", someData);
}
}
1 | Implement the message listener interface |
2 | Override the first message |
3 | Override the second message |
As you can see, we are simply logging information when those methods are executed.
The second listener is identical to the first except for a couple log statements:
Listener2
classpackage com.example.listeners;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Listener2 implements MessageListener {
@Override
public void somethingHappened() {
log.info("> Listener2.somethingHappened()");
}
@Override
public void somethingHappenedWithData(SomeData someData) {
log.info("> Listener2.somethingHappenedWithData()");
log.info("> {}", someData);
}
}
These two listeners will now get called whenever any sender sends those messages.
Next, we create a message sender that we name Speaker
. Here it is:
Speaker
classpackage com.example.listeners;
import com.tccc.kos.commons.core.context.annotations.Autowired;
import com.tccc.kos.commons.util.ListenerList;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Speaker {
@Autowired
private ListenerList<MessageListener> listenerList; (1)
public void saySomething() {
log.info("> Speaker.saySomething()");
listenerList.fireEvent(MessageListener::somethingHappened); (2)
}
public void saySomethingWithData() {
log.info("> Speaker.saySomethingWithData()");
SomeData someData = new SomeData(23, "Fred", "Flintstone");
listenerList.fireEvent(listener -> listener.somethingHappenedWithData(someData)); (3)
}
public void saySomethingWithDataToSpecificClass() {
log.info("> Speaker.saySomethingWithDataToSpecificClass()");
SomeData someData1 = new SomeData(24, "Wilma", "Flintstone");
listenerList.fireEvent(Listener1.class, listener -> listener.somethingHappenedWithData(someData1)); (4)
SomeData someData2 = new SomeData(25, "Barney", "Rubble");
listenerList.fireEvent(Listener2.class, listener -> listener.somethingHappenedWithData(someData2)); (5)
}
}
1 | Autowire a ListenerList indicating the type of message it will handle (KOS instantiates this object) |
2 | Send the message that contains no payload to all listeners |
3 | Send the message with given payload to all listeners |
4 | Send a message to only the Listener1 class |
5 | Send a message to only the Listener2 class |
In order to execute our code, we need to modify our KOS application by adding a ListenerModule
class and making a tweak to MyKosApp
.
The ListenerModule
class initializes our application and then runs its code:
ListenerModule
classpackage com.example.listeners;
import com.example.Module;
import com.tccc.kos.commons.core.context.BeanContext;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ListenerModule implements Module {
@Override
public void init(BeanContext ctx) {
log.info("> ListenerModule.init()");
ctx.add(new Speaker()); (1)
ctx.add(new Listener1());
ctx.add(new Listener2());
}
@Override
@SneakyThrows
public void run() {
log.info("> ListenerModule.run()");
Speaker speaker = Objects.requireNonNull(ctx.getBean(Speaker.class)); (2)
Thread.sleep(1000);
speaker.saySomething(); (3)
Thread.sleep(1000);
speaker.saySomethingWithData(); (3)
Thread.sleep(1000);
speaker.saySomethingWithDataToSpecificClass(); (3)
}
}
1 | Create each of our objects and add them to the context |
2 | Grab a reference to the speaker class |
3 | Issue the three different calls to test our messaging |
Change the one line in our KOS application class as indicated below:
MyKosApp
classpackage com.example;
public class MyKosApp extends SystemApplication<BaseAppConfig> {
private Module module;
public void initModule() {
module = new ListenerModule(getCtx()); (1)
}
// . . .
}
1 | Modify this line to instantiate the ListenerModule |
Debug this program in your IDE and monitor the log output.
The application log output should look something like this (blank lines inserted for clarity):
ListernerModule
output> ====== BEGIN =========================================
> MyKosApp.load()
> ListenerModule.init()
> MyKosApp.start()
> ListenerModule.run()
> Speaker.saySomething() (1)
> Listener2.somethingHappened() (2)
> Listener1.somethingHappened()
> Speaker.saySomethingWithData() (3)
> Listener2.somethingHappenedWithData() (4)
> SomeData(id=23, firstName=Fred, lastName=Flintstone)
> Listener1.somethingHappenedWithData()
> SomeData(id=23, firstName=Fred, lastName=Flintstone)
> Speaker.saySomethingWithDataToSpecificClass() (5)
> Listener1.somethingHappenedWithData()
> SomeData(id=24, firstName=Wilma, lastName=Flintstone) (6)
> Listener2.somethingHappenedWithData()
> SomeData(id=25, firstName=Barney, lastName=Rubble)
> ====== END ===========================================
1 | The Speaker class saySomething() method is executed, which fires the somethingHappended() event |
2 | Both listeners receive that message (note the receive order is not guaranteed) |
3 | The Speaker class saySomethingWithData() method is executed, which fires the somethingHappenedWithData() event |
4 | Both listeners receive that message and display the same data |
5 | The Speaker class now sends a message only to Listener1 |
6 | The Speaker class now sends a message only to Listener2 |