Kurs:Neueste Internet- und WWW-Technologien/Web-Entwicklung mit GWT (Google Web Toolkit)

Google Web Toolkit (GWT) ist ein unter der Apache License 2.0 veröffentlichtes Open Source Toolkit zur clientseitigen Ajax-Webanwendungsentwicklung in Java. Dabei wird das Webseitenlayout durch Widgets definiert. Durch einen Java-nach- JavaScript-Compiler wird der Code nach JavaScript übersetzt und im Browser ausgeführt.

Entwicklungsgeschichte von GWT Bearbeiten

Der erste GWT Release Candiate wurde am 16. Mai 2006 veröffentlicht[1]. Seit Version 1.2 RC 1 wird neben Windows und Linux auch Mac OS für die Anwendungsentwicklung unterstützt. GWT 2.0 erschien am 8. Dezember 2009 und stellte als neue Features den Development Mode und Declarative User Interfaces bereit. Die zum derzeitigen Stand aktuellste Version von GWT ist Version 2.5.0 RC 1 und ersetzt den Development Mode durch den Super Dev Mode. Der Funktionsumfang wird weiterhin durch The Elemental Library zum effizienten DOM-Zugriff und die Nutzung der HTML5 APIs erweitert[2].

Ziele der Technologie Bearbeiten

Viele Back-Ends von Webseiten sind in Java geschrieben. Java EE findet vor allem im Business Computing häufige Anwendung. GWT bietet dem erfahrenen Java-Entwickler einen schnellen Einstieg in die Webentwicklung. Für praktisch alle Fälle kann vollständig auf JavaScript verzichtet werden. Trotzdem bietet GWT dem erfahrenen JavaScript Programmierer eine JavaScript Schnittstelle, das JavaScript Native Interface. Der Java-nach-Javascript-Compiler erzeugt Cross Browser optimierten JavaScript Code und unterstützt dabei auch Android und iPhone Browser. Dabei wird vorausgesetzt, dass JavaScript im Browser aktiviert ist.

Das Google Web Toolkit versteht sich selbst weniger als ein Web-Framework, mehr als ein Tool Set für Entwickler[3]. Dabei diktiert GWT weniger Vorgaben als andere bekannte Web Frameworks. Beispiele dafür sind die Client-Server-Kommunikation, die erst vom Entwickler nach seinen Wünschen programmiert werden muss und der Verzicht auf eine Model-View-Controller Projektstruktur. GWT bietet weiterhin eine Abstraktionsschicht über der DOM-Manipulation. Im neusten Release wurde diese Abstraktionsschicht aus Performancegründen aufgeweicht und kann nun optional direkt angesprochen werden. Zur Front-End-Enwicklung werden Widgets eingesetzt. Dabei erinnert die Implementierung an klassische GUI-Frameworks wie Java Swing oder QtGui. Durch Verwendung der Sprache Java bietet GWT statische Typisierung. So können bereits zur Compile-Zeit mehr Fehler erkannt werden.

Um in der Entwicklungsphase der Webseite lange Compile-Zeiten zu vermeiden, bietet GWT den Super Dev Mode, früher Hosted/Developmentmode. Dieser lässt die Webseite in einer Java VM laufen und bietet somit eingeschränkte Möglichkeiten für Hot Deployments. Neu im Super Dev Mode dazugekommen ist die Neukompilierung von Codefragmenten. Somit können nun auch neu erstellte Funktionen und Klassen hot deployed werden. Zuvor funktionierte Hot Deployment nur bei Änderungen innerhalb der bereits zur Compilezeit bekannten Klassenstrukturen.

Integration in Eclipse Bearbeiten

GWT Projekte werden üblichweise durch das Maven-Buildsystem oder Eclipse-spezifisch als Web Application Project erzeugt. Exemplarisch wird hier die Integration in Eclipse gezeigt. Das Google Web Toolkit SDK kann in Eclipse über Help > Install New Software ... unter Angabe der folgenden URL: http://dl.google.com/eclipse/plugin/3.7 installiert werden. Die Nummer hinter Plugins richtet sich dabei nach der verwendeten Eclipse Versionsnummer. Um im Development Mode entwickeln zu können, ist noch das Development Mode Plugin für einen beliebigen Browser zu installieren. Hier sei erwähnt, dass für die aktuellsten Browserversionen meist kein passendes Plugin verfügbar ist. Zur Zeit der Erstellung funktionierte nur ein Chrome Plugin unter Windows.

