Введение в MB Ambassador

Введение в MB Ambassador

 

1. обзор

Проще говоря,MBassador - этоa high-performance event bus utilizing the publish-subscribe semantics.

Сообщения передаются одному или нескольким одноранговым узлам без предварительного знания количества подписчиков или того, как они используют сообщение.

2. Maven Dependency

Прежде чем мы сможем использовать библиотеку, нам нужно добавить зависимостьmbassador:


    net.engio
    mbassador
    1.3.1

3. Базовая обработка событий

3.1. Простой пример

Начнем с простого примера публикации сообщения:

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


В верхней части этого тестового класса мы видим созданиеMBassador с его конструктором по умолчанию. Затем в методе@Before мы вызываемsubscribe() и передаем ссылку на сам класс.

Вsubscribe(), диспетчер проверяет подписчика на наличие аннотаций@Handler.

И в первом тесте мы вызываемdispatcher.post(…).now() для отправки сообщения, что приводит к вызовуhandleString().

Этот первоначальный тест демонстрирует несколько важных концепций. Any Object can be a subscriber, as long as it has one or more methods annotated with @Handler. У подписчика может быть любое количество обработчиков.

Мы используем тестовые объекты, которые подписываются на самих себя для простоты, но в большинстве производственных сценариев диспетчеры сообщений будут принадлежать к классам, отличным от классов потребителей.

У методов-обработчиков есть только один входной параметр - сообщение, и они не могут генерировать проверенные исключения.

Подобно методуsubscribe(), метод post принимает любыеObject. ЭтотObject доставляется подписчикам.

Когда сообщение отправлено, оно доставляется всем слушателям, которые подписались на этот тип сообщения.

Давайте добавим еще один обработчик сообщений и отправим сообщение другого типа:

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

Как и ожидалось, когда мы отправляем_ an _Integer, вызываетсяhandleInteger(), аhandleString() - нет. Один диспетчер может использоваться для отправки более одного типа сообщений.

3.2. Мертвые сообщения

Так куда же уходит сообщение, когда для него нет обработчика? Давайте добавим новый обработчик событий, а затем отправим сообщение третьего типа:

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

В этом тесте мы отправляемLong вместоInteger.. НиhandleInteger(), ниhandleString() не вызываются, ноhandleDeadEvent().

When there are no handlers for a message, it gets wrapped in a DeadMessage object. Поскольку мы добавили обработчик дляDeadmessage, мы захватываем его.

DeadMessage можно игнорировать; если приложению не нужно отслеживать мертвые сообщения, они могут никуда не исчезать.

4. Использование иерархии событий

Отправка событийString иInteger является ограничивающей. Давайте создадим несколько классов сообщений:

public class Message {}

public class AckMessage extends Message {}

public class RejectMessage extends Message {
    int code;

    // setters and getters
}

У нас есть простой базовый класс и два класса, которые его расширяют.

4.1. Отправка базового классаMessage

Начнем с событийMessage:

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

Познакомьтесь с MB Ambassador - высокопроизводительной шиной для мероприятий в пабе. Это ограничивает использованиеMessages, но добавляет дополнительный уровень безопасности типов.

Когда мы отправляемMessage,handleMessage() получает его. Два других обработчика этого не делают.

4.2. Отправка сообщения подкласса

ОтправимRejectMessage:

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

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

Когда мы отправляемRejectMessage, обаhandleRejectMessage() иhandleMessage() получают его.

ПосколькуRejectMessage расширяетMessage,, обработчикMessage получил его в дополнение к обработчикуRejectMessage.

Давайте проверим это поведение с помощьюAckMessage:

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

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

Как мы и ожидали, когда мы отправляемAckMessage, его получают какhandleAckMessage(), так иhandleMessage().

5. Фильтрация сообщений

Организация сообщений по типу уже является мощной функцией, но мы можем отфильтровать их еще больше.

5.1. Фильтр по классу и подклассу

Когда мы отправилиRejectMessage илиAckMessage, мы получили событие как в обработчике событий для конкретного типа, так и в базовом классе.

Мы можем решить эту проблему иерархии типов, сделавMessage абстрактным и создав класс, напримерGenericMessage. Но что, если у нас нет такой роскоши?

Мы можем использовать фильтры сообщений:

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. Библиотека предлагает два примера:

Filters.RejectSubtypes делает то, что следует из названия: он отфильтровывает любые подтипы. В этом случае мы видим, чтоRejectMessage не обрабатываетсяhandleBaseMessage().

Filters.SubtypesOnly также делает то, что предполагает его название: он отфильтровывает любые базовые типы. В этом случае мы видим, чтоMessage не обрабатываетсяhandleSubMessage().

5.2. IMessageFilterс

ИFilters.RejectSubtypes, иFilters.SubtypesOnly реализуютIMessageFilter.

