Einführung in MBassador

Einführung in MBassador

 

1. Überblick

Einfach ausgedrückt istMBassadora high-performance event bus utilizing the publish-subscribe semantics.

Nachrichten werden an einen oder mehrere Peers gesendet, ohne vorher zu wissen, wie viele Abonnenten es gibt oder wie sie die Nachricht verwenden.

2. Maven-Abhängigkeit

Bevor wir die Bibliothek verwenden können, müssen wir die Abhängigkeit vonmbassadorhinzufügen:


    net.engio
    mbassador
    1.3.1

3. Grundlegende Ereignisbehandlung

3.1. Einfaches Beispiel

Wir beginnen mit einem einfachen Beispiel für die Veröffentlichung einer Nachricht:

private MBassador dispatcher = new MBassador<>();
private String messageString;

@Before
public void prepareTests() {
    dispatcher.subscribe(this);
}

@Test
public void whenStringDispatched_thenHandleString() {
    dispatcher.post("TestString").now();

    assertNotNull(messageString);
    assertEquals("TestString", messageString);
}

@Handler
public void handleString(String message) {
    messageString = message;
}


Am Anfang dieser Testklasse sehen wir die Erstellung einesMBassador mit seinem Standardkonstruktor. Als nächstes rufen wir in der@Before-Methodesubscribe() auf und übergeben einen Verweis auf die Klasse selbst.

Insubscribe(), überprüft der Dispatcher den Teilnehmer auf@Handler Anmerkungen.

Und im ersten Test rufen wirdispatcher.post(…).now() auf, um die Nachricht zu versenden - was dazu führt, dasshandleString() aufgerufen wird.

Dieser erste Test demonstriert mehrere wichtige Konzepte. Any Object can be a subscriber, as long as it has one or more methods annotated with @Handler. Ein Teilnehmer kann eine beliebige Anzahl von Handlern haben.

Wir verwenden Testobjekte, die sich der Einfachheit halber selbst abonnieren. In den meisten Produktionsszenarien werden Nachrichten-Disponenten jedoch in andere Klassen als Verbraucher eingeteilt.

Handler-Methoden haben nur einen Eingabeparameter - die Nachricht - und können keine aktivierten Ausnahmen auslösen.

Ähnlich wie bei dersubscribe()-Methode akzeptiert die Post-Methode alleObject. DieseObject werden an Abonnenten geliefert.

Wenn eine Nachricht gesendet wird, wird sie an alle Listener gesendet, die den Nachrichtentyp abonniert haben.

Fügen wir einen weiteren Nachrichtenhandler hinzu und senden einen anderen Nachrichtentyp:

private Integer messageInteger;

@Test
public void whenIntegerDispatched_thenHandleInteger() {
    dispatcher.post(42).now();

    assertNull(messageString);
    assertNotNull(messageInteger);
    assertTrue(42 == messageInteger);
}

@Handler
public void handleInteger(Integer message) {
    messageInteger = message;
}

Wenn wir_ an _Integer versenden, wird erwartungsgemäßhandleInteger() aufgerufen undhandleString() nicht. Ein einzelner Dispatcher kann zum Senden mehrerer Nachrichtentypen verwendet werden.

3.2. Tote Nachrichten

Wohin geht eine Nachricht, wenn es keinen Handler dafür gibt? Fügen wir einen neuen Ereignishandler hinzu und senden dann einen dritten Nachrichtentyp:

private Object deadEvent;

@Test
public void whenLongDispatched_thenDeadEvent() {
    dispatcher.post(42L).now();

    assertNull(messageString);
    assertNull(messageInteger);
    assertNotNull(deadEvent);
    assertTrue(deadEvent instanceof Long);
    assertTrue(42L == (Long) deadEvent);
}

@Handler
public void handleDeadEvent(DeadMessage message) {
    deadEvent = message.getMessage();
}

In diesem Test versenden wirLong anstelle vonInteger.. WederhandleInteger() nochhandleString() werden aufgerufen, sondernhandleDeadEvent().

When there are no handlers for a message, it gets wrapped in a DeadMessage object. Da wir einen Handler fürDeadmessage hinzugefügt haben, erfassen wir ihn.

DeadMessage können ignoriert werden. Wenn eine Anwendung tote Nachrichten nicht verfolgen muss, kann sie nirgendwo hingehen.

4. Verwenden einer Ereignishierarchie

Das Senden vonString- undInteger-Ereignissen ist einschränkend. Erstellen wir einige Nachrichtenklassen:

public class Message {}

public class AckMessage extends Message {}

public class RejectMessage extends Message {
    int code;

    // setters and getters
}

Wir haben eine einfache Basisklasse und zwei Klassen, die sie erweitern.

4.1. Senden einer BasisklasseMessage

Wir beginnen mitMessage Ereignissen:

