In
the last post, we learnt event-based communication on basis of
Observer /
Event Listener and
Mediator patterns. Due to their shortcomings I would like to show more efficient ways for event-based communication. We will start with
Google Guava EventBus and end up with
CDI (Contexts and Dependency Injection for the Java EE platform).
Guava EventBus
Google Guava library has an useful package
eventbus. The class
EventBus allows publish-subscribe-style communication between components without requiring the components to explicitly register with one another. Because we develop web applications, we should encapsulate an instance of this class in a scoped bean. Let's write
EventBusProvider bean.
public class EventBusProvider implements Serializable {
private EventBus eventBus = new EventBus("scopedEventBus");
public static EventBus getEventBus() {
// access EventBusProvider bean
ELContext elContext = FacesContext.getCurrentInstance().getELContext();
EventBusProvider eventBusProvider =
(EventBusProvider) elContext.getELResolver().getValue(elContext, null, "eventBusProvider");
return eventBusProvider.eventBus;
}
}
I would like to demonstrate all main features of Guava
EventBus in only one example. Let's write the following event hierarchy:
public class SettingsChangeEvent {
}
public class LocaleChangeEvent extends SettingsChangeEvent {
public LocaleChangeEvent(Object newLocale) {
...
}
}
public class TimeZoneChangeEvent extends SettingsChangeEvent {
public TimeZoneChangeEvent(Object newTimeZone) {
...
}
}
The next steps are straightforward. To receive events, an object (bean) should expose a public method, annotated with
@Subscribe annotation, which accepts a single argument with desired event type. The object needs to pass itself to the
register() method of
EventBus instance. Let's create two beans:
public MyBean1 implements Serializable {
@PostConstruct
public void initialize() throws Exception {
EventBusProvider.getEventBus().register(this);
}
@Subscribe
public void handleLocaleChange(LocaleChangeEvent event) {
// do something
}
@Subscribe
public void handleTimeZoneChange(TimeZoneChangeEvent event) {
// do something
}
}
public MyBean2 implements Serializable {
@PostConstruct
public void initialize() throws Exception {
EventBusProvider.getEventBus().register(this);
}
@Subscribe
public void handleSettingsChange(SettingsChangeEvent event) {
// do something
}
}
To post an event, simple provide the event object to the
post() method of
EventBus instance. The
EventBus instance will determine the type of event and route it to all registered listeners.
public class UserSettingsForm implements Serializable {
private boolean changed;
public void localeChangeListener(ValueChangeEvent e) {
changed = true;
// notify subscribers
EventBusProvider.getEventBus().post(new LocaleChangeEvent(e.getNewValue()));
}
public void timeZoneChangeListener(ValueChangeEvent e) {
changed = true;
// notify subscribers
EventBusProvider.getEventBus().post(new TimeZoneChangeEvent(e.getNewValue()));
}
public String saveUserSettings() {
...
if (changed) {
// notify subscribers
EventBusProvider.getEventBus().post(new SettingsChangeEvent());
return "home";
}
}
}
Guava
EventBus allows to create any listener that is reacting for many different events - just annotate many methods with
@Subscribe and that's all. Listeners can leverage existing events hierarchy. So if Listener A is waiting for events A, and event A has a subclass named B, this listener will receive both type of events: A and B. In our example, we posted three events:
SettingsChangeEvent,
LocaleChangeEvent and
TimeZoneChangeEvent. The
handleLocaleChange() method in the
MyBean1 will only receive
LocaleChangeEvent. The method
handleTimeZoneChange() will only receive
TimeZoneChangeEvent. But look at the method
handleSettingsChange() in the
MyBean2. It will receive all three events!
As you may see, a manually registration is still needed (
EventBusProvider.getEventBus().register(this)) and the problem with scoped beans, I mentioned in
the previous post, is still existing. We should be aware of scoping of
EventBusProvider and scoping of publish / subscriber beans. But as you may also see, we have some improvements in comparison to the
Mediator pattern: no special interfaces are needed, the subscriber's method names are not fix defined, multi-listeners are possible too, no effort to manage registered instances, etc. Last but not least - asynchronous
AsyncEventBus and subscription to
DeadEvent (for listening for any events which were dispatched without listeners - handy for debugging). Follow
this guide please to convert an existing EventListener-based system to EventBus-based one.
CDI (Contexts and Dependency Injection)
Every JEE 6 compliant application server supports CDI (the JSR-299 specification). It defines a set of complementary services that help improve the structure of application code. The best-known implementations of CDI are
OpenWebBeans and
JBoss Weld. Events in CDI allow beans to interact with no dependency at all. Event producers raise events that are delivered to event observers by the container. This basic schema might sound like the familiar
Observer /
Observable pattern, but there are a couple of benefits.
- Event producers and event observers are decoupled from each other.
- Observers can specify a combination of "selectors" to narrow the set of event notifications they will receive.
- Observers can be notified immediately or with delaying until the end of the current transaction.
- No headache with scoping by conditional observer methods (remember problem of scoped beans and Mediator / EventBus?).
Conditional observer methods allow to obtain the bean instance that already exists, only if the scope of the bean that declares the observer method is currently active, without creating a new bean instance.
If the observer method is not conditional, the corresponding bean will be always created. You are flexible!
CDI event mechanism is the best approach for the event-based communication in my opinion. The subject is complex. Let's only show the basic features. An observer method is a method of a bean with a parameter annotated
@Observes.
public MyBean implements Serializable {
public void onLocaleChangeEvent(@Observes Locale locale) {
...
}
}
The event parameter may also specify qualifiers if the observer method is only interested in qualified events - these are events which have those qualifiers.
public void onLocaleChangeEvent(@Observes @Updated Locale locale) {
...
}
An event qualifier is just a normal qualifier, defined using
@Qualifier. Here is an example:
@Qualifier
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
public @interface Updated {}
Event producers fire events using an instance of the parametrized
Event interface. An instance of this interface is obtained by injection. A producer raises events by calling the
fire() method of the
Event interface, passing the event object.
public class UserSettingsForm implements Serializable {
@Inject @Any Event<Locale> localeEvent;
public void localeChangeListener(ValueChangeEvent e) {
// notify all observers
localeEvent.fire((Locale)e.getNewValue());
}
}
The container calls all observer methods, passing the event object as the value of the event parameter. If any observer method throws an exception, the container stops calling observer methods, and the exception is re-thrown by the
fire() method.
@Any annotation above acts as an alias for any and all qualifiers. You see, no manually registration of observers is necessary. Easy? Specifying other qualifiers at the injection point is simple as well:
// this will raise events to observers having parameter @Observes @Updated Locale
@Inject @Updated Event<Locale> localeEvent;
You can also have multiple event qualifiers. The event is delivered to every observer method that has an event parameter to which the event object is assignable, and does not have any event qualifier except the event qualifiers matching those specified at the
Event injection point. The observer method may have additional parameters, which are injection points. Example:
public void onLocaleChangeEvent(@Observes @Updated Locale locale, User user) {
...
}
What is about a specifying the qualifier dynamically? CDI allows to obtain a proper qualifier instance by means of
AnnotationLiteral. This way, we can pass the qualifier to the
select() method of
Event. Example:
public class DocumentController implements Serializable {
Document document;
@Inject @Updated @Deleted Event<Document> documentEvent;
public void updateDocument() {
...
// notify observers with @Updated annotation
documentEvent.select(new AnnotationLiteral<Updated>(){}).fire(document);
}
public void deleteDocument() {
...
// notify observers with @Deleted annotation
documentEvent.select(new AnnotationLiteral<Deleted>(){}).fire(document);
}
}
Let's talk about "conditional observer methods". By default, if there is no instance of an observer in the current context, the container will instantiate the observer in order to deliver an event to it. This behaviour isn't always desirable. We may want to deliver events only to instances of the observer that already exist in the current context. A conditional observer is specified by adding
receive = IF_EXISTS to the
@Observes annotation.
public void onLocaleChangeEvent(@Observes(receive = IF_EXISTS) @Updated Locale locale) {
...
}
Read more about Scopes and Contexts
here. In this short post we can not talk more about further features like "event qualifiers with members" and "transactional observers". I would like to encourage everybody to start learn CDI. Have much fun!
Edit: Found today a great CDI specific possibility
how to obtain a list of listeners for an event. Solution: Inject
BeanManager (allows to interact directly with the container) and call
<T> java.util.Set<ObserverMethod<? super T>> resolveObserverMethods(T event, java.lang.annotation.Annotation... qualifiers)