GWT-Tutorial Bearbeiten

Das folgende Tutorial soll den geneigten Leser mit den Grundzügen der GWT-Entwicklung vertraut machen. Es baut dabei auf der Arbeit von Bastian Tenbergen auf[4]. Es wird ein Login Screen entwickelt, der den Nutzer anhand seines Passworts identifiziert und einloggt. Ich warne dabei ausdrücklich Teile des hier gelisteten Code in Real World Applikationen zu integrieren. Ziel des Tutorials ist GWT umfangreicher kennenzulernen, nicht aber auf Sicherheitsaspekte einzugehen.

Front-End Entwicklung Bearbeiten

Als Programmeinstiegspunkt muss mindestens eine Klasse das EntryPoint Interface implementieren. Die dabei implementierte onModuleLoad-Methode wird dabei nach dem Konstruktor der Klasse gerufen. Die Widget-Nutzung und das Widget Subclassing funktioniert wie in gängigen GUI Toolkits. Zum Layouten einer Seite werden üblicherweise GWT Panels verwendet. Dabei handelt es sich um Container-Widgets, die andere Widgets enthalten und grafisch anordnen. Als Widgets stehen unter anderem HTML Objekte, Buttons, Hyperlinks und die erwähnten Layouts zur Verfügung. Eine vollständige Liste der bereits vordefinierten Widgets findet sich hier. Um auf Benutzereingaben reagieren zu können, werden EventHandler mit Methoden wie onClick() und onKeyPress() definiert. Für den Programmierer ist es außerdem wichtig zu wissen, dass GWT nur eine Teilmenge der Java runtime library implementiert. Eine vollständige Liste der unterstützten Funktionalität findet sich hier. Im Folgenden ist Beispielcode für Layouts und EventHandler gelistet.

Panel Bearbeiten

VerticalPanel dialogVPanel = new VerticalPanel();
dialogVPanel.addStyleName("dialogVPanel");
dialogVPanel.setHorizontalAlignment(VerticalPanel.ALIGN_RIGHT);
dialogVPanel.add(successHTML);
dialogVPanel.add(closeButton);
dialogBox.setWidget(dialogVPanel);

EventHandler Bearbeiten

// KeyPressHandler defined inline
passwordBox.addKeyPressHandler(new KeyPressHandler() {
    public void onKeyPress(KeyPressEvent event)
    {
        if(event.getCharCode() == KeyCodes.KEY_ENTER)
	{
	    login();
	}
    }
});

Einbettung in statische Webseiten und CSS-Integration Bearbeiten

Standardmäßig wird die GWT-Applikation als gesamtes HTML-Body-Element eingebettet. Üblicher ist jedoch die Integration in HTML Elemente über eine id.

<body>
<h1>Google Web Toolkit: Database Access</h1>
<div id="loginScreen"></div>
</body>

Auf diese id kann dann in GWT zugegriffen werden.

// associate the main panel with the HTML host page
RootPanel.get("loginScreen").add(mainPanel);

Es bestehen zwei Möglichkeiten Styles aus .css Dateien anzuwenden. Der Code zeigt die Anwendung eines neuen sekundär definierten Styles.

errorMsgLabel.setStyleName("errorMessage");

Möchte man bereits bestehende primäre Styles verändern bzw. erweitern, muss man eine Namenskonvention in der CSS-Datei beachten. Dazu werden bereits vorhandene primäre GWT Styles mit einem Bindestrich und einer beliebigen Zeichenfolge erweitert.

/* login button styling*/
.gwt-Button-login {
width: 50px;
}

Die Einbindung in GWT erfolgt über die setStyleDependentName-Methode.

