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.
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:
- JavaScript is single-threaded so we don’t have to worry about threading or synchronization for the most part.
- No threading also means no executor service, but we’ll use deferred (or scheduled) commands to simulate it.
- Thread-locals are also out.
- 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 && type.isAssignableTo(eventListenerType) && !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:
- are interfaces
- are not parameterized (like a
java.util.List
) - extend java.util.EventListener
- 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 > 0; for (int i = 0; i < 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<%2$s>(", 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<%2$s>(){", 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
.
The app has four display areas that plug into the event bus:
- SearchUI– accepts user input and starts the search
- ResultsUI – holds search results and handles clickfor full details
- DetailsUI– shows full details when result is clicked
- 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.
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.
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.
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.
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.