Skip to content

Powerful event-bus optimized for high throughput in multi-threaded applications. Features: Sync and Async event publication, weak/strong references, event filtering, annotation driven

License

Notifications You must be signed in to change notification settings

bennidi/mbassador

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

build status maven central javadoc wiki

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.

wiki

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

Usage

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:

  1. Zero‑configuration listener discovery via classpath/package auto‑scanning
  2. 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

  1. discovering class names in the given packages (directory or JAR) and
  2. 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 container managed environments where listeners are created and destroyed by frameworks, i.e. Spring, Guice etc. Just add everything to the bus, it will ignore objects without handlers and automatically clean-up orphaned weak references after the garbage collector has done its job.

Instead of using weak references, a listener can be configured to be referenced using strong references using @Listener(references=References.Strong). Strongly referenced listeners will stick around until explicitly unsubscribed.

Message filtering

MBassador offers type-safe message filtering using lambda-compatible filter classes. Filters are configured using the @Filter annotation and multiple filters can be attached to a single message handler. Filters implement the IMessageFilter functional interface, allowing for clean, type-safe filtering logic. Messages that have matching handlers but do not pass the configured filters result in the publication of a FilteredMessage object which wraps the original message. FilteredMessage events can be handled by registering listeners that handle FilteredMessage.

Filters can be reused across handlers by wrapping them in custom annotations (available since version 1.3.1)

    public static final class RejectAllFilter implements IMessageFilter {

        @Override
        public boolean accepts(Object event,  SubscriptionContext context) {
            return false;
        }
    }

    @IncludeFilters({@Filter(RejectAllFilter.class)})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RejectAll {}
    
    public static class FilteredMessageListener{
    
        // will cause republication of a FilteredEvent
        @Handler
        @RejectAll
        public void handleNone(Object any){
            FilteredEventCounter.incrementAndGet();
        }

        
    }

Interface-based handler definitions

MBassador supports defining message handlers on interface methods. Implementing classes automatically inherit these handler configurations, making it easy to create reusable handler contracts and reducing boilerplate code.

Basic example:

// Define the handler contract in an interface
interface EventProcessor {
    @Handler
    void processEvent(MyEvent event);
}

// Implementing class automatically gets the handler registration
class MyEventProcessor implements EventProcessor {
    @Override
    public void processEvent(MyEvent event) {
        // Handler is automatically registered via interface
        // No @Handler annotation needed here
    }
}

Precedence rules:

  1. Class annotations override interface annotations - If both the interface method and the implementing class method have @Handler, the class annotation takes precedence
  2. Last interface wins - When implementing multiple interfaces with the same method signature (diamond problem), the last interface in the implements clause wins
  3. Filters are inherited - Filter annotations on interface methods are also inherited by implementing classes

Advanced example:

// Interface with filters and priority
interface PrioritizedHandler {
    @Handler(priority = 10, filters = @Filter(ImportantMessageFilter.class))
    void handleImportant(Message msg);
}

// Class can override with its own configuration
class CustomHandler implements PrioritizedHandler {
    @Override
    @Handler(priority = 5)  // This takes precedence over interface
    public void handleImportant(Message msg) {
        // Uses priority = 5, not priority = 10
    }
}

// Or inherit completely from interface
class DefaultHandler implements PrioritizedHandler {
    @Override  // No @Handler needed
    public void handleImportant(Message msg) {
        // Inherits priority = 10 and filter from interface
    }
}

This feature is particularly useful for:

  • Creating handler contracts in libraries/frameworks
  • Reducing code duplication across similar listeners
  • Enforcing consistent handler configurations
  • Building plugin architectures with predefined handler interfaces

Enveloped messages

Message handlers can declare to receive an enveloped message using Enveloped. The envelope can wrap different types of messages to allow a single handler to handle multiple, unrelated message types.

Handler priorities

A handler can be associated with a priority to influence the order in which messages are delivered when multiple matching handlers exist

Custom error handling

Errors during message delivery are sent to all registered error handlers which can be added to the bus as necessary.

Extensibility

MBassador is designed to be extensible with custom implementations of various components like message dispatchers and handler invocations (using the decorator pattern), metadata reader (you can add your own annotations) and factories for different kinds of objects. A configuration object is used to customize the different configurable parts, see Features

Installation

Requirements: MBassador now utilizes modern Java features (Class-File API) and requires Java 24 (or a compatible newer release).

MBassador is available from the Maven Central Repository using the following coordinates:

<dependency>
    <groupId>net.engio</groupId>
    <artifactId>mbassador</artifactId>
    <version>{see.git.tags.for.latest.version}</version>
</dependency>

You can also download binary release and javadoc from the maven central repository. Of course you can always clone the repository and build from source.

Documentation

There is ongoing effort to extend documentation and provide code samples and detailed explanations of how the message bus works. Code samples can also be found in the various test cases. Please read about the terminology used in this project to avoid confusion and misunderstanding.

Integrations

There is a spring-extension available to support CDI-like transactional message sending in a Spring environment. This is a good example of integration with other frameworks. Another example is the Guice integration.

Credits

The initial inspiration for creating this component comes from Google Guava's event bus implementation. I liked the simplicity of its design and I trust in the code quality of google libraries. Unfortunately it uses strong references only.

Thanks to all contributors, especially

Many thanks also to ej-technologies for providing an open source license of JProfiler and Jetbrains for a license of IntelliJ IDEA

OSS used by MBassador: jUnit | maven | mockito | slf4j

Contribute

Pick an issue from the list of open issues and start implementing. Make your PRs small and provide test code! Take a look at this issue for a good example.

Note: Due to the complexity of the data structure and synchronization code it took quite a while to get a stable core. New features will only be implemented if they do not require significant modification to the core. The primary focus of MBassador is to provide high-performance extended pub/sub.

Sample code and documentation are both very appreciated contributions. Especially integration with different frameworks is of great value. Feel free and welcome to create Wiki pages to share your code and ideas. Example: Guice integration

License

This project is distributed under the terms of the MIT License. See file "LICENSE" for further reference.

About

Powerful event-bus optimized for high throughput in multi-threaded applications. Features: Sync and Async event publication, weak/strong references, event filtering, annotation driven

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 22