private MBassador dispatcher = new MBassador<>();

private Message message;
private AckMessage ackMessage;
private RejectMessage rejectMessage;

@Before
public void prepareTests() {
    dispatcher.subscribe(this);
}

@Test
public void whenMessageDispatched_thenMessageHandled() {
    dispatcher.post(new Message()).now();
    assertNotNull(message);
    assertNull(ackMessage);
    assertNull(rejectMessage);
}

@Handler
public void handleMessage(Message message) {
    this.message = message;
}

@Handler
public void handleRejectMessage(RejectMessage message) {
   rejectMessage = message;
}

@Handler
public void handleAckMessage(AckMessage message) {
    ackMessage = message;
}

Entdecken Sie MBassador - einen leistungsstarken Pub-Sub-Eventbus. Dies beschränkt uns auf die Verwendung vonMessages, fügt jedoch eine zusätzliche Ebene der Typensicherheit hinzu.

Wenn wir einMessage senden, empfängthandleMessage() es. Die anderen beiden Handler nicht.

4.2. Senden einer Unterklassen-Nachricht

Senden wir einRejectMessage:

@Test
public void whenRejectDispatched_thenMessageAndRejectHandled() {
    dispatcher.post(new RejectMessage()).now();

    assertNotNull(message);
    assertNotNull(rejectMessage);
    assertNull(ackMessage);
}

Wenn wir einRejectMessage senden, empfangen es sowohlhandleRejectMessage() als auchhandleMessage().

DaRejectMessageMessage, erweitert, hat es der Handler vonMessagezusätzlich zum Handler vonRejectMessageempfangen.

Überprüfen wir dieses Verhalten mitAckMessage:

@Test
public void whenAckDispatched_thenMessageAndAckHandled() {
    dispatcher.post(new AckMessage()).now();

    assertNotNull(message);
    assertNotNull(ackMessage);
    assertNull(rejectMessage);
}

Wie wir erwartet haben, empfangen sowohlhandleAckMessage() als auchhandleMessage(), wenn wirAckMessage senden.

5. Nachrichten filtern

Das Organisieren von Nachrichten nach Typ ist bereits eine leistungsstarke Funktion, aber wir können sie noch weiter filtern.

5.1. Filter nach Klasse und Unterklasse

Wenn wirRejectMessage oderAckMessage gepostet haben, haben wir das Ereignis sowohl im Ereignishandler für den jeweiligen Typ als auch in der Basisklasse erhalten.

Wir können dieses Problem mit der Typhierarchie lösen, indem wirMessage abstrakt machen und eine Klasse wieGenericMessage erstellen. Aber was ist, wenn wir diesen Luxus nicht haben?

Wir können Nachrichtenfilter verwenden:

private Message baseMessage;
private Message subMessage;

@Test
public void whenMessageDispatched_thenMessageFiltered() {
    dispatcher.post(new Message()).now();

    assertNotNull(baseMessage);
    assertNull(subMessage);
}

@Test
public void whenRejectDispatched_thenRejectFiltered() {
    dispatcher.post(new RejectMessage()).now();

    assertNotNull(subMessage);
    assertNull(baseMessage);
}

@Handler(filters = { @Filter(Filters.RejectSubtypes.class) })
public void handleBaseMessage(Message message) {
    this.baseMessage = message;
}

@Handler(filters = { @Filter(Filters.SubtypesOnly.class) })
public void handleSubMessage(Message message) {
    this.subMessage = message;
}

The filters parameter for the @Handler annotation accepts a Class that implements IMessageFilter. Die Bibliothek bietet zwei Beispiele:

DasFilters.RejectSubtypes macht, wie der Name schon sagt: Es filtert alle Subtypen heraus. In diesem Fall sehen wir, dassRejectMessage nicht vonhandleBaseMessage(). behandelt wird

DasFilters.SubtypesOnly macht auch, wie der Name schon sagt: Es filtert alle Basistypen heraus. In diesem Fall sehen wir, dassMessage nicht vonhandleSubMessage(). behandelt wird

5.2. IMessageFilter

DieFilters.RejectSubtypes und dieFilters.SubtypesOnly implementieren beideIMessageFilter.

RejectSubTypes vergleicht die Klasse der Nachricht mit ihren definierten Nachrichtentypen und lässt im Gegensatz zu Unterklassen nur Nachrichten zu, die einem ihrer Typen entsprechen.

5.3. Filtern mit Bedingungen

Glücklicherweise gibt es eine einfachere Möglichkeit, Nachrichten zu filtern. MBassador supports a subset of Java EL expressions as conditions for filtering messages.

Filtern wir dieString-Nachricht nach ihrer Länge:

private String testString;

@Test
public void whenLongStringDispatched_thenStringFiltered() {
    dispatcher.post("foobar!").now();

    assertNull(testString);
}

