JSF Event-based communication: New-school approach
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) { ... } }
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"; } } }
- 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?).
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()); } }
// this will raise events to observers having parameter @Observes @Updated Locale @Inject @Updated Event<Locale> localeEvent;
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); } }
public void onLocaleChangeEvent(@Observes(receive = IF_EXISTS) @Updated Locale locale) { ... }