okButton.addStyleDependentName("login");

Remote Procedure Calls - Kommunikation mit dem Back-End Bearbeiten

Fast jede etwas komplexere Webanwendung kommuniziert in irgendeiner Form mit einem Back-End, das Daten in einer Datenbank ablegt und auf diese zugreift. Im hier gezeigten Tutorial handelt es sich um ein Java Back-End. Das ist jedoch nicht umbedingt erforderlich. GWT kann über das JSON Format auch mit Python, PHP oder anderen Back-Ends kommunizieren. In jedem Fall erfolgt diese Kommunikation aber über Remote Procedure Calls. Nutzt man GWT mit einem Java Back-End, dann läuft der Servercode in einem Servlet.

 
Interfaces und Klassen zur Kommunikation mit einem Java Back-End über Remote Procedure Calls.

Um einen Remote Procedure Call absetzen zu können definiert man ein Interface das von RemoteService erbt, den Service zur Verfügung stellt und alle RPC Methoden listet. Danach erzeugt man eine Klasse die von RemoteServiceServlet erbt und das oben definierte Interface implementiert. Außerdem wird noch ein asynchrones Interface für den Service definiert, der von der Client-Seite mit Hilfe eines Callback gerufen werden kann. Im Client-Code generiert GWT selbstständig eine Service Proxy Klasse, die über eine Fabrikmethode eine Klasse des asynchronen Interfaces aus der Service Klasse erstellt. Clientseitig muss nun noch eine Klasse die das Generic AsyncCallback Interface implementiert erstellt werden. Dazu müssen die Methoden onFailure und onSuccess implementiert werden[5]. Der clientseitige Quellcode verdeutlich den Vorgang:

private final DBConnectionAsync rpc;

// initialize the service proxy
rpc = (DBConnectionAsync)GWT.create(DBConnection.class);

// start remote procedure call
AsyncCallback<User> callback = new AuthenticationHandler<User>();
rpc.authenticateUser(usernameBox.getText(), passwordBox.getText(), callback);

// nested private class implements AsyncCallback Generic
private class AuthenticationHandler<T> implements AsyncCallback<User>
{
    public void onFailure(Throwable ex)
    {
	// ...
    }
	
    public void onSuccess(User result)
    {
        // ...
    }
}

Servlet Name Mapping Bearbeiten

Um auf der Server-Seite das richtige Servlet anzusprechen fordert GWT ein Servlet Name Mapping. Dazu gibt es im war Verzeichnis des Projekts eine Datei web.xml, die über das Tag <servlet-mapping> den Servlet Container spezifiziert.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
              http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5"
         xmlns="http://java.sun.com/xml/ns/javaee">

  <!-- Servlets -->
  <servlet>
    <servlet-name>mySQLConnection</servlet-name>
    <servlet-class>com.tutorial.server.MySQLConnection</servlet-class>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>mySQLConnection</servlet-name>
    <url-pattern>/loginscreen/login</url-pattern>
  </servlet-mapping>
  
  <!-- Default page to serve -->
  <welcome-file-list>
    <welcome-file>LoginScreen.html</welcome-file>
  </welcome-file-list>

</web-app>

Das synchrone Interface für den Service mappt diese Angaben durch Java Annotations.

//This associates the service with a default path relative to the module base URL
@RemoteServiceRelativePath("login")
public interface DBConnection extends RemoteService
{
    public User authenticateUser(String user, String pass);
}

Serialisierung Bearbeiten

Zum Transfer von Daten zwischen Client und Server werden in diesem Tutorial Data Transfer Objects verwendet. Auch hier sei gesagt, dass auch JSON als Format verwendet werden kann. Diese serialisierbaren, reinen Datenklassen unterliegen einigen Restriktionen. DTOs müssen entweder GWTs IsSerializable Interface, oder java.io.Serializable implementieren. Zusätzlich müssen diese noch einen leeren parameterlosen Konstruktor haben. Da dieser von der Anwendung nie gerufen wird, sollte dieser private definiert werden. Außer diesen serialisierbaren, nutzerdefinierten Klassen definiert GWT, dass ein Typ serialisierbar ist, wenn[6]

  • es sich um einen Primitivtyp, String, Date oder eine Primitiv-Wrapper-Klasse handelt
  • es sich um ein Array serialisierbarer Typen handelt (Arrays dürfen Arrays enthalten).

