Introduction à MBassador

Introduction à MBassador

 

1. Vue d'ensemble

En termes simples,MBassador esta high-performance event bus utilizing the publish-subscribe semantics.

Les messages sont diffusés à un ou plusieurs pairs sans que l'on sache combien d'abonnés il y a ou comment ils l'utilisent.

2. Dépendance Maven

Avant de pouvoir utiliser la bibliothèque, nous devons ajouter la dépendancembassador:


    net.engio
    mbassador
    1.3.1

3. Gestion des événements de base

3.1. Exemple simple

Nous allons commencer par un exemple simple de publication d'un message:

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;
}


En haut de cette classe de test, nous voyons la création d'unMBassador avec son constructeur par défaut. Ensuite, dans la méthode@Before, nous appelonssubscribe() et passons une référence à la classe elle-même.

Danssubscribe(),, le répartiteur inspecte l'abonné pour les annotations de@Handler.

Et, dans le premier test, nous appelonsdispatcher.post(…).now() pour distribuer le message - ce qui entraîne l'appel dehandleString().

Ce test initial démontre plusieurs concepts importants. Any Object can be a subscriber, as long as it has one or more methods annotated with @Handler. Un abonné peut avoir n'importe quel nombre de gestionnaires.

Nous utilisons des objets de test qui s'abonnent à eux-mêmes par souci de simplicité, mais dans la plupart des scénarios de production, les répartiteurs de messages seront dans des classes différentes de celles des consommateurs.

Les méthodes de gestion n'ont qu'un seul paramètre d'entrée - le message, et ne peuvent pas lever d'exceptions cochées.

Similaire à la méthodesubscribe(), la méthode post accepte tous lesObject. CeObject est livré aux abonnés.

Lorsqu'un message est publié, il est remis à tous les écouteurs qui se sont abonnés au type de message.

Ajoutons un autre gestionnaire de messages et envoyons un type de message différent:

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;
}

Comme prévu, lorsque nous distribuons_ an _Integer,handleInteger() est appelé ethandleString() ne l'est pas. Un seul répartiteur peut être utilisé pour envoyer plusieurs types de message.

3.2. Messages morts

Alors, où va un message quand il n'y a pas de gestionnaire pour cela? Ajoutons un nouveau gestionnaire d'événements, puis envoyons un troisième type de message:

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();
}

Dans ce test, nous distribuons unLong au lieu d'unInteger. NihandleInteger() nihandleString() ne sont appelés, maishandleDeadEvent() l'est.

When there are no handlers for a message, it gets wrapped in a DeadMessage object. Depuis que nous avons ajouté un gestionnaire pourDeadmessage, nous le capturons.

DeadMessage peut être ignoré en toute sécurité; si une application n'a pas besoin de suivre les messages morts, elle peut être autorisée à aller nulle part.

4. Utilisation d'une hiérarchie d'événements

L'envoi d'événementsString etInteger est limité. Créons quelques classes de messages:

public class Message {}

public class AckMessage extends Message {}

public class RejectMessage extends Message {
    int code;

    // setters and getters
}

Nous avons une classe de base simple et deux classes qui l'étendent.

4.1. Envoi d'une classe de baseMessage

Nous allons commencer par les événementsMessage:

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;
}

Découvrez MBassador - un bus d’événements pub-sub hautes performances. Cela nous limite à utiliserMessages mais ajoute une couche supplémentaire de sécurité de type.

Lorsque nous envoyons unMessage,handleMessage() le reçoit. Les deux autres gestionnaires ne le font pas.

4.2. Envoi d'un message de sous-classe

Envoyons unRejectMessage:

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

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

Lorsque nous envoyons unRejectMessage à la foishandleRejectMessage() ethandleMessage() le reçoivent.

PuisqueRejectMessage étendMessage,, le gestionnaire deMessage l'a reçu, en plus du gestionnaire deRejectMessage.

Vérifions ce comportement avec unAckMessage:

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

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

Comme nous nous y attendions, lorsque nous envoyons unAckMessage, leshandleAckMessage() ethandleMessage() le reçoivent.

5. Filtrage des messages

Organiser les messages par type est déjà une fonctionnalité puissante, mais nous pouvons les filtrer encore plus.

5.1. Filtrer par classe et sous-classe

Lorsque nous avons publié unRejectMessage ouAckMessage, nous avons reçu l'événement à la fois dans le gestionnaire d'événements pour le type particulier et dans la classe de base.

Nous pouvons résoudre ce problème de hiérarchie de types en rendantMessage abstrait et en créant une classe telle queGenericMessage. Mais que faire si nous n’avons pas ce luxe?

Nous pouvons utiliser des filtres de messages:

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. La bibliothèque propose deux exemples:

LeFilters.RejectSubtypes fait comme son nom l'indique: il filtrera tous les sous-types. Dans ce cas, on voit queRejectMessage n'est pas manipulé parhandleBaseMessage().

LeFilters.SubtypesOnly fait aussi comme son nom l'indique: il filtrera tous les types de base. Dans ce cas, on voit queMessage n'est pas manipulé parhandleSubMessage().

5.2. IMessageFilter

LesFilters.RejectSubtypes et lesFilters.SubtypesOnly implémentent tous deux lesIMessageFilter.