@Handler(condition = "msg.length() < 7")
public void handleStringMessage(String message) {
    this.testString = message;
}

Die Nachricht "foobar!" Ist sieben Zeichen lang und wird gefiltert. Senden wir ein kürzeresString:

@Test
public void whenShortStringDispatched_thenStringHandled() {
    dispatcher.post("foobar").now();

    assertNotNull(testString);
}

Jetzt ist die „Fußleiste“ nur noch sechs Zeichen lang und wird durchgereicht.

UnsereRejectMessage enthalten ein Feld mit einem Accessor. Schreiben wir einen Filter dafür:

private RejectMessage rejectMessage;

@Test
public void whenWrongRejectDispatched_thenRejectFiltered() {

    RejectMessage testReject = new RejectMessage();
    testReject.setCode(-1);

    dispatcher.post(testReject).now();

    assertNull(rejectMessage);
    assertNotNull(subMessage);
    assertEquals(-1, ((RejectMessage) subMessage).getCode());
}

@Handler(condition = "msg.getCode() != -1")
public void handleRejectMessage(RejectMessage rejectMessage) {
    this.rejectMessage = rejectMessage;
}

Auch hier können wir eine Methode für ein Objekt abfragen und die Nachricht entweder filtern oder nicht.

5.4. Erfassen Sie gefilterte Nachrichten

Ähnlich wie beiDeadEvents, möchten wir möglicherweise gefilterte Nachrichten erfassen und verarbeiten. Es gibt auch einen speziellen Mechanismus zum Erfassen gefilterter Ereignisse. Filtered events are treated differently from “dead” events.

Schreiben wir einen Test, der dies veranschaulicht:

private String testString;
private FilteredMessage filteredMessage;
private DeadMessage deadMessage;

@Test
public void whenLongStringDispatched_thenStringFiltered() {
    dispatcher.post("foobar!").now();

    assertNull(testString);
    assertNotNull(filteredMessage);
    assertTrue(filteredMessage.getMessage() instanceof String);
    assertNull(deadMessage);
}

@Handler(condition = "msg.length() < 7")
public void handleStringMessage(String message) {
    this.testString = message;
}

@Handler
public void handleFilterMessage(FilteredMessage message) {
    this.filteredMessage = message;
}

@Handler
public void handleDeadMessage(DeadMessage deadMessage) {
    this.deadMessage = deadMessage;
}

Mit der Hinzufügung einesFilteredMessage-Handlers können wirStrings verfolgen, die aufgrund ihrer Länge gefiltert werden. DasfilterMessage enthält unsere zu langenString, währenddeadMessagenull. bleibt

6. Asynchroner Nachrichtenversand und -verarbeitung

Bisher haben alle unsere Beispiele den synchronen Nachrichtenversand verwendet. Wenn wirpost.now() aufriefen, wurden die Nachrichten an jeden Handler in demselben Thread übermittelt, von dem aus wirpost() aufgerufen haben.

6.1. Asynchroner Versand

MBassador.post() gibtSyncAsyncPostCommand zurück. Diese Klasse bietet verschiedene Methoden an, darunter:

  • now() - Nachrichten synchron versenden; Der Anruf wird blockiert, bis alle Nachrichten zugestellt wurden

  • asynchronously() - führt die Nachrichtenveröffentlichung asynchron aus

Verwenden wir den asynchronen Versand in einer Beispielklasse. In diesen Tests verwenden wirAwaitility, um den Code zu vereinfachen:

private MBassador dispatcher = new MBassador<>();
private String testString;
private AtomicBoolean ready = new AtomicBoolean(false);

@Test
public void whenAsyncDispatched_thenMessageReceived() {
    dispatcher.post("foobar").asynchronously();

    await().untilAtomic(ready, equalTo(true));
    assertNotNull(testString);
}

@Handler
public void handleStringMessage(String message) {
    this.testString = message;
    ready.set(true);
}

Wir rufen in diesem Testasynchronously() auf und verwenden einAtomicBoolean als Flag mitawait(), um darauf zu warten, dass der Zustellungsthread die Nachricht übermittelt.

Wenn wir den Aufruf vonawait() auskommentieren, besteht das Risiko, dass der Test fehlschlägt, da wirtestString überprüfen, bevor der Übermittlungsthread abgeschlossen ist.

6.2. Asynchroner Handler-Aufruf

Der asynchrone Versand ermöglicht es dem Nachrichtenanbieter, zur Nachrichtenverarbeitung zurückzukehren, bevor die Nachrichten an jeden Handler zugestellt werden, ruft jedoch weiterhin jeden Handler in der angegebenen Reihenfolge auf, und jeder Handler muss warten, bis der vorherige beendet ist.

Dies kann zu Problemen führen, wenn ein Handler eine teure Operation ausführt.