Datenbankverbindung im Servlet einrichten Bearbeiten

Die Datenbankverbindung wird in diesem Tutorial über JDBC aufgebaut. Außerhalb des Development Modes ist es notwendig, dass der benötigte JDBC-Treiber auch im war Verzeichnis des Deployment Servers vorhanden ist. Der gelistete Konstruktor zeigt den Verbindungsaufbau. Es sei darauf hingewiesen, dass die Fehlerbehandlung keinesfalls ausreichend für den Real World Einsatz ist. Würde ein Client den Service ansprechen, würden Datenbankanfragen an ein NULL Objekt abgesetzt werden.

public MySQLConnection()
{
    try
    {
	Class.forName("com.mysql.jdbc.Driver").newInstance();
	conn = DriverManager.getConnection(url, user, pass);
    }
    catch(Exception e)
    {
	System.out.println("***ERROR:***" + e.getMessage());
    }
}

Deployment auf Tomcat Bearbeiten

Das Deployment der Webapplikation wird hier exemplarisch auf einem Tomcat gezeigt. Damit die Datenbank angesprochen werden kann muss der Datenbanktreiber sowohl im Projekt Classpath, als auch im war Verzeichnis des Tomcats hinzugefügt werden.

 
Projektstruktur in Eclipse. Markiert sind die Datenbanktreiber im Classpath sowie auf dem Tomcat.

Um Java nach JavaScript zu kompilieren wählt man mit Rechtsklick das Projekt aus und dann Google > GWT Compile. Danach müssen alle benötigten Bibliotheken in war/WEB-INF/lib kopiert werden. Des Weiteren müssen alle Java Sources in war/WEB-INF/classes kompiliert werden. Schließlich muss noch der Inhalt des war Ordners in einen separaten Ordner in Tomcat's /webapps Ordner kopiert werden. Eine Testeingabe im Browser wie http://localhost:8080/LoginScreen/LoginScreen.html führt zur Seite.

Debugging im Development Mode Bearbeiten

Ein großer Vorzug des Development Modes ist das Debugging. Da die Webseite in einer Java VM läuft, können Breakpoints wie gewohnt gesetzt und der gesamte Callstack überprüft werden. Es stehen alle Debuggingmechanismen die Eclipse bietet zur Verfügung.

GWT-Quellcode des Tutorials Bearbeiten

Für einen schnellen und erfolgreichen Einstieg in GWT sei hier die Tutorial Anwendung vollständig abgedruckt. Nicht aufgelistet sind die von GWT erzeugten Dateien LoginScreen.html, LoginScreen.css und web.xml. Die Datei web.xml wurde bereits hier abgedruckt.

Entry Point Klasse Bearbeiten

public class LoginScreen implements EntryPoint
{
    private final DBConnectionAsync rpc;
    private TextBox usernameBox = new TextBox();
    private PasswordTextBox passwordBox = new PasswordTextBox();
    private Button loginButton = new Button();
    private Label errorLabel = new Label();
    private DialogBox dialogBox = new DialogBox();
    private HTML successHTML = new HTML();
    private Button closeButton = new Button("Close");
	
    public GwtTest()
    {
	// initialize the service proxy
	rpc = (DBConnectionAsync)GWT.create(DBConnection.class);
    }
	
