How to build a simple GWT event bus using Generators

In his Google I/O session Best Practices For Architecting Your GWT App, Ray Ryan discusses the benefits of using an event bus in GWT (Google Web Toolkit) applications. Inspired by this talk, I decided to try my hand at building a simple GWT event bus modeled after our pure java event bus.

See the previous articles:
Use dynamic proxies to create a simple, powerful event bus (Part 1)
Use dynamic proxies to create a simple, powerful event bus (Part 2)

Using the event bus

If you’ve followed along with the previous event bus blogs, you already know what I mean by simple – simple to use. We want an event bus that doesn’t require a fleet of event classes or fireXXX methods.

1
2
3
4
5
6
7
8
9
10
11
12
EventBus eventBus = new EventBus();
 
// Listen for newSearch events
eventBus.addListener(SearchListener.class, new SearchListener(){
	public void newSearch(String query) {
		System.out.println("Searching for " + query);
	}
});
 
// Publish newSearch event
SearchListener publisher = eventBus.getPublisher(SearchListener.class);
publisher.newSearch("GWT event bus");

We want to call a method, like newSearch("GWT event bus"), and have it automatically invoked on all observers as if we’d called each one ourselves. We want method multicasting. As you can see in the code above, we use the same SearchListener interface to both observe newSearch events and publish them.

GWT generators

In the original event bus we used reflection, specifically, we used Java’s dynamic proxies to accomplish the publisher-listener magic. GWT doesn’t have anything like this, but it does provide hooks into the compiler for code generators. Basically, we can generate the publisher code at compile to perform the same function dynamic proxies did at runtime.

GWT Compile Steps

Code generation happens (it seems) during GWT’s precompile phase. The actual compiler hook is triggered by a combination of a GWT.create() call in the Java source and a generate-with element in the GWT module XML file. The GWT.create method takes a class literal to instantiate – GWT.create(PublisherFactoryRegistry.class) in this case. The generate-with element tells the compiler that EventPublisherGenerator will create the Java source that extends (or implements if it were an interface) PublisherFactoryRegistry.

1
2
3
<generate-with class="com.northconcepts.eventbus.generator.EventPublisherGenerator">
    <when-type-assignable class="com.northconcepts.eventbus.client.PublisherFactoryRegistry" />
</generate-with>

The compiler treats the Java source emitted by EventPublisherGenerator like any other class and optimizes it, converts it to JavaScript, obfuscates it, etc. GWT.create then returns an instance of the generated class to its caller. Google calls this whole process Deferred Binding using Generators.

For more details on the GWT compiler, watch Ray Cromwell’s Optimizing apps with the GWT Compiler session at Google I/O.

Event bus changes

GWT is a special variant of Java and not everything from standard Java is available (primarily because GWT only supports what can be emulated in JavaScript). I mentioned before that reflection and dynamic proxies are not available, here are a few more differences you’ll want to note when comparing the code here with the pure Java event bus:

  1. JavaScript is single-threaded so we don’t have to worry about threading or synchronization for the most part.
  2. No threading also means no executor service, but we’ll use deferred (or scheduled) commands to simulate it.
  3. Thread-locals are also out.
  4. While JavaScript uses a garbage collector, we don’t have the same hooks into it as we do in Java. This means no soft references and no automatic releasing of observers.

EventBus.getPublisher() changes

1
2
3
4
5
6
7
8
9
10
public <T extends EventListener> T getPublisher(Object eventSource, 
Class<T> eventListenerClass, Object topic) {
    T publisher = PUBLISHER_FACTORIES.getPublisher(eventListenerClass);
    EventPublisher<T> publisher2 = (EventPublisher<T>) publisher;
    publisher2.setEventBus(this);
    publisher2.setEventSource(eventSource);
    publisher2.setEventListenerClass(eventListenerClass);
    publisher2.setTopic(topic);
    return publisher;
}

The old, pure Java getPublisher method would implement the listener interface using a dynamic proxy. The proxy would publish events to the bus whenever called. This new GWT event bus keeps a registry of factories to create publishers that implement each type of event listener interface. Publishers also extend the EventPublisher class which allows getPublisher to initialize them with the event source, topic, etc. Like the non-GWT version, publishers queue events to the bus when called.

