Saturday, July 21, 2012

Event-based communication in JSF. New-school approach.

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)

12 comments:

  1. Excellent post Oleg!
    When 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!

    ReplyDelete
  2. 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 :-).

    ReplyDelete
  3. Nice article!

    I 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

    ReplyDelete
  4. I think this is by reason of @Dependent scope. CDI events can't be used with @Dependent according to the specification.

    I 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.).

    ReplyDelete
  5. Thanks for the response. Then I will try the Guava event bus.

    So given your explanation, if I want to communicate view scoped jsf beans, the "EventBusProvider" should be view scoped too, right?

    ReplyDelete
  6. yes, it should be view scoped to avoid troubles

    ReplyDelete
  7. @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.

    ReplyDelete
  8. Myfaces 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.

    ReplyDelete
  9. As usual, great article Oleg! :)

    The 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.

    ReplyDelete
  10. 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?

    And yes, I agree totally - The omission of @ViewScoped in CDI is indeed severe!!

    ReplyDelete
    Replies
    1. You can convert @ViewScoped JSF scope to CDI by using CODI. There are a lot of other goodies in CODI as well.
      At this moment, CODI is merged with Seam 3 into DeltaSpike. So we will have one great CDI extension library with everything we need.

      Delete
    2. 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.

      Delete