Introdução ao MBassador
1. Visão geral
Simplificando,MBassador éa high-performance event bus utilizing the publish-subscribe semantics.
As mensagens são transmitidas para um ou mais pares sem o conhecimento prévio de quantos assinantes existem ou de como eles usam a mensagem.
2. Dependência do Maven
Antes de podermos usar a biblioteca, precisamos adicionar a dependênciambassador:
net.engio
mbassador
1.3.1
3. Tratamento Básico de Eventos
3.1. Exemplo Simples
Começaremos com um exemplo simples de publicação de uma mensagem:
private MBassador
No topo desta classe de teste, vemos a criação de umMBassador com seu construtor padrão. A seguir, no método@Before, chamamossubscribe()e passamos uma referência para a própria classe.
Emsubscribe(),, o despachante inspeciona o assinante em busca de anotações de@Handler.
E, no primeiro teste, chamamosdispatcher.post(…).now() para despachar a mensagem - o que resulta na chamada dehandleString().
Este teste inicial demonstra vários conceitos importantes. Any Object can be a subscriber, as long as it has one or more methods annotated with @Handler. Um assinante pode ter qualquer número de manipuladores.
Estamos usando objetos de teste que se inscrevem por uma questão de simplicidade, mas na maioria dos cenários de produção, os despachantes de mensagens estarão em classes diferentes dos consumidores.
Os métodos do manipulador têm apenas um parâmetro de entrada - a mensagem, e não podem lançar nenhuma exceção verificada.
Semelhante ao métodosubscribe(), o método post aceita qualquerObject. EsteObject é entregue aos assinantes.
Quando uma mensagem é postada, ela é entregue a todos os ouvintes que se inscreveram no tipo de mensagem.
Vamos adicionar outro gerenciador de mensagens e enviar um tipo de mensagem diferente:
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;
}
Como esperado, quando despachamos_ an _Integer,handleInteger() é chamado ehandleString() não. Um único expedidor pode ser usado para enviar mais de um tipo de mensagem.
3.2. Mensagens Mortas
Então, para onde vai uma mensagem quando não há manipulador? Vamos adicionar um novo manipulador de eventos e enviar um terceiro tipo de mensagem:
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();
}
Neste teste, despachamos aLong em vez deInteger. NemhandleInteger() nemhandleString() são chamados, mashandleDeadEvent() é.
When there are no handlers for a message, it gets wrapped in a DeadMessage object. Como adicionamos um manipulador paraDeadmessage, nós o capturamos.
DeadMessage pode ser ignorado com segurança; se um aplicativo não precisa rastrear mensagens mortas, eles não podem ir a lugar nenhum.
4. Usando uma hierarquia de eventos
O envio de eventosStringeInteger é limitante. Vamos criar algumas classes de mensagem:
public class Message {}
public class AckMessage extends Message {}
public class RejectMessage extends Message {
int code;
// setters and getters
}
Temos uma classe base simples e duas classes que a estendem.
4.1. Enviando uma classe baseMessage
Começaremos com eventosMessage:
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;
}
Descubra o MBassador - um barramento de eventos pub-sub de alto desempenho. Isso nos limita a usarMessages, mas adiciona uma camada adicional de segurança de tipo.
Quando enviamos umMessage,handleMessage() o recebe. Os outros dois manipuladores não.
4.2. Enviando uma Mensagem de Subclasse
Vamos enviar umRejectMessage:
@Test
public void whenRejectDispatched_thenMessageAndRejectHandled() {
dispatcher.post(new RejectMessage()).now();
assertNotNull(message);
assertNotNull(rejectMessage);
assertNull(ackMessage);
}
Quando enviamos umRejectMessage, tantohandleRejectMessage()ehandleMessage() o recebe.
ComoRejectMessage estendeMessage,, o manipuladorMessage o recebeu, além do manipuladorRejectMessage.
Vamos verificar esse comportamento com umAckMessage:
@Test
public void whenAckDispatched_thenMessageAndAckHandled() {
dispatcher.post(new AckMessage()).now();
assertNotNull(message);
assertNotNull(ackMessage);
assertNull(rejectMessage);
}
Como esperávamos, quando enviamosAckMessage,handleAckMessage() ehandleMessage() o recebem.
5. Filtrando mensagens
Organizar mensagens por tipo já é um recurso poderoso, mas podemos filtrá-las ainda mais.
5.1. Filtrar por classe e subclasse
Quando postamos umRejectMessage ouAckMessage, recebemos o evento no manipulador de eventos para o tipo específico e na classe base.
Podemos resolver esse problema de hierarquia de tipo tornandoMessage abstrato e criando uma classe comoGenericMessage. Mas e se não tivermos esse luxo?
Podemos usar filtros de mensagens:
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. A biblioteca oferece dois exemplos:
OFilters.RejectSubtypes faz o que seu nome sugere: ele irá filtrar quaisquer subtipos. Neste caso, vemos queRejectMessage não é tratado porhandleBaseMessage().
OFilters.SubtypesOnly também faz o que seu nome sugere: ele irá filtrar quaisquer tipos de base. Neste caso, vemos queMessage não é tratado porhandleSubMessage().
5.2. IMessageFilter
OFilters.RejectSubtypes e oFilters.SubtypesOnly implementamIMessageFilter.
RejectSubTypes compara a classe da mensagem com seus tipos de mensagem definidos e só permitirá a passagem de mensagens iguais a um de seus tipos, ao contrário de quaisquer subclasses.
5.3. Filtro com Condições
Felizmente, existe uma maneira mais fácil de filtrar mensagens. MBassador supports a subset of Java EL expressions as conditions for filtering messages.
Vamos filtrar uma mensagemString com base em seu comprimento:
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;
}
O "foobar!" A mensagem tem sete caracteres e é filtrada. Vamos enviar umString mais curto:
@Test
public void whenShortStringDispatched_thenStringHandled() {
dispatcher.post("foobar").now();
assertNotNull(testString);
}
Agora, o "foobar" tem apenas seis caracteres e é transmitido.
NossoRejectMessage contém um campo com um acessador. Vamos escrever um filtro para isso:
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;
}
Aqui, novamente, podemos consultar um método em um objeto e filtrar a mensagem ou não.
5.4. Capturar mensagens filtradas
Semelhante aDeadEvents,, podemos querer capturar e processar mensagens filtradas. Também existe um mecanismo dedicado para capturar eventos filtrados. Filtered events are treated differently from “dead” events.
Vamos escrever um teste que ilustra isso:
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;
}
Com a adição de um manipuladorFilteredMessage, podemos rastrearStrings que são filtrados por causa de seu comprimento. OfilterMessage contém nossoString muito longo, enquantodeadMessage permanecenull.
6. Envio e tratamento de mensagens assíncronas
Até agora, todos os nossos exemplos usaram envio de mensagem síncrona; quando chamamospost.now(), as mensagens foram entregues a cada manipulador no mesmo encadeamento do qual chamamospost().
6.1. Despacho Assíncrono
OMBassador.post() retorna umSyncAsyncPostCommand. Esta classe oferece vários métodos, incluindo:
-
now() - despacha mensagens de forma síncrona; a chamada será bloqueada até que todas as mensagens sejam entregues
-
asynchronously() - executa a publicação da mensagem de forma assíncrona
Vamos usar despacho assíncrono em uma classe de amostra. UsaremosAwaitility nesses testes para simplificar o código:
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);
}
Chamamosasynchronously() neste teste e usamos umAtomicBoolean como um sinalizador comawait() para aguardar o thread de entrega entregar a mensagem.
Se comentarmos a chamada paraawait(), corremos o risco de o teste falhar, porque verificamostestString antes que o thread de entrega seja concluído.
6.2. Invocação do manipulador assíncrono
O envio assíncrono permite que o provedor de mensagens retorne ao processamento de mensagens antes que as mensagens sejam entregues a cada manipulador, mas ainda chama cada manipulador em ordem e cada manipulador precisa aguardar o término do anterior.
Isso pode causar problemas se um manipulador executar uma operação cara.
O MBassador fornece um mecanismo para chamada de manipulador assíncrono. Os manipuladores configurados para isso recebem mensagens em seu encadeamento:
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);
}
Os manipuladores podem solicitar invocação assíncrona com a propriedadedelivery = Invoke.Asynchronously na anotaçãoHandler. Verificamos isso em nosso teste comparando os nomesThread no método de despacho e no manipulador.
7. Personalizando MBassador
Até agora, usamos uma instância do MBassador com sua configuração padrão. O comportamento do despachante pode ser modificado com anotações, semelhantes às que vimos até agora; vamos cobrir mais alguns para terminar este tutorial.
7.1. Manipulação de exceção
Os manipuladores não podem definir exceções verificadas. Em vez disso, o despachante pode receber umIPublicationErrorHandler como um argumento para seu construtor:
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();
}
}
QuandohandleString() lança umError,, ele é salvo emerrorCause.
7.2. Prioridade do manipulador
Handlers are called in reverse order of how they are added, but this isn’t behavior we want to rely on. Mesmo com a capacidade de chamar manipuladores em seus threads, ainda podemos precisar saber em que ordem eles serão chamados.
Podemos definir a prioridade do manipulador explicitamente:
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);
}
Os manipuladores são chamados da prioridade mais alta para a mais baixa. Manipuladores com a prioridade padrão, que é zero, são chamados por último. Vemos que o tratador numerapop() na ordem inversa.
7.3. Rejeite subtipos, da maneira mais fácil
O que aconteceu comhandleMessage() no teste acima? Não precisamos usarRejectSubTypes.class para filtrar nossos subtipos.
RejectSubTypes é um sinalizador booleano que fornece a mesma filtragem da classe, mas com melhor desempenho do que a implementação deIMessageFilter.
No entanto, ainda precisamos usar a implementação baseada em filtro para aceitar apenas subtipos.
8. Conclusão
O MBassador é uma biblioteca simples e direta para transmitir mensagens entre objetos. As mensagens podem ser organizadas de várias maneiras e podem ser enviadas de maneira síncrona ou assíncrona.
E, como sempre, o exemplo está disponível emthis GitHub project.