EventBus.publishEvent() changes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected <T extends EventListener> void publishEvent(
final Event<T> event) {
    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
        @Override public void execute() {
            try { 
                deliverEvent(event); 
            } catch (RuntimeException e) { 
                throw e; 
            } catch (Throwable e) { 
                throw new RuntimeException(e.getMessage(), e); 
            } 
        } 
    });
}

The only change to publishEvent is in how it schedules event delivery. The new method uses GWT’s scheduler service (formerly called deferred commands) since executor services are not supported.

EventBus.deliverEvent() changes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected <T extends EventListener> void deliverEvent(Event<T> event)
throws Throwable {
    try {
        currentEvent = event;
        // Deliver to typed listeners
        TypedListenerList<T> list = getTypedListenerList(
        event.getEventListenerClass());
        if (list != null) {
            for (EventListenerStub<?> stub : list.getList()) {
                stub.deliverEvent(this, event);
            }
        }
        // Deliver to untyped listeners
        for (EventListenerStub<UntypedEventListener> stub : 
        untypedListeners) {
            stub.deliverEvent(this, event);
        }
    } finally {
        currentEvent = null;
    }
}

Changes to deliverEvent are also minimal. The cleanupGarbage() call is removed since we don’t have soft references to clean up and currentEvent is demoted from a thread-local to just an instance field.

Generated code

Let’s take a look at the code we need to generate. Here we produce two kinds of classes: a event publisher for each event listener and one PublisherFactoryRegistry implementation to hold the publisher factories.

Event publishers

Publishers implement the event listener interfaces, convert method calls into events, and publishes the events to the bus.

1
2
3
4
5
public interface SearchListener extends EventListener {
    void newSearch(String query); 
    void searchFinished(String query, ArrayList<Data> results); 
    void detailsViewed(Data data); 
}

The code generator seeing the above SearchListener interface will create the following SearchListener__publisher class. Each implemented method creates an event object containing things you’d expect like event source, topic, and arguments. Since we don’t have reflection, the event object also contains an EventDispatcher to invoke the observer’s method when directed to do so by the bus.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SearchListener__publisher 
extends EventPublisher implements SearchListener {
  ...
  public void searchFinished(final String query, 
  final ArrayList results) {
    publishEvent(new Event<SearchListener>(
    getEventSource(), 
    this, 
    SearchListener.class, 
    getTopic(), 
    "searchFinished", 
    new String[]{"query", "results"}, 
    new String[]{"java.lang.String", "java.util.ArrayList"}, 
    new Object[]{query, results}, 
    new EventDispatcher<SearchListener>(){
      public void dispatch(SearchListener listener) {
        listener.searchFinished(query, results);
      }
    }));
  }
  ...
}

Publisher factory registry

1
2
3
4
5
6
7
8
9
public class PublisherFactoryRegistryImpl 
extends PublisherFactoryRegistry {
  {
    ...
    publishers.put(SearchListener.class, new EventPublisherFactory() {
        public SearchListener create() {
            return new SearchListener__publisher();}});
  }
}

The PublisherFactoryRegistry class is used by the bus to find factories that create publishers (like SearchListener__publisher). The concrete PublisherFactoryRegistryImpl we generate is only there to register a factory for each publisher we generate — by putting it into the publishers HashMap.

Preserving the generated code

You can tell the GWT compiler to keep the generated Java code around for you to review. Just add the -gen folder compiler flag, where folder is the path to generate temporary files.

The Code Generator