    @Override
    public void onModuleLoad()
    {
	loginButton.setText("Login");
		
	// get HTML body element
	RootPanel.get("nameFieldContainer").add(usernameBox);
	RootPanel.get("passwordFieldContainer").add(passwordBox);
	RootPanel.get("loginButtonContainer").add(loginButton);
	RootPanel.get("errorLabelContainer").add(errorLabel);
		
	// move cursor focus to the input box
	usernameBox.setFocus(true);
		
	// set Style
	errorLabel.setStyleName("errorLabel");
	errorLabel.setVisible(false);

	// create the PopUp dialog box
	dialogBox.setText("Login Successful");
	dialogBox.setAnimationEnabled(true);
	// we can set the id of a widget by accessing its Element
	closeButton.getElement().setId("closeButton");
	VerticalPanel dialogVPanel = new VerticalPanel();
	dialogVPanel.addStyleName("dialogVPanel");
	dialogVPanel.setHorizontalAlignment(VerticalPanel.ALIGN_RIGHT);
	dialogVPanel.add(successHTML);
	dialogVPanel.add(closeButton);
	dialogBox.setWidget(dialogVPanel);

	// add a handler to close the DialogBox
	closeButton.addClickHandler(new ClickHandler() {
  	    public void onClick(ClickEvent event)
	    {
		dialogBox.hide();
		loginButton.setEnabled(true);
		loginButton.setFocus(true);
	    }
	});
		
	// listen for mouse events on the login button
	loginButton.addClickHandler(new ClickHandler() {
	    public void onClick(ClickEvent event)
	    {
		login();
	    }
	});
		
	// listen for keyboard events in the input boxes
	passwordBox.addKeyPressHandler(new KeyPressHandler() {
	    public void onKeyPress(KeyPressEvent event)
	    {
		if(event.getCharCode() == KeyCodes.KEY_ENTER)
		{
		    login();
		}
	    }
	});
    }
	
    private void login()
    {
	if(FieldVerifier.isValidName(usernameBox.getText()) && FieldVerifier.isValidName(passwordBox.getText()))
	{
	    // start remote procedure call
	    AsyncCallback<User> callback = new AuthenticationHandler<User>();
	    rpc.authenticateUser(usernameBox.getText(), passwordBox.getText(), callback);
	}
	else
	{
	    errorLabel.setText("Text Fields are empty. Please insert Name and Password.");
	    errorLabel.setVisible(true);
	}
    }
	
    // nested private class
    private class AuthenticationHandler<T> implements AsyncCallback<User>
    {
	public void onFailure(Throwable ex)
	{
 	    errorLabel.setText(ex.getMessage());
	}
		
	public void onSuccess(User result)
	{
	    if(result == null)
	    {
		errorLabel.setVisible(true);
		errorLabel.setText("Login failed.\n User or Password are not valid. Try again.");
		return;
	    }
	    errorLabel.setText("");
	    loginButton.setEnabled(false);
	    successHTML.setHTML("<b>Welcome back, " + result.getUserName() + " !</b>");
	    dialogBox.center();
	    closeButton.setFocus(true);
	}
    }
}

MySQLConnection Klasse Bearbeiten

public class MySQLConnection extends RemoteServiceServlet implements DBConnection
{
    private Connection conn = null;
    private String url = "jdbc:mysql://localhost/tutorial_db";
    private String user = "root";
    private String pass = "";
	
    public MySQLConnection()
    {
	try
	{
	    Class.forName("com.mysql.jdbc.Driver").newInstance();
	    conn = DriverManager.getConnection(url, user, pass);
	}
	catch(Exception e)
	{
	    System.out.println("***ERROR:***" + e.getMessage());
	}
    }
	
    public User authenticateUser(String user, String pass)
    {
	User userObject = null;
	try
	{
  	    // normally at this point one would use a password hashing library but this is above the scope of the tutorial
	    PreparedStatement ps = conn.prepareStatement("select * from user where user = \"" + user + "\" AND " + "password = \"" + pass + "\"");
	    ResultSet result = ps.executeQuery();
			
	    while(result.next())
	    {
		userObject = new User(result.getString(1), result.getString(2));
	    }
			
	    result.close();
	    ps.close();
	}
	catch(SQLException e)
	{
	    System.out.println("***ERROR:***" + e.getMessage());
	}
		
	// this is also not secure but for this tutorial I use it to show how to serialize objects with rpc
	return userObject;
    }
}

DTO Klasse Bearbeiten

