-
Notifications
You must be signed in to change notification settings - Fork 145
Guice Integration
I really liked the MBassador Event Bus implementation and decided to integrate it with my Krail project. That is what this example is based on, and shows how to provide multiple bus instances aligned to Guice scopes. You may not need multiple buses of course, but it is straightforward to remove those you don't want. A couple of the scopes are also custom scopes specific to Krail, but the principles are the same for any scope. All of the code shown here is in a single Guice Module, sub-classed from AbstractModule.
MBassador provides both synchronous and asynchronous implementations, and it would be useful to be able to select either of them during configuration. Constructors for both of the implementations (SyncMessageBus and MBassador) can take a configuration object in their constructor. To support these options we will use the PubSubSupport interface as the common factor. Construction with a configuration object means we will need to use a @Provides method or a Provider.
For reasons which will become clear a bit later, the act of constructing a bus is placed in its own method:
private PubSubSupport<BusMessage> createBus(IBusConfiguration config, IPublicationErrorHandler publicationErrorHandler, ConfigurationErrorHandler
configurationErrorHandler, String name, boolean useAsync) {
config.addPublicationErrorHandler(publicationErrorHandler);
config.addConfigurationErrorHandler(configurationErrorHandler);
PubSubSupport<BusMessage> eventBus;
eventBus = (useAsync) ? new MBassador<>(config) : new SyncMessageBus<>(config);
log.debug("instantiated a {} Bus with id {}", name, eventBus.getRuntime()
.get(Properties.Common.Id));
return eventBus;
}
One thing to note here - - there are 2 error handlers, one for configuration and one for publication, passed as parameters to this method. If you configure these in the wrong order, you will probably get a warning message in the console, even if everything works.
I will just use one example here, of the three buses the full version uses - the principles are the same for each bus.
@Provides
@GlobalBus
@Singleton
protected PubSubSupport<BusMessage> providesGlobalBus(@GlobalBus IBusConfiguration config, @GlobalBus IPublicationErrorHandler publicationErrorHandler,
@GlobalBus ConfigurationErrorHandler configurationErrorHandler) {
return createBus(config, publicationErrorHandler, configurationErrorHandler, "Global", true);
}
In this case, for a Singleton bus, createBus is called with the async option 'true'. Note also the use of the @GlobalBus binding annotation, which enables us to distinguish between the different buses. This is applied to the @Provides method so that application code can determine which bus to inject, and to the parameters of the @Provides method to select the correct error handlers and configuration.
The bus configuration objects may need to be different for each bus, we use a separate @Provides method for each. The @GlobalBus binding annotation is again used to distinguish this configuration from others.
@Provides
@GlobalBus
protected IBusConfiguration globalBusConfig() {
return new BusConfiguration().addFeature(Feature.SyncPubSub.Default())
.addFeature(Feature.AsynchronousHandlerInvocation.Default())
.addFeature(Feature.AsynchronousMessageDispatch.Default());
}
The bindings for publication and configuration handlers are separated out into their own methods - you certainly don't have to do this, the bindings could just as easily be in the Module's configure() method. This approach is really just a matter of style.
protected void bindConfigurationErrorHandlers() {
bind(ConfigurationErrorHandler.class).annotatedWith(UIBus.class)
.to(DefaultEventBusConfigurationErrorHandler.class);
bind(ConfigurationErrorHandler.class).annotatedWith(SessionBus.class)
.to(DefaultEventBusConfigurationErrorHandler.class);
bind(ConfigurationErrorHandler.class).annotatedWith(GlobalBus.class)
.to(DefaultEventBusConfigurationErrorHandler.class);
}
protected void bindPublicationErrorHandlers() {
bind((IPublicationErrorHandler.class)).annotatedWith(UIBus.class)
.to(DefaultEventBusErrorHandler.class);
bind((IPublicationErrorHandler.class)).annotatedWith(SessionBus.class)
.to(DefaultEventBusErrorHandler.class);
bind((IPublicationErrorHandler.class)).annotatedWith(GlobalBus.class)
.to(DefaultEventBusErrorHandler.class);
}
All we need now is to finalise the Guice configuration, by calling the two binding methods above:
@Override
protected void configure() {
bindConfigurationErrorHandlers();
bindPublicationErrorHandlers();
}
With the techniques above you will be able to inject whichever bus, or buses, you wish to use into your application code. You can take integration a step further, however, and automatically subscribe using your own selection rules, with the help of Guice AOP. This means that classes which only subscribe to a bus only need to be annotated with @Listener - there is no need to explicitly inject the bus.
First we need to provide a Matcher to select only those classes which are annotated with @Listener (BTW -@Listener is @Inherited, so it may be annotated on a super class)
private class ListenerAnnotationMatcher extends AbstractMatcher<TypeLiteral<?>> {
@Override
public boolean matches(TypeLiteral<?> t) {
Class<?> rawType = t.getRawType();
return rawType.isAnnotationPresent(Listener.class);
}
}
Interception is a little tricky because we have to take account of the fact that the Guice Injector is still in the process of being constructed, so there are limits to what can be injected. Note the call in 'hear' to 'EventBusAutoSubscriber'. That's the class which contains the logic to decide which class should be subscribed to which bus and will be application specific. A TypeListener is needed for the interception, and when it is invoked, it will need the buses to subscribe to. They do not exist yet, so again we need to use Providers.
private class BusTypeListener implements TypeListener {
private Provider<PubSubSupport<BusMessage>> globalBusProvider;
private Provider<PubSubSupport<BusMessage>> sessionBusProvider;
private Provider<PubSubSupport<BusMessage>> uiBusProvider;
public BusTypeListener(Provider<PubSubSupport<BusMessage>> uiBusProvider, Provider<PubSubSupport<BusMessage>> sessionBusProvider,
Provider<PubSubSupport<BusMessage>> globalBusProvider) {
this.uiBusProvider = uiBusProvider;
this.sessionBusProvider = sessionBusProvider;
this.globalBusProvider = globalBusProvider;
}
/**
* The logic for auto subscribing can be changed by providing an alternative implementation of EventBusAutoSubscriber, but it has to be created using
* 'new' here, because the Injector is not yet complete
*
* @param type
* @param encounter
* @param <I>
*/
public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
encounter.register(new EventBusAutoSubscriber(uiBusProvider, sessionBusProvider, globalBusProvider));
}
}
We need to supply the Providers to the TypeListener above, so we add to the configure() method:
TypeLiteral<PubSubSupport<BusMessage>> eventBusLiteral = new TypeLiteral<PubSubSupport<BusMessage>>() {
};
Key<PubSubSupport<BusMessage>> uiBusKey = Key.get(eventBusLiteral, UIBus.class);
final Provider<PubSubSupport<BusMessage>> uiBusProvider = this.getProvider(uiBusKey);
Key<PubSubSupport<BusMessage>> sessionBusKey = Key.get(eventBusLiteral, SessionBus.class);
final Provider<PubSubSupport<BusMessage>> sessionBusProvider = this.getProvider(sessionBusKey);
Key<PubSubSupport<BusMessage>> globalBusKey = Key.get(eventBusLiteral, GlobalBus.class);
final Provider<PubSubSupport<BusMessage>> globalBusProvider = this.getProvider(globalBusKey);
If you are not familiar with the Guice TypeLiteral, this may look a little odd - essentially it is a way of safely identifying a Generic.
Finally we need to create the TypeListener using the matcher and the bus providers, again in the configure() method:
bindListener(new ListenerAnnotationMatcher(), new BusTypeListener(uiBusProvider, sessionBusProvider, globalBusProvider));
There are probably many ways of using multiple event buses, this is just one example - it fits nicely with Guice scopes - but could easily be adapted to other situations. I hope you will find it useful.