RejectSubTypes compare la classe du message à ses types de messages définis et n'autorisera que les messages qui correspondent à l'un de ses types, par opposition à toutes les sous-classes.

5.3. Filtrer avec des conditions

Heureusement, il existe un moyen plus simple de filtrer les messages. MBassador supports a subset of Java EL expressions as conditions for filtering messages.

Filtrons un messageString en fonction de sa longueur:

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;
}

Le message «foobar!» Compte sept caractères et est filtré. Envoyons unString plus court:

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

    assertNotNull(testString);
}

Maintenant, la "foobar" ne fait que six caractères et est transmise.

NotreRejectMessage contient un champ avec un accesseur. Écrivons un filtre pour cela:

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;
}

Là encore, nous pouvons interroger une méthode sur un objet et filtrer le message ou non.

5.4. Capturer les messages filtrés

Similaire àDeadEvents,, nous souhaitons peut-être capturer et traiter les messages filtrés. Il existe également un mécanisme dédié à la capture des événements filtrés. Filtered events are treated differently from “dead” events.

Écrivons un test qui illustre ceci:

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;
}

Avec l'ajout d'un gestionnaireFilteredMessage, nous pouvons suivre lesStrings qui sont filtrés en raison de leur longueur. LefilterMessage contient nosString trop longs tandis quedeadMessage restenull.

6. Distribution et gestion des messages asynchrones

Jusqu'à présent, tous nos exemples ont utilisé l'envoi de messages synchrones; lorsque nous avons appelépost.now(), les messages ont été remis à chaque gestionnaire dans le même thread que nous avons appelépost().

6.1. Distribution asynchrone

LeMBassador.post() renvoie unSyncAsyncPostCommand. Cette classe propose plusieurs méthodes, notamment:

  • now() - envoie les messages de manière synchrone; l'appel sera bloqué jusqu'à ce que tous les messages aient été livrés

  • asynchronously() - exécute la publication du message de manière asynchrone

Utilisons la répartition asynchrone dans un exemple de classe. Nous utiliseronsAwaitility dans ces tests pour simplifier le code:

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);
}

Nous appelonsasynchronously() dans ce test et utilisons unAtomicBoolean comme indicateur avecawait() pour attendre que le thread de livraison délivre le message.

Si nous commentons l'appel àawait(), nous risquons que le test échoue, car nous vérifionstestString avant la fin du thread de livraison.

6.2. Invocation de gestionnaire asynchrone

La répartition asynchrone permet au fournisseur de messages de revenir au traitement des messages avant que les messages ne soient remis à chaque gestionnaire, mais il appelle toujours chaque gestionnaire dans l'ordre, et chaque gestionnaire doit attendre que le précédent se termine.

Cela peut entraîner des problèmes si un gestionnaire effectue une opération coûteuse.

MBassador fournit un mécanisme d’appel de gestionnaire asynchrone. Les gestionnaires configurés pour cela reçoivent des messages dans leur 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);
}

Les gestionnaires peuvent demander un appel asynchrone avec la propriétédelivery = Invoke.Asynchronously sur l'annotationHandler. Nous vérifions cela dans notre test en comparant les nomsThread dans la méthode de répartition et le gestionnaire.

7. Personnalisation de MBassador

Jusqu'à présent, nous avons utilisé une instance de MBassador avec sa configuration par défaut. Le comportement du répartiteur peut être modifié avec des annotations, similaires à celles que nous avons vues jusqu'à présent; nous en couvrirons quelques autres pour terminer ce didacticiel.

7.1. Gestion des exceptions

Les gestionnaires ne peuvent pas définir les exceptions vérifiées. Au lieu de cela, le répartiteur peut recevoir unIPublicationErrorHandler comme argument de son constructeur:

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();
    }
}

LorsquehandleString() lance unError,, il est enregistré danserrorCause.

7.2. Priorité du gestionnaire

Handlers are called in reverse order of how they are added, but this isn’t behavior we want to rely on. Même avec la possibilité d'appeler des gestionnaires dans leurs threads, nous pouvons encore avoir besoin de savoir dans quel ordre ils seront appelés.

Nous pouvons définir explicitement la priorité du gestionnaire:

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);
}

Les gestionnaires sont appelés de la priorité la plus élevée à la plus basse. Les gestionnaires avec la priorité par défaut, qui est zéro, sont appelés en dernier. Nous voyons que le gestionnaire numérotepop() off dans l'ordre inverse.

7.3. Rejeter les sous-types, en toute simplicité

Qu'est-il arrivé àhandleMessage() dans le test ci-dessus? Nous n'avons pas besoin d'utiliserRejectSubTypes.class pour filtrer nos sous-types.

RejectSubTypes est un indicateur booléen qui fournit le même filtrage que la classe, mais avec de meilleures performances que l'implémentation deIMessageFilter.

Nous devons toutefois utiliser l'implémentation basée sur les filtres pour accepter uniquement les sous-types.

8. Conclusion

MBassador est une bibliothèque simple et directe pour la transmission de messages entre objets. Les messages peuvent être organisés de différentes manières et être envoyés de manière synchrone ou asynchrone.

Et, comme toujours, l'exemple est disponible enthis GitHub project.