public class User implements Serializable
{
    private String username;
    @SuppressWarnings("unused")
    private String password;
	
    @SuppressWarnings("unused")
    private User()
    {
	// here because of the gwt serialization specs
    }
	
    public User(String username, String password)
    {
	this.username = username;
	this.password = password;
    }
	
    public String getUserName()
    {
	return username;
    }
}

DBConnection Interface Bearbeiten

//This associates the service with a default path relative to the module base URL
@RemoteServiceRelativePath("login")
public interface DBConnection extends RemoteService
{
    public User authenticateUser(String user, String pass);
}

DBConnectionAsync Interface Bearbeiten

public interface DBConnectionAsync
{
    public void authenticateUser(String user, String pass, AsyncCallback<User> callback);
}

Diskussion Bearbeiten

Das Google Web Toolkit geht einen anderen Weg als viele populäre Frameworks, deren Urvater Rails ist. GWT unterstützt weder Scaffolding noch Templates noch eine sofortige Datenbankanbindung. Auch eine Festlegung auf das MVC Entwurfsmuster entfällt. Die Lernkurve beginnt deshalb vergleichsweise flach und Standardfunktionalität muss erst einmal implementiert werden. Dieser Schritt erfordert Einarbeitungszeit um mit GWT effizient arbeiten zu können. Ist diese Hürde erst einmal genommen und ich hoffe das Tutorial ebnet dafür den Weg, dann entfaltet GWT seine Stärken. Als vorteilhaft stellt sich heraus, dass interne Abläufe besser als in anderen Frameworks nachvollzogen werden können. Durch die statische Typisierung von Java fallen viele Fehler schon zur Compile-Zeit auf und durch das Debugging im Development ist die Überwachung des Programmzustands ähnlich einfach wie bei der Erstellung von Desktopanwendungen. GWT ist für den weniger affinen JavaScript Entwickler eine gute und effiziente Alternative. Außerdem entlastet GWT den Programmierer ständig zwischen Templatesprachen und JavaScript zu wechseln. Das wirkt sich positiv auf die Codequalität aus. Außerdem lassen sich GWT Projekte sehr gut in bestehende Java EE Applikationen als Front-End einbinden und es stehen viele Java Werkzeuge auch für den GWT Entwickler zur Verfügung.

Nachteilig an Webseiten in der Java VM ist die Geschwindigkeit. Bereits bei wenig Seiteninhalt dauert der Methodenruf von onModuleLoad unverhältnismäßig lang. Durch die Hot Deployment Funktionalität wird dieser Effekt jedoch abgeschwächt und der neue Super Dev Mode verbessert die Situation noch einmal. Trotzdem ist es nicht zu empfehlen ständig die Webseite für den Server nach JavaScript zu kompilieren, denn dieser Vorgang dauert schon bei kleinen Anwendungen auf einem Standardrechner 30 Sekunden. Für Prototypen von Webseiten sollte GWT nicht verwendet werden. Dazu ist der Entwicklungsprozess zu behebig.

GWT ist für Java Programmierer einen Blick wert. Viele Aspekte der Webentwicklung werden angenehmerweise durch ein Toolset erschlagen. Cross Browser Optimierungen erledigt der Compiler. Ein großer Teil an "Framework Magic" entfällt und belässt die Kontrolle beim Programmierer. Der Preis dafür ist sich zuerst eine Infrastruktur zwischen Client und Server zu bauen. Bereits eingebaute Widgets eignen sich besonders für Businessanwendungen. Für andere Webauftritte eignen sich GWT Applikationen als Modul in einer Seite.

Einzelnachweise Bearbeiten

  1. GWT Release Archive Abgerufen am 22.07.2012
  2. GWT Release Notes Abgerufen am 22.07.2012
  3. GWT Design Axiome Abgerufen am 22.07.2012
  4. Database Access in GWT – The Missing Tutorial Abgerufen am 22.07.2012
  5. Creating Services. GWT Developer Guide Abgerufen am 22.07.2012
  6. Serializable Types. GWT Developer Guide Abgerufen am 22.07.2012

Weblinks Bearbeiten