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?).
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)
Excellent post Oleg!
ReplyDeleteWhen I was reading first paragraph of your old-school pattern post, I was thinking about CDI Event/Observer pattern that can fill best for this gap. and in this post you covered it as the best solution ;)
in fact, I think CDI is the best DI container to develop applications. it's easy to learn and makes developers life much better.
Guava event bus is good too and I was reading about that for the first time!
Continue writing such great stuff!
Heidarzadeh, thanks for your reply and reading my posts. Yes, Guava EventBus is not bad, especially for Java desktop clients. CDI is the best in my opinion. I've tested it, it works smoothly and leaves nothing to be desired. Now, I have to port my web apps to CDI completely and remove old manual event listeners registrations :-).
ReplyDeleteNice article!
ReplyDeleteI like a lot the CDI approach, however I have a problem: I want to use it to communicate two View-scoped JSF managed beans (both the observer and the producer), but when I deploy the app I got an exception:
WELD-000404 Conditional observer method [[method] public cups.view.controllers.NewColoniaDialog.onDialogRequest(Colonia)] cannot be declared by a @Dependent scoped bean
Have you tried this scenario? Or CDI events can only be used with CDI scoped beans?
Thanks
I think this is by reason of @Dependent scope. CDI events can't be used with @Dependent according to the specification.
ReplyDeleteI didn't test CDI events with non CDI scoped beans. I think it will not work because CDI container (and not JSF) should manage beans with annotated methods (@Observes, etc.).
Thanks for the response. Then I will try the Guava event bus.
ReplyDeleteSo given your explanation, if I want to communicate view scoped jsf beans, the "EventBusProvider" should be view scoped too, right?
yes, it should be view scoped to avoid troubles
ReplyDelete@Arturo SerranoJuly, Consider that CDI does not work with @ViewScoped, if you want to have an equivalent scope use Myfaces CODI @ViewAccessScoped or Seam3-faces module.
ReplyDeleteMyfaces CODI @ViewAccessScoped != JSF @ViewScoped. @ViewAccessScoped is broader, you can use it across pages when a ViewAccessScoped bean is accessed across several pages. But if not, it's the same.
ReplyDeleteAs usual, great article Oleg! :)
ReplyDeleteThe omission of @ViewScoped in CDI is indeed severe. Or more technically; JSF 2 not providing a CDI compatible @ViewScoped is severe. CDI should not have knowledge about JSF, but JSF definitely should know about CDI.
For Java EE 6, the popular explanation is that CDI was really late to the game and the other specs (like JSF and JAX-RS) had already finished and could thus not include CDI dependencies.
JSF 2.2 however already has a CDI dependency for the new @FlowScoped, so it should absolutely be possible now to add this. I proposed to add this feature here: http://java.net/jira/browse/JAVASERVERFACES_SPEC_PUBLIC-1087 If you or anyone else thinks this is important too, please vote for it or participate in the discussion.
Hi Arjan. Do you read my thoughts :-) You will not believe, but I voted for this issue today. After your post I've also commented one thing what makes me headache currently. Maybe we should really think about an external small project with a clean CDI impl. for the view scope? What do you think?
ReplyDeleteAnd yes, I agree totally - The omission of @ViewScoped in CDI is indeed severe!!
You can convert @ViewScoped JSF scope to CDI by using CODI. There are a lot of other goodies in CODI as well.
DeleteAt this moment, CODI is merged with Seam 3 into DeltaSpike. So we will have one great CDI extension library with everything we need.
I use CODI and know about DeltaSpike. But as I said here http://java.net/jira/browse/JAVASERVERFACES_SPEC_PUBLIC-1087, CODI's @ViewScoped has a small issue with Weld and @Inject in a super class. Actually Weld issue, I think not a CODI one because it works with OpenWebBeans.
DeleteEventBus doesn't implement Serializable, therefore you'll get a NotSerializableException if you try to serialize the EventBusProvider class
ReplyDeleteI'm trying to integrate CDI event on spring /JSF application with cdisource api to make the link between CDI and spring bean... The integration is not trivial... there are many differents errors that i cannot detail here. Now i am going to integrate guada in place of CDI event.
ReplyDeleteI am trying to integrate CDI event on Spring / JSF application with cdisource api to link CDI beans and Spring bean. The integration is not trivial. There are many errors that i cannot detail here. Now i am going to integrate Guava in place of CDI event.
ReplyDelete