Mbassador
Powerful event-bus optimized for high throughput in multi-threaded applications. Features: Sync and Async event publication, weak/strong references, event filtering, annotation driven
Install / Use
/learn @bennidi/MbassadorREADME
MBassador
MBassador is a light-weight, high-performance event bus implementing the publish subscribe pattern. It is designed for ease of use and aims to be feature rich and extensible while preserving resource efficiency and performance.
The core of MBassador is built around a custom data structure that provides non-blocking reads and minimized lock contention for writes such that performance degradation of concurrent read/write access is minimal. Benchmarks that illustrate the advantages of this design are available in this github repository.
The code is production ready: 86% instruction coverage, 82% branch coverage with randomized and concurrently run test sets, no major bug has been reported in the last 18 month. No modifications to the core will be made without thoroughly testing the code.
Usage | Features | Installation | Wiki | Release Notes | Integrations | Credits | Contribute | License
<h2>Usage</h2>Using MBassador in your project is very easy. Create as many instances of MBassador as you like (usually a singleton will do) bus = new MBassador(), mark and configure your message handlers with @Handler annotations and finally register the listeners at any MBassador instance bus.subscribe(aListener). Start sending messages to your listeners using one of MBassador's publication methods bus.post(message).now() or bus.post(message).asynchronously().
MBassador also supports optional auto-scanning of packages for listeners annotated with @Listener, allowing zero‑configuration discovery in larger applications.
As a first reference, consider this illustrative example. You might want to have a look at the collection of examples to see its features on more detail.
// Define your handlers
@Listener(references = References.Strong)
class SimpleFileListener{
@Handler
public void handle(File file){
// do something with the file
}
@Handler(delivery = Invoke.Asynchronously)
public void expensiveOperation(File file){
// do something with the file
}
@Handler(filters = @Filter(LargeFileFilter.class))
@Enveloped(messages = {HashMap.class, LinkedList.class})
public void handleLarge(MessageEnvelope envelope) {
// handle objects without common super type
}
static class LargeFileFilter implements IMessageFilter<MessageEnvelope> {
public boolean accepts(MessageEnvelope envelope, SubscriptionContext context) {
Object msg = envelope.getMessage();
if (msg instanceof Map) return ((Map) msg).size() >= 10000;
if (msg instanceof Collection) return ((Collection) msg).size() >= 10000;
return false;
}
}
}
// somewhere else in your code
MBassador bus = new MBassador();
bus.subscribe (new SimpleFileListener());
bus.post(new File("/tmp/smallfile.csv")).now();
bus.post(new File("/tmp/bigfile.csv")).asynchronously();
Auto-Scanning and Modern Handler Invocation
MBassador supports two complementary “modern” features:
- Zero‑configuration listener discovery via classpath/package auto‑scanning
- High‑performance handler invocation using
java.lang.invoke.MethodHandle
1. Traditional usage (backward compatible)
In the classic style you create a bus and manually subscribe listener instances:
MBassador<Event> bus = new MBassador<>();
// Manually constructed listener
bus.subscribe(new MyEventListener());
// Publish events
bus.post(new Event()).now();
This is 100% backward compatible with existing MBassador code. Listeners use @Handler as before, and nothing about registration or dispatch semantics changes.
2. Modern auto‑scanning usage (JDK 24+ Class-File API)
You can optionally enable auto‑scanning of packages for listener classes:
// Zero-config style: automatically scans given packages and subscribes found listeners
MBassador<Event> bus = new MBassador<>();
bus.autoScan("com.myapp.listeners", "org.company.handlers");
The constructor above will:
- Scan the given packages using the JDK Class‑File API (no class loading needed for discovery).
- Find all classes that:
- Have at least one method annotated with @Handler, and
- Are annotated (directly or via meta‑annotation) with @Listener.
- Instantiate them via their default constructor and subscribe them to the bus.
You can also trigger scanning manually on an existing bus:
MBassador<Event> bus = new MBassador<>();
// Later in application bootstrap
bus.autoScan("com.myapp.listeners", "org.company.handlers");
This pattern is inspired by the Dimension‑DI DependencyScanner: it uses a two‑phase process of
- discovering class names in the given packages (directory or JAR) and
- analyzing class bytes via the Class‑File API to detect @Handler methods, only loading classes that are actually needed.
Note: Only classes with an accessible no‑arg constructor can be auto‑instantiated. Classes without such a constructor will be skipped with a diagnostic message.
You can enable auto‑scanning by calling autoScan(...) on an existing bus.
By default, any class annotated with @Listener and having at least one @Handler method is a candidate for auto-scan. You can opt out by setting:
@Listener(autoScan = false)
public class InternalListener {
...
}
3. Mixed usage (auto‑scan + manual subscription)
Auto‑scan and manual subscription can be freely mixed:
MBassador<Event> bus = new MBassador<>();
// 1) Automatically discover and subscribe all listeners in the given packages
bus.autoScan("com.myapp.listeners");
// 2) Manually add specific listeners (e.g. programmatically constructed)
bus.subscribe(new SpecialEventListener());
// Both auto‑discovered and manually registered listeners will receive events
bus.post(new Event()).now();
This is useful when you want a convention‑based baseline (auto‑scanned listeners), plus a few explicit registrations for application‑specific or dynamically created handlers.
4. MethodHandle‑based handler invocation
By default, MBassador now uses a MethodHandle‑based handler invocation implementation:
@Handler(invocation = MethodHandleInvocation.class)
public void handle(MyEvent event) {
// ...
}
If no invocation is specified on @Handler, the default is MethodHandleInvocation, which internally uses java.lang.invoke.MethodHandle instead of reflective Method.invoke(...). This:
- Performs access checks once at handle creation time.
- Reduces invocation overhead versus traditional reflection.
- Preserves existing error‑handling semantics:
- Any exception thrown by a handler is wrapped in a PublicationError.
- All configured IPublicationErrorHandlers are invoked, exactly as before.
You can still plug in a custom invocation strategy by providing your own HandlerInvocation subclass and referencing it via the invocation attribute on @Handler.
Features
Annotation driven
|Annotation|Function|
|:-----|:-----|
|@Handler|Mark a method as message handler|
|@Listener|Can be used to customize listener wide configuration like the used reference type|
|@Enveloped|A message envelope can be used to pass messages of different types into a single handler|
|@Filter|Add filtering to prevent certain messages from being published|
Delivers everything, respects type hierarchy
Messages do not need to implement any interface and can be of any type. The class hierarchy of a message is considered during message delivery, such that handlers will also receive subtypes of the message type they consume for - e.g. a handler of Object.class receives everything. Messages that do not match any handler result in the publication of a DeadMessage object which wraps the original message. DeadMessage events can be handled by registering listeners that handle DeadMessage.
Synchronous and asynchronous message delivery
There are two types of (a-)synchronicity when using MBassador: message dispatch and handler invocation. Message dispatch
Synchronous dispatch means that the publish method blocks until all handlers have been processed. Note: This does not necessarily imply that each handler has been invoked and received the message - due to the possibility to combine synchronous dispatch with asynchronous handlers. This is the semantics of publish(Object obj) and post(Objec obj).now()
Asynchronous dispatch means that the publish method returns immediately and the message will be dispatched in another thread (fire and forget). This is the semantics of publishAsync(Object obj) and post(Objec obj).asynchronously()
Handler invocation
Synchronous handlers are invoked sequentially and from the same thread within a running publication. Asynchronous handlers means that the actual handler invocation is pushed to a queue that is processed by a pool of worker threads.
Configurable reference types
By default, MBassador uses weak references for listeners to relieve the programmer of the need to explicitly unsubscribe listeners that are not used anymore and avoid memory-leaks. This is very comfortable in c


