EventStudio is yet another pure Java event bus implementation providing pub/sub pattern with events queue capabilities for intra-jvm event communication.
There are many different libraries with slightly different approaches implementing the pub/sub pattern but none of them was a perfect match for the problem I was trying to solve.
Working on a rich client software that can have multiple modules/plugins and:
- I wanted to be able to send events to the whole application (every listener subscribed on a given event)
- I wanted to be able to send events to a single module (every listener belonging to a given module and subscribed on a given event)
- I wanted the event to be queued in case no listener was listening and delivered as soon as a listener registers.
- I wanted listeners to be able to define at runtime what module they belong to. The idea was to create components (e.g. a SpecializedComboBox) listening for events (e.g. DisableEvent) and be able to reuse those components in different modules. I couldn't use the classical annotation approach with some topic/filter static value since every new instance of the component needed to be registered as listener for the module creating it.
The solution I found was to mimic a network of radio stations. For those familiar with RabbitMQ, the architecture is loosely resembling a DirectExchange where the event class is the binding key.
Listeners
s registers themselves on a Station
for a specific event class and publishers can broadcast their events to a Station
.
- A
Supervisor
can be registered on aStation
and it will be notified of every message going through theStation
before handing it to theListeners
s. - Publishers can broadcast to all the
Station
s - Undelivered events are stored in a queue and delivered as soon as
Listener
s for those events register.
- Branch 2 requires JDK 8 or higher, branch 3 requires JDK 11 or higher, branch 4 requires JDK 21 or higher
- Minimal dependencies (slf4j)
- Thread safe
- Fully unit tested
- Fully documented
- Super simple API
- Any pojo can be an event
- Programmatically add/remove support to add/remove listeners to/from a given station
- Annotation support to add listeners to a station with runtime name (any pojo can be a listener)
- Broadcast to a given station or every station
Supervisor
of aStation
to be notified of every message going through theStation
- Enqueue undelivered events and deliver them as soon as a listener registers
- Strong/Soft/Weak listeners reference support
- Prioritize listeners to enforce an execution order
- Quick and dirty, super simple veto system
- Helper methods to completely hide the
Station
abstraction and behave like a traditional pub/sub event bus - Strict event class matching (i.e. child events are not notified to listeners registered on parent events)
- Singleton pattern provided but not enforced
- Async listeners execution
<dependency>
<groupId>org.pdfsam</groupId>
<artifactId>eventstudio</artifactId>
<version>2.0.0</version>
</dependency>
public class ParentEvent {
}
public class ChildEvent extends ParentEvent {
}
public class ParentListener implements Listener<ParentEvent> {
public void onEvent(ParentEvent event) {
System.out.println("Got it!");
}
}
public class GenParentListener<T extends ParentEvent> implements Listener<T> {
public void onEvent(T event) {
System.out.println("Got it!");
}
}
Add them without specifying a Station
name for a traditional Pub/Sub pattern, hiding the Station
abstraction or add them to a specific station.
import static org.eventstudio.StaticStudio.eventStudio;
.....
public void initListenersTraditional() {
eventStudio().add(new ParentListener());
eventStudio().add(new ParentListener(), 1, ReferenceStrength.SOFT);
eventStudio().add(ChildEvent.class, new GenParentListener<ChildEvent>());
eventStudio().add(ChildEvent.class, new GenParentListener<ChildEvent>(),
0, ReferenceStrength.STRONG);
}
public void initListenersWithStation() {
eventStudio().add(new ParentListener(), "MyStation");
eventStudio().add(new ParentListener(), "MyStation", 1,
ReferenceStrength.WEAK);
eventStudio().add(ChildEvent.class, new GenParentListener<ChildEvent>(),
"MyStation");
eventStudio().add(ChildEvent.class, new GenParentListener<ChildEvent>(),
"MyStation", 0, ReferenceStrength.STRONG);
}
Define a supervisor:
public class MyStationSupervisor implements Supervisor{
public void inspect(Object event) {
System.out.println("All good!");
}
}
And register it:
public void initSupervisor() {
eventStudio().supervisor(new MyStationSupervisor());
eventStudio().supervisor(new MyStationSupervisor(), "MyStation");
}
ParentListener parentListener = new ParentListener();
GenParentListener<ChildEvent> genericListener = new GenParentListener<ChildEvent>();
....
public void removeListeners() {
eventStudio().remove(parentListener);
eventStudio().remove(parentListener, "MyStation");
eventStudio().remove(ChildEvent.class, genericListener, "MyStation");
eventStudio().remove(ChildEvent.class, genericListener);
}
Broadcast events without specifying a Station
name for a traditional Pub/Sub pattern, hiding the Station
abstraction or broadcast them to a specific station or to all the Station
s.
public void broadcast() {
eventStudio().broadcast(new ParentEvent());
eventStudio().broadcast(new ParentEvent(), "MyStation");
eventStudio().broadcastToEveryStation(new ChildEvent());
}
Clear a Station
, events won't be notified anymore.
public void clear() {
eventStudio().clear();
eventStudio().clear( "MyStation");
}
Every Listener
is allowed to veto the event it is listening for by throwing a BroadcastInterruptionException
, broadcast of the event will be interrupted and lower priority Listener
s won't receive it.
public class VetoListener implements Listener<ParentEvent> {
public void onEvent(ParentEvent event) {
if(some condition){
throw new BroadcastInterruptionException("You won't get any!");
}
System.out.println("Got it!");
}
}
Any annotated pojo can be registered as a listener. Use the @EventListener
annotation on a single parameter method.
public class Foo{
@EventListener
private void onParent(ParentEvent event){
System.out.println("Got it!");
}
@EventListener(priority=1, strength=ReferenceStrength.SOFT)
private void onChild(ChildEvent event){
System.out.println("Got it!");
}
}
and add the pojo to EventStudio. No Station
is specified, traditional Pub/Sub.
public void addAnnotated() {
eventStudio().addAnnotatedListeners(new Foo());
}
You can specify the Station
name of an annotated method or the Station
name for all the annotated methods of a pojo. Station
names defined on annotated methods have precedence over the pojo Station
definition. Enum
and String
values can be used as station name.
public class Foo {
@EventStation
private String station = "MyStation";
@EventListener
private void onParent(ParentEvent event) {
System.out.println("Got it!");
}
@EventListener(station = "AnotherStation")
private void onChild(ChildEvent event) {
System.out.println("Got it!");
}
}
And finally, also methods returning Enum
and String
can be annotated to retrieve a runtime value for the Station
name.
public class SpecializedComboBox {
private String moduleName;
public SpecializedComboBox(String moduleName) {
this.moduleName = moduleName;
}
@EventStation
private String stationName() {
return this.moduleName;
}
@EventListener
private void onDisable(DisableEvent event) {
this.setEnabled(false);
}
}
and add the pojo to EventStudio:
public void addAnnotated() {
eventStudio().addAnnotatedListeners(new SpecializedComboBox("ModuleA"));
eventStudio().addAnnotatedListeners(new SpecializedComboBox("ModuleB"));
}
and broadcast event to the module:
public void disableModuleA() {
eventStudio().broadcast(new DisableEvent(), "ModuleA");
}
Any annotated field (public, protected, default (package) access, and private fields, but excludes inherited fields) with the @EventStation
is discovered and used as Station
.
Any annotated method (including protected, default (package) access, private, public declared by the class or interface and those inherited from superclasses and superinterfaces) with the @EventStation
is discovered and used as Station
.
Any annotated method (including protected, default (package) access, private, public declared by the class or interface and those inherited from superclasses and superinterfaces) with the @EventListener
is discovered and used as Listener
.
In case of overridden methods no funky logic is applied trying to guess what the user intentions are, if you override an annotated method (either with @EventStation
or @EventListener
) and you want it to be discovered, it has to be annotated as well. In short if a method is overridden, annotations are not inherited. Consider making your annotated methods as final to avoid subclasses to override them causing unexpected behaviors.
public class ParentListener {
@EventListener
public void listen(String event) {
// do something
}
}
public class ChildListener extends ParentListener {
@Override
public void listen(String event) {
// do something else
}
}
....
eventStudio().addAnnotatedListeners(new ChildListener());
In the previous case no listener is discovered because the annotated method is overridden.
Annotated methods are reflectively invoked using dynamic method lookup as documented in The Java Language Specification, Second Edition, section 15.12.4.4. See the [javadoc] (http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/reflect/Method.html#invoke%28java.lang.Object,%20java.lang.Object...%29).