Introduction
From time to time we like to share experiences from fellow developers with you. It's a pleasure to present you today with this guest blog post by Geoff Speicher, Chief System Architect of Hydro4GE.
Overview
Hydro4GE (pronounced
hy-dro-forge) is a Platform-as-a-Service (PaaS) for building online
database applications. Building a powerful tool for developers
itself requires a powerful toolkit to meet developers' expectations
of a development environment: a highly-interactive, rich user
interface (UI) with emerging features such as database schema
visualization via interactive, scalable graphics. This article
describes our experience in using GWT to rewrite our old HTML+AJAX
UI to deliver all of these features with a polished look and solid
performance.
Building the UI
When we set out to rewrite the UI using GWT, a quick inventory
of available widgets was in order. Our initial reaction was similar
to what some others have expressed: that the native GWT widget
library did not have quite the same breadth or flair as some
third-party libraries such as ExtJS or SmartGWT. However,
experimentation with these libraries proved that they are also
fairly heavy in weight, and noticeably impacted the application's
performance both in its initial download and its interactive
responsiveness.
The reason is simple: these generic JavaScript libraries, though
highly optimized, are still just that —
generic. It
is unrealistic for these libraries to achieve the same level of
efficiency as code that is produced by the GWT compiler. For this
reason, we wanted to avoid the use of external JavaScript libraries
when possible.
After some careful consideration and a more detailed analysis
of our needs, we discovered that we only needed a small handful of widgets that
GWT didn't already provide, and all but one of those (covered in the next section)
were easily built using
Composite. There is already an
excellent
blog entry for building Composites, so we will not cover that topic here.
The visual styling of both the native and Composite widgets was easy
to customize thanks to GWT's liberal and logical use of CSS classnames.
As a quick demonstration, it's pretty impressive what a difference
you can achieve by building a few simple Composite widgets and
tweaking some of the default styles. Compare the default styling
of an input form with one modified by a few simple customizations:
GWT Default Styles & Widgets
|
|
If you have not already jumped on the bandwagon, Firefox+Firebug
is an indispensable tool for inspecting HTML and tweaking CSS.
Thanks to this and the flexibility of the GWT library, we were able
to achieve the polished look that we wanted without much work and
without sacrificing performance.
Building a Widget from an External Library
What about the one widget that we couldn't build using
Composite?
We want to use vector graphics to generate scalable diagrams depicting
database structure and user interaction for systems you build with
Hydro4GE. There are a small handful of vector graphics libraries
out there for GWT, but all of them require concessions that we are
not willing or able to make. The Google Web Toolkit Incubator's
GWTCanvas
does not support text rendering (due to a limitation of HTML canvas),
and although projects such as
abstractcanvas
attempt to overcome this limitation, text cannot be rotated and
precisely scaled.
In this section, we will show you how to integrate with
Raphael, a lightweight JavaScript
library for cross-platform vector graphics. Raphael side-steps the
HTML canvas issue by using SVG on supported platforms, and Microsoft
VML on Internet Explorer. Raphael does everything we need to build
our diagrams, except for one thing: integrate directly with our GWT
code.
We achieved this integration through two levels of abstraction: (1) a
JavaScript Overlay Type
to provide a zero-overhead interface to the underlying JavaScript API,
and (2) a
GWT Widget
to wrap the overlay with a more Java-friendly API, resulting in a first-class Widget that will operate
side-by-side with native GWT Widgets. Let's have a look at the
details for this two-part implementation, starting with the
Overlay class.
The Overlay
The RaphaelJS class is nearly an exact replica of the underlying
Raphael API. This is made necessary by the restrictions that GWT
enforces on
JavaScriptObject types, so the implementation is fairly uncreative:
one method per Raphael method. Many of these methods appear in the nested
class
Shape, which represents the type returned by most of the native Raphael
methods. The basic idea for the class is:
class RaphaelJS extends JavaScriptObject {
protected static class Shape extends JavaScriptObject {
public final native Shape rotate(double degree, boolean abs) /*-{
return this.rotate(degree, abs);
}-*/;
public final native Shape scale(double sx, double sy) /*-{
return this.scale(sx, sy);
}-*/;
public final native Shape translate(double dx, double dy) /*-{
return this.translate(dx, dy);
}-*/;
// ...
}
/**
* factory method
*/
static public final native RaphaelJS create(Element e,
int w, int h) /*-{
return $wnd.Raphael(e, w, h);
}-*/;
public final native Element node() /*-{
return this.node;
}-*/;
public final native Shape circle(double x, double y, double r) /*-{
return this.circle(x,y,r);
}-*/;
public final native Shape rect(double x, double y,
double w, double h) /*-{
return this.rect(x, y, w, h);
}-*/;
// ...
}
The Widget
The Overlay bridges the gap between GWT and Javacript, but the
Widget is what truly makes the library useful to our application.
Since the Widget does not have the restrictions of the
JavaScriptObject
Overlay, we have the freedom to define our own API as an adaptor
to the Overlay.
public class Raphael extends Widget {
private RaphaelJS overlay;
public class Shape extends Widget {
protected RaphaelJS.Shape rs;
protected Shape(RaphaelJS.Shape s) {
setElement(s.node());
rs = s;
}
public Shape rotate(double degree, boolean isAbsolute) {
rs.rotate(degree, isAbsolute);
return this;
}
public Shape scale(double sx, double sy) {
rs.scale(sx, sy);
return this;
}
public Shape translate(double dx, double dy) {
rs.translate(dx, dy);
return this;
}
// ...
}
public class Circle extends Shape {
public Circle(double x, double y, double r) {
super(overlay.circle(x, y, r));
}
}
public class Rectangle extends Shape {
public Rectangle(double x, double y, double w, double h) {
super(overlay.rect(x, y, w, h));
}
public Rectangle(double x, double y, double w, double h, double r) {
super(overlay.rect(x, y, w, h, r));
}
}
public class Text extends Shape {
public Text(double x, double y, String str) {
super(overlay.text(x, y, str));
}
}
// ...
public Raphael(int width, int height) {
Element raphaelDiv = DOM.createDiv();
setElement(raphaelDiv);
overlay = RaphaelJS.create(raphaelDiv, width, height);
}
}
This implementation defines separate classes that represent the
different types of objects (
Circle,
Text,
Rectangle) developers can
append to a drawing. A possible alternative implementation might
simply expose the underlying JavaScript API as a native GWT widget
— in the end, you can write an API that suits your needs.
We chose this implementation because it allows us to implement
scalable drawings through inheritance, resulting in custom classes that
can be instantiated and appended to any Panel. For example, to
create a fullscreen drawing that contains a single, centered circle
of radius 20:
public class MyDrawing extends Raphael {
public MyDrawing(int width, int height) {
super(width, height);
Circle c = new Circle(width/2, height/2, 20);
// Raphael automatically appends the Circle to this drawing
}
}
public class MyApp implements EntryPoint {
public void onModuleLoad() {
MyDrawing d = new MyDrawing(Window.getClientWidth(),
Window.getClientHeight());
RootPanel.get().add(d);
}
}
This is a trivial example, but it clearly demonstrates the simplicity
that can be achieved by integrating an external JavaScript library
as a Widget. To the consumers of this library, there is no difference
between using it (a third-party Javascript library) and the native
GWT widget library. That's a powerful statement, and a testament
to the GWT design team.
Communicating with the server
With the UI visually complete, it was time to move on to integration
with the backend. Our existing backend was implemented in PHP, and
we did not want to rewrite it in Java just to support GWT-RPC.
Without native support for GWT-RPC calls to a PHP backend, we needed
a flexible and efficient communication framework to accomplish the
equivalent task. We chose JSON for its simplicity and solid support
in both GWT and PHP, but ultimately we learned that regardless of
the efficiency of the encoding, it's still easy to make design
mistakes that will lead to inefficient communications.
First, we need to get the client and server talking to each other.
Making JSON-encoded client requests from GWT is made trivial by using the
RequestBuilder
class in conjunction with
JSONObject,
and handling JSON on the server with PHP is easily accomplished using
Zend Framework,
which provides the
Zend_Json
class for encoding/decoding JSON, and
Zend_Controller
for handling and routing requests. This provides the framework we
need to tie into our PHP code running on the server.
Having addressed the encoding and handling of requests, our
attention turned to dealing with the size and frequency of requests.
The whole point of making AJAX requests was to transfer little bits
of data instead of re-sending an entire HTML document, but we found
a break-even point where the payload became so small that the request
frequency was the limiting factor. At one point, we had gotten so
carried away breaking up information into atomic pieces that the
overhead of each request was exceeding 90% of the total time to
complete the request, leaving less than 10% of the time to actually
process and transfer the payload.
The reason this can happen is pretty straightforward. Over any
network connection, there is a minimum amount of time necessary for
the HTTP request/response to live its lifecycle. This is compounded
by the two-connection browser limit plus overhead imposed by the
network stack and connection latency. The net effect of all this
is that if you dispatch one hundred requests in rapid succession,
each of which contains only a few hundred bytes of payload data,
it would take nearly one hundred times as long to complete all one
hundred requests as it would take to transfer the entire payload
(tens of kilobytes) in one request. In terms of real-world figures,
this translates to a 300 or 400 millisecond time for completion,
as opposed to 30 or 40 seconds!
This lesson reminds us that when you write software, you must
always design for the limitations of the platform, even when you
have great tools at your disposal. In this case, to write efficient
web software with GWT, you need to make every HTTP request reach
its fullest potential by maximizing its effects.
We achieved this by modeling each request as a collection of
atomic commands. In this simple model, each command can return a
set of data, and is associated with one or more response data
handlers. The commands are processed on the server within a database
transaction so that the entire request either succeeds or fails as
a single unit without leaving the database in an inconsistent
state.
This abstraction allows us to queue up many physical database
operations into one logical JSON request, not only improving
performance but also allowing us to handle the results in a modular
way that involves less code than a typical GWT request callback.
In this sense we have actually managed to reduce the complexity of
the asynchronous HTTP request for this specialized case. For
example, take the code that handles selection of user roles from
the Hydro4GE database:
public class SelectRoleHandler implements DatabaseRequest.Handler {
public void handle(DatabaseRequest.Result result) {
for (int row=0; row < result.getRowCount(); row++) {
JSONObject data_row = result.getRowValues(row);
Role r = new Role(data_row);
// process role...
}
}
}
Compare this to the code that you would typically write for an
HTTP request callback:
public class SelectRoleCallback implements RequestCallback {
public void onError(Request request, Throwable exception) {
Window.alert("Couldn't retrieve JSON");
}
public void onResponseReceived(Request request, Response response) {
if (200 == response.getStatusCode()) {
JSONArray data_set =
JSONParser.parse(response.getText()).isArray();
if (data_set != null) {
for (int row=0; row < data_set.size(); row++) {
JSONObject data_row = data_set.get(row).isObject();
Role r = new Role(data_row);
// process role...
}
}
} else {
Window.alert("Couldn't retrieve JSON ("
+ response.getStatusText() + ")");
}
}
}
Besides being easier to understand (and half as many lines!),
the Handler code has the advantage over the
Callback in that multiple
independent handlers can be attached to a single request in order
to achieve separation of responsibility in handling each response.
In addition, each handler is isolated from the results of other
commands in the same request, so that there is no confusion over
which results you are handling. This example does not even introduce
the complexity of handling multiple command responses in the typical
RequestCallback code, but the
Handler code supports it implicitly
by its nature.
These concepts can and should be applied to GWT-RPC or any other
communications encoding. The implementation details are different,
but the spirit is the same: make every HTTP request really count.
Conclusion
The techniques I have demonstrated here can be used
equally effectively in both new and existing projects. Whether
you are trying to build a slick UI, wrap an external library, or
talk to a server, I have only scratched the surface of what is
possible with GWT. I am especially looking forward to GWT 2.0 for
continued improvements on some of the topics covered here, made
possible by some exciting upcoming features such as
UiBinder and
CssResource.
In short, I am pretty excited about GWT and Hydro4GE, and I hope
you are too. Be sure to check out the sneak peek of
Hydro4GE to see a
demo of our GWT UI in action!
Geoffrey C. Speicher, MS is a Software Engineer at Software Engineering
Associates and the Chief System Architect of Hydro4GE.