RejectSubTypes сравнивает класс сообщения с определенными для него типами сообщений и будет пропускать только сообщения, равные одному из его типов, в отличие от любых подклассов.

5.3. Фильтр с условиями

К счастью, существует более простой способ фильтрации сообщений. MBassador supports a subset of Java EL expressions as conditions for filtering messages.с

Давайте отфильтруем сообщениеString по его длине:

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

Сообщение «foobar!» Состоит из семи символов и фильтруется. Отправим более короткийString:

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

    assertNotNull(testString);
}

Теперь «foobar» имеет длину всего шесть символов и проходит через него.

НашRejectMessage содержит поле с аксессором. Напишем для этого фильтр:

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

Здесь мы снова можем запросить метод объекта и либо отфильтровать сообщение, либо нет.

5.4. Захват отфильтрованных сообщений

ПодобноDeadEvents,, мы можем захватывать и обрабатывать отфильтрованные сообщения. Также есть специальный механизм для сбора отфильтрованных событий. Filtered events are treated differently from “dead” events.

Напишем тест, который это иллюстрирует:

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

С добавлением обработчикаFilteredMessage мы можем отслеживатьStrings, которые фильтруются из-за их длины. filterMessage содержит наш слишком длинныйString, аdeadMessage остаетсяnull.

6. Асинхронная отправка и обработка сообщений

До сих пор во всех наших примерах использовалась синхронная отправка сообщений; когда мы вызывалиpost.now(), сообщения доставлялись каждому обработчику в том же потоке, из которого мы вызывалиpost().

6.1. Асинхронная отправка

MBassador.post() возвращаетSyncAsyncPostCommand. Этот класс предлагает несколько методов, в том числе:

  • now() - отправлять сообщения синхронно; звонок будет заблокирован, пока все сообщения не будут доставлены

  • asynchronously() - выполняет публикацию сообщения асинхронно

Давайте использовать асинхронную отправку в примере класса. Мы будем использоватьAwaitility в этих тестах, чтобы упростить код:

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

В этом тесте мы вызываемasynchronously() и используемAtomicBoolean в качестве флага сawait(), чтобы дождаться, пока поток доставки доставит сообщение.

Если мы закомментируем вызовawait(), мы рискуем провалом теста, потому что мы проверяемtestString до завершения потока доставки.

6.2. Вызов асинхронного обработчика

Асинхронная диспетчеризация позволяет поставщику сообщений вернуться к обработке сообщений до того, как сообщения будут доставлены каждому обработчику, но он по-прежнему вызывает каждый обработчик по порядку, и каждый обработчик должен ждать завершения предыдущего.

Это может привести к проблемам, если один обработчик выполняет дорогостоящую операцию.

MB Ambassador предоставляет механизм для асинхронного вызова обработчика. Обработчики, настроенные для этого, получают сообщения в своей ветке:

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

Обработчики могут запрашивать асинхронный вызов с помощью свойстваdelivery = Invoke.Asynchronously в аннотацииHandler. Мы проверяем это в нашем тесте, сравнивая именаThread в методе отправки и обработчике.

7. Настройка MBassador

До сих пор мы использовали экземпляр MBassador с конфигурацией по умолчанию. Поведение диспетчера можно изменить с помощью аннотаций, подобных тем, которые мы видели до сих пор; мы рассмотрим еще несколько, чтобы закончить это руководство.

7.1. Обработка исключений

Обработчики не могут определять проверенные исключения. Вместо этого диспетчеру можно предоставитьIPublicationErrorHandler в качестве аргумента его конструктору:

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

КогдаhandleString() выбрасываетError,, он сохраняется вerrorCause.

7.2. Приоритет обработчика

Handlers are called in reverse order of how they are added, but this isn’t behavior we want to rely on. Даже имея возможность вызывать обработчики в своих потоках, нам все равно может потребоваться знать, в каком порядке они будут вызываться.

Мы можем установить приоритет обработчика явно:

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

Обработчики вызываются с наивысшего приоритета на низший. Обработчики с приоритетом по умолчанию, равным нулю, называются последними. Мы видим, что обработчик номеровpop() выключен в обратном порядке.

7.3. Отклонять подтипы, простой способ

Что случилось сhandleMessage() в приведенном выше тесте? Нам не нужно использоватьRejectSubTypes.class для фильтрации наших подтипов.

RejectSubTypes - это логический флаг, который обеспечивает ту же фильтрацию, что и класс, но с лучшей производительностью, чем реализацияIMessageFilter.

Нам все еще нужно использовать реализацию на основе фильтров для принятия только подтипов.

8. Заключение

MB Ambassador - простая и понятная библиотека для передачи сообщений между объектами. Сообщения могут быть организованы различными способами и могут отправляться синхронно или асинхронно.

И, как всегда, пример доступен вthis GitHub project.