The contract for GWT code generators is to extend com.google.gwt.core.ext.Generator and implement its generate(TreeLogger logger, GeneratorContext context, String typeName) method. The implemented method should return the name of the generated subclass for the type passed into GWT.create. As you review the code, you’ll notice that we’ve added several helper classes (ClassDeclaration, ClassBuilder, CSV, etc.) to make the code less verbose.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class EventPublisherGenerator extends Generator {
 
  private TreeLogger logger;
  private GeneratorContext context;
  private TypeOracle typeOracle;
  private JClassType type;
  private JClassType eventListenerType;
  private JClassType eventType;
  private JClassType eventDispatcherType;
  private JClassType eventPublisherType;
  private JClassType eventPublisherFactoryType;
 
  @Override
  public String generate(TreeLogger logger, GeneratorContext context, String typeName) throws UnableToCompleteException {
    try {
      init(logger, context, typeName);
      return generate();
    } catch (Throwable e) {
      logger.log(TreeLogger.ERROR, "generate failed: " + typeName, e);
      throw new UnableToCompleteException();
    }
  }
 
  private void init(TreeLogger logger, GeneratorContext context, String typeName) throws Throwable {
    this.logger = logger;
    this.context = context;
    this.typeOracle = context.getTypeOracle();
    this.type = typeOracle.getType(typeName);
    this.eventListenerType = typeOracle.getType(EventListener.class.getName());
    this.eventType = typeOracle.getType(Event.class.getName());
    this.eventDispatcherType = typeOracle.getType(EventDispatcher.class.getName());
    this.eventPublisherType = typeOracle.getType(EventPublisher.class.getName());
    this.eventPublisherFactoryType = typeOracle.getType(EventPublisherFactory.class.getName());
  }

We’ve created an init method to act as a poor man’s constructor for the generator. It uses the compiler’s metadata registry (TypeOracle) to lookup several classes we’ll need later.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private String generate() throws Throwable {
  final ClassDeclaration clazz = new ClassDeclaration(
      type.getPackage().getName(), 
      type.getSimpleSourceName() + "Impl", 
      type.getQualifiedSourceName());
 
  final ClassBuilder classBuilder = new ClassBuilder(logger, context, clazz);
  if (classBuilder.classAlreadyExists()) {
    return classBuilder.commit();
  }
 
  classBuilder.println("{");
  classBuilder.indent();
 
  visitEventListeners(logger.branch(TreeLogger.TRACE, "Event listeners"), new ClassVisitor() {
    @Override
    public void visitClass(JClassType eventListenerType) {
      String name = createPublisher(eventListenerType);
      if (name != null) {
        classBuilder.println("publishers.put(%1$s.class, new %2$s() {public %1$s create() {return new %3$s();}});", 
            eventListenerType.getQualifiedSourceName(), 
            eventPublisherFactoryType.getQualifiedSourceName(),
            name);
      }
    }
  });
 
  classBuilder.outdentln("}");
 
  return classBuilder.commit();
}

The no-arg generate method is where the work starts. Here we create the PublisherFactoryRegistryImpl class and give it an instance initializer where the factories are registered. We also call createPublisher() to generate the code for the publisher implementation for each EventListener sub-interface. The visitor pattern is used here to let us focus on code generation while delegating the EventListener finder logic to visitEventListeners().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void visitEventListeners(TreeLogger logger, ClassVisitor visitor) {
  FIND_CANDIDATES_LOOP: 
  for (JClassType type : typeOracle.getTypes()) {
    if (type.isInterface() != null &amp;&amp; type.isAssignableTo(eventListenerType) &amp;&amp; !type.equals(eventListenerType)) {
 
      if (type.isParameterized() != null) {
        logger.branch(TreeLogger.WARN, type.getQualifiedSourceName() + " - skipped - is parameterized type").log(
            TreeLogger.WARN, type.getParameterizedQualifiedSourceName());
        continue FIND_CANDIDATES_LOOP;
      }
 
      for (JMethod method : type.getMethods()) {
        if (!method.getReturnType().equals(JPrimitiveType.VOID)) {
          logger.branch(TreeLogger.WARN, type.getQualifiedSourceName() + " - skipped - has method that returns non-void")
              .log(TreeLogger.WARN, method.getReadableDeclaration());
          continue FIND_CANDIDATES_LOOP;
        }
      }
 
      logger.log(TreeLogger.INFO, type.getQualifiedSourceName());
      visitor.visitClass(type);
    }
  }
}

The visitEventListeners method looks for types the compiler knows about that match the following criteria:

  1. are interfaces
  2. are not parameterized (like a java.util.List)
  3. extend java.util.EventListener
  4. have only void returning methods

The visitor’s visitClass method is called for matches, while partial matches emit a compiler warning along with the reason for the mismatch.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
private String createPublisher(JClassType eventListenerType) {
  ClassDeclaration clazz = new ClassDeclaration(
      eventListenerType.getPackage().getName(), 
      eventListenerType.getName() + "__publisher", 
      eventPublisherType.getQualifiedSourceName());
 
  clazz.addImplementedInterface(eventListenerType.getQualifiedSourceName());
 
  ClassBuilder classBuilder = new ClassBuilder(logger, context, clazz);
  if (classBuilder.classAlreadyExists()) {
    return classBuilder.commit();
  }
 
  for (JMethod method : eventListenerType.getMethods()) {
    CSV paramerNames = new CSV();
    CSV paramerTypeNames = new CSV();
    CSV arguments = new CSV();
    final JParameter[] parameters = method.getParameters();
    final boolean hasParameters = parameters.length &gt; 0;
 
    for (int i = 0; i &lt; parameters.length; i++) {
      JParameter parameter = parameters[i];
      paramerNames.add("\"" + parameter.getName() + "\"");
      paramerTypeNames.add("\"" + MethodDeclaration.getParameterTypeName(method, i) + "\"");
      arguments.add(parameter.getName());
    }
 
    classBuilder.startMethod(new MethodDeclaration(method));
 
    classBuilder.println("publishEvent(new %1$s&lt;%2$s&gt;(", 
        eventType.getQualifiedSourceName(), 
        eventListenerType.getQualifiedSourceName());
 
    classBuilder.println("getEventSource(), ");
    classBuilder.println("this, ");
    classBuilder.println("%1$s.class, ", eventListenerType.getQualifiedSourceName());
    classBuilder.println("getTopic(), ");
    classBuilder.println("\"%1$s\", ", method.getName());
    if (hasParameters) {
      classBuilder.println("new String[]{%1$s}, ", paramerNames);
      classBuilder.println("new String[]{%1$s}, ", paramerTypeNames);
      classBuilder.println("new Object[]{%1$s}, ", arguments);
    } else {
      classBuilder.println("null, null, null, ");
    }
    classBuilder.println("new %1$s&lt;%2$s&gt;(){", 
        eventDispatcherType.getQualifiedSourceName(), 
        eventListenerType.getQualifiedSourceName());
 
    classBuilder.indentln("public void dispatch(%1$s listener) {", eventListenerType.getQualifiedSourceName());
    classBuilder.indentln("listener.%1$s(%2$s);", method.getName(), (hasParameters?arguments:""));
    classBuilder.outdentln("}");
    classBuilder.outdentln("}));");
 
    classBuilder.endMethod();
  }
 
  return classBuilder.commit();
}

This lengthy method creates the publisher implementation. It implements each method in the event listener interface to publish its description and payload as an event to the bus.

Example

The downlad for this blog includes a simple search example to demonstrate using the event bus. You can run the EventBusExample module (/src/com/northconcepts/eventbusexample/EventBusExample.gwt.xml) or try out the precompiled app by browsing to WebContent/index.html.

Search App Mockup

The app has four display areas that plug into the event bus:

  1. SearchUI- accepts user input and starts the search
  2. ResultsUI – holds search results and handles clickfor full details
  3. DetailsUI- shows full details when result is clicked
  4. StatusUI – shows search progress and results summary.

The actual search function is performed by a non-visual component called SearchEngine. Like the display areas, it also plugs into the bus to both observer and publish events.

Event Bus - New Search event

When the user fills in the text field and presses the search button, the SearchUI publishes a newSearch event to the bus. The SearchEngine receives the notification and runs the search. The StatusUI also receives the notification and displays a search-in-progress message to the user.

Event Bus - Search finished event

When the search is complete, SearchEngine publishes a searchFinished message to the bus. The ResultsUI receives the notification and displays the data, if any, in a table to the user. The StatusUI also receives the notification and displays to the user either a message with the number of results found or a message that none were found.

Event Bus - Details viewed event

If results were found, the user can click one of the items in the ResultsUI which will publish a detailsViewed event. The DetailsUI will receive this event and display all the data for the selected record.

Conclusion

While GWT doesn’t support Java reflection, it does provide a powerful code generation facility. Code generation has the benefit of enabling dynamic, reflexive programming without the runtime cost. Once you get used to GWT’s approach, you’ll start seeing many opportunities to reduce your code size by leveraging GWT’s generators.

Download

The event bus download contains the entire source code (including Eclipse project). The source code is licensed under the terms of the Apache License, Version 2.0.

Java Exception Tracking

If you enjoyed this, I’ve got an exception tracking tool for Java coming out. Sign-up at StackHunter.com to be notified when it does.

About Dele Taylor

Dele Taylor is the founder of North Concepts: http://NorthConcepts.com. You can follow him on Twitter @DeleTaylor and Google +DeleTaylor.
Code Generation, Event Bus, GWT, Java | permalink

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>