MBassador bietet einen Mechanismus für den asynchronen Handleraufruf. Hierfür konfigurierte Handler empfangen Nachrichten in ihrem Thread:

private Integer testInteger;
private String invocationThreadName;
private AtomicBoolean ready = new AtomicBoolean(false);

@Test
public void whenHandlerAsync_thenHandled() {
    dispatcher.post(42).now();

    await().untilAtomic(ready, equalTo(true));
    assertNotNull(testInteger);
    assertFalse(Thread.currentThread().getName().equals(invocationThreadName));
}

@Handler(delivery = Invoke.Asynchronously)
public void handleIntegerMessage(Integer message) {

    this.invocationThreadName = Thread.currentThread().getName();
    this.testInteger = message;
    ready.set(true);
}

Handler können einen asynchronen Aufruf mit der Eigenschaftdelivery = Invoke.Asynchronouslyin der AnnotationHandleranfordern. Wir überprüfen dies in unserem Test, indem wir dieThread-Namen in der Versandmethode und den Handler vergleichen.

7. MBassador anpassen

Bisher haben wir eine Instanz von MBassador mit der Standardkonfiguration verwendet. Das Verhalten des Dispatchers kann mit Anmerkungen geändert werden, ähnlich denen, die wir bisher gesehen haben. Wir werden einige weitere behandeln, um dieses Tutorial zu beenden.

7.1. Ausnahmebehandlung

Handler können keine geprüften Ausnahmen definieren. Stattdessen kann dem Dispatcher einIPublicationErrorHandler als Argument für seinen Konstruktor bereitgestellt werden:

public class MBassadorConfigurationTest
  implements IPublicationErrorHandler {

    private MBassador dispatcher;
    private String messageString;
    private Throwable errorCause;

    @Before
    public void prepareTests() {
        dispatcher = new MBassador(this);
        dispatcher.subscribe(this);
    }

    @Test
    public void whenErrorOccurs_thenErrorHandler() {
        dispatcher.post("Error").now();

        assertNull(messageString);
        assertNotNull(errorCause);
    }

    @Test
    public void whenNoErrorOccurs_thenStringHandler() {
        dispatcher.post("Error").now();

        assertNull(errorCause);
        assertNotNull(messageString);
    }

    @Handler
    public void handleString(String message) {
        if ("Error".equals(message)) {
            throw new Error("BOOM");
        }
        messageString = message;
    }

    @Override
    public void handleError(PublicationError error) {
        errorCause = error.getCause().getCause();
    }
}

WennhandleString()Error, auslöst, wird es inerrorCause. gespeichert

7.2. Handler-Priorität

Handlers are called in reverse order of how they are added, but this isn’t behavior we want to rely on. Selbst wenn Handler in ihren Threads aufgerufen werden können, müssen wir möglicherweise noch wissen, in welcher Reihenfolge sie aufgerufen werden.

Wir können die Priorität des Handlers explizit festlegen:

private LinkedList list = new LinkedList<>();

@Test
public void whenRejectDispatched_thenPriorityHandled() {
    dispatcher.post(new RejectMessage()).now();

    // Items should pop() off in reverse priority order
    assertTrue(1 == list.pop());
    assertTrue(3 == list.pop());
    assertTrue(5 == list.pop());
}

@Handler(priority = 5)
public void handleRejectMessage5(RejectMessage rejectMessage) {
    list.push(5);
}

@Handler(priority = 3)
public void handleRejectMessage3(RejectMessage rejectMessage) {
    list.push(3);
}

@Handler(priority = 2, rejectSubtypes = true)
public void handleMessage(Message rejectMessage)
    logger.error("Reject handler #3");
    list.push(3);
}

@Handler(priority = 0)
public void handleRejectMessage0(RejectMessage rejectMessage) {
    list.push(1);
}

Handler werden von der höchsten zur niedrigsten Priorität aufgerufen. Handler mit der Standardpriorität Null werden als letzte aufgerufen. Wir sehen, dass der Handlerpop()in umgekehrter Reihenfolge ausschaltet.

7.3. Subtypen auf einfache Weise ablehnen

Was ist mithandleMessage() im obigen Test passiert? Wir müssen nichtRejectSubTypes.class verwenden, um unsere Untertypen zu filtern.

RejectSubTypes ist ein boolesches Flag, das dieselbe Filterung wie die Klasse bietet, jedoch eine bessere Leistung als die Implementierung vonIMessageFilteraufweist.

Wir müssen die filterbasierte Implementierung jedoch weiterhin nur zum Akzeptieren von Untertypen verwenden.

8. Fazit

MBassador ist eine einfache und unkomplizierte Bibliothek zum Weitergeben von Nachrichten zwischen Objekten. Nachrichten können auf verschiedene Arten organisiert und synchron oder asynchron versendet werden.

Und wie immer ist das Beispiel inthis GitHub project verfügbar.