Monthly Archives: Juli 2011

Modularisierung von GWT Anwendungen

Es macht viel Sinn eine GWT Anwendung zu modularisieren. Denn, was mit GWT richtig gut gelingt, ist die Wiederverwendung bereits geschriebenen Codes. Und, da der GWT Compiler nur referenzierte Klassen übersetzt, braucht man sich keine Sorgen über Code zu machen, der sich zwar im Modul befindet aber nicht verwendet wird. Das ist eine die gute Nachricht.

Ich habe mir ein Szenario ausgedacht, das veranschaulichen soll, wie die Modularisierung von GWT Projekten funktionieren kann; und ich setze dabei auf Maven. Insbesondere auf das GWT Maven Plugin.

Modul Abhängigkeiten

Modul Abhängigkeiten

  • API Ist ein Modul, dass Transferobjekte, Service-Interfaces und Command-Objekte enthält. Also der kleinste gemeinsame Nenner von Server und Client. Dieses Modul hat keine Abhängigkeiten zu GWT oder sonstigen Bibliotheken. Es enthält nur Klassen, die sowohl der Client, als auch der Server kennen müssen. So dass dieses Modul praktisch auch für andere Clients genutzt werden kann. Ich denke dabei an eine iPhone oder Android App.
  • GWT-Base Ist das solide Fundament weiterer GWT Module. Es kennt GIN, einen EventBus, eine RPC Schnittstelle, allgemeine Events, Fehlerhandling, Mocks, Testklassen, usw. Also alles das, was eine GWT Anwendung grundsätzlich benötigt und für das jedes mal ein neues Setup notwendig wäre.
  • GWT-Shop enthält die Basisfunktionalität des Shopsystems.
  • Die weiteren Module sind dann die konkrete Anwendungen.

Der Anfang – das Wurzelprojekt

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
<?xml version="1.0" encoding="UTF-8"?>
<project ...>
  <groupId>com.example.client</groupId>
  <artifactId>root</artifactId>
  <packaging>pom</packaging>
  <version>1.0</version>
 
  <modules>
    <module>api</module>
    <module>gwt-base</module>
    <module>gwt-shop</module>
    <module>gwt-...</module>
    <module>...</module>
  </modules>
 
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.7</version>
      <scope>test</scope>
    </dependency>
    ...
  </dependencies>
 
  <properties>
    <projectVersion>0.0.3</projectVersion>
    <gwtVersion>2.3.0</gwtVersion>
    <maven.compiler.source>1.6</maven.compiler.source>
    <maven.compiler.target>1.6</maven.compiler.target>
  </properties>
 
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.0.2</version>
        <configuration>
          <source>${maven.compiler.source}</source>
          <target>${maven.compiler.target}</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-site-plugin</artifactId>
        <version>2.0-beta-6</version>
        <configuration>
          <inputEncoding>UTF-8</inputEncoding>
          <outputEncoding>UTF-8</outputEncoding>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Ich beginne mit dem Wurzelprojekt und definiere die groupId, die artifactId und die version. Das packaging muss pom sein, um Maven zu erklären, dass es Untermodule gibt, die sich auf dieses Wurzelprojekt beziehen.

Zudem kommen die einzelnen Module hinzu. Dann kann ich hier bereits die Möglichkeit nutzen Abhängigkeiten anzugeben, die in allen anderen Modulen ebenfalls benötigt werden. z.B. jUnit. Auch welche Compiler-Version zu verwenden ist und welches Encoding. Alles das, was nur einmal gemacht werden sollte und was für die Untermodule ebenfalls gilt.

Zum Schluss definiere ich noch eine Eigenschaft, die ich mit projectVersion benenne und als Wert eine Versionsnummer vergebe. Wozu die ist, dazu komme ich gleich.

Das API Modul

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
<?xml version="1.0" encoding="UTF-8"?>
<project ...>
  <groupId>com.example.client</groupId>
  <artifactId>api</artifactId>
  <version>${projectVersion}</version>
 
  <parent>
    <groupId>com.example.client</groupId>
    <artifactId>root</artifactId>
    <version>1.0</version>
  </parent>
 
  <build>
    <resources>
      <resource>
        <directory>src/main/java</directory>
        <includes>
          <include>**/*.java</include>
        </includes>
      </resource>
      <resource>
        <directory>src/main/resources</directory>
        <includes>
          <include>**/*.xml</include>
        </includes>
      </resource>
    </resources>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-source-plugin</artifactId>
        <version>2.1.2</version>
        <executions>
          <execution>
            <id>attach-sources</id>
            <phase>verify</phase>
            <goals>
              <goal>jar-no-fork</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Das Wurzelprojekt steht, so dass ich mich nun an das API Modul machen kann. Das es sich um ein reines Java-Modul ohne weitere Abhängigkeiten handelt, gibt es hier nur drei Besonderheiten, auf die ich aufmerksam machen möchte:

  1. Die Version des Moduls bekommt den Wert aus der in dem Wurzelprojekt definierten ${projectVersion}-Variable. In Maven gibt es leider nicht die Möglichkeit auf die Version des übergeordneten Projekts zu verweisen (Die Verwendung von ${parent.version} als Version ist nicht erlaubt). Andererseits möchte ich nicht für jedes Modul eine eigene Version pflegen, deshalb diese Krücke.
  2. Da der GWT Compiler den Quellcode seiner Abhängigkeiten kennen muss, nutze ich das maven-source-plugin, um alle Quellen (*.java) mit in das Archiv zu packen.
  3. Damit GWT dieses Modul als ein GWT-Modul erkennt, muss noch die Api.gwt.xml definiert werden und sich ebenfalls im Archiv befinden. Die Moduldefinition muss dabei lediglich den Packagenamen der für GWT sichtbaren Quellen enthalten:

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<module rename-to='client-api'>
  <source path='client'/>
</module>

Das GWT-Base Modul

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<?xml version="1.0" encoding="UTF-8"?>
<project ...>
  <groupId>com.example.client</groupId>
  <artifactId>gwt-base</artifactId>
  <packaging>jar</packaging>
  <version>${projectVersion}</version>
 
  <parent>
    <groupId>com.example.client</groupId>
    <artifactId>root</artifactId>
    <version>1.0</version>
  </parent>
 
  <dependencies>
    <dependency>
      <groupId>com.example.client</groupId>
      <artifactId>api</artifactId>
      <version>${projectVersion}</version>
    </dependency>
    <dependency>
      <groupId>com.google.gwt</groupId>
      <artifactId>gwt-user</artifactId>
      <version>2.3.0</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.google.gwt</groupId>
      <artifactId>gwt-servlet</artifactId>
      <version>2.3.0</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>com.google.gwt.inject</groupId>
      <artifactId>gin</artifactId>
      <version>1.5.0</version>
    </dependency>
    <dependency>
      <groupId>com.google.inject</groupId>
      <artifactId>guice</artifactId>
      <version>3.0</version>
    </dependency>
    <dependency>
      <groupId>com.google.inject.extensions</groupId>
      <artifactId>guice-servlet</artifactId>
      <version>3.0</version>
    </dependency>
  </dependencies>
 
  <build>
    <resources>
      <resource>
        <directory>src/main/java</directory>
        <includes>
          <include>**/*.java</include>
        </includes>
      </resource>
      <resource>
        <directory>src/main/resources</directory>
        <includes>
          <include>**/*.xml</include>
        </includes>
      </resource>
    </resources>
 
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>gwt-maven-plugin</artifactId>
        <version>2.3.0</version>
        <configuration>
          <compileReport>true</compileReport>
          <modules>
            <module>com.example.client.gwtbase.Base</module>
          </modules>
          <compileSourcesArtifacts>
            <compileSourcesArtifact>com.example.client:api</compileSourcesArtifact>
          </compileSourcesArtifacts>
        </configuration>
      </plugin>
 
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-source-plugin</artifactId>
        <version>2.1.2</version>
        <executions>
          <execution>
            <id>attach-sources</id>
            <phase>verify</phase>
            <goals>
              <goal>jar-no-fork</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
 
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>2.2</version>
        <executions>
          <execution>
            <id>add-test-files</id>
            <goals>
              <goal>test-jar</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
 
  <repositories>
    <repository>
      <id>gin</id>
      <url>http://repo2.maven.org/maven2/</url>
    </repository>
  </repositories>
</project>

Jetzt wird es spannend, denn es geht um das erste richtige GWT Modul. Folgendes ist wichtig:

  1. Es wird die Abhängigkeit zum API Modul definiert.
  2. Es werden weitere Abhängigkeiten zu GWT, GIN und Guice definiert.
  3. Das GWT Maven Plugin kommt zum Einsatz und wird so konfiguriert, dass die Quellen des API-Moduls gezogen und mit übersetzt werden. Siehe compileSourcesArtifacts in der POM und mehr dazu gibt es hier.
  4. Dieses Modul wird ebenfalls mit den Quellen in das Maven Repository gepackt. Darüber hinaus werden auch noch die Quellen aus den Testverzeichnissen mitgenommen. Der Grund ist, auf diese Weise kann bereits hier die Testumgebung mit Mocks und Basis-Testklassen aufgebaut werden. Da Guice als DI Framework verwendet wird, bietet es sich an Guice-TestModule zu implementieren und sie dann in den anderen GWT Modulen wieder zu verwenden.

Wie die GIN und Guice Konfiguration für das Base-Modul aussieht, erkläre ich in einem anderen Post.

Weitere GWT Module

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?xml version="1.0" encoding="UTF-8"?>
<project>
  <artifactId>gwt-shop</artifactId>
  <version>${projectVersion}</version>
 
  <parent>
    <groupId>com.example.client</groupId>
    <artifactId>gwt</artifactId>
    <version>1.0</version>
  </parent>
 
  <dependencies>
    <dependency>
      <groupId>com.example.client</groupId>
      <artifactId>gwt-base</artifactId>
      <version>${projectVersion}</version>
    </dependency>
    <dependency>
      <groupId>com.example.client</groupId>
      <artifactId>gwt-base</artifactId>
      <version>${projectVersion}</version>
      <type>test-jar</type>
    </dependency>
    <dependency>
      <groupId>com.googlecode.gwtquery</groupId>
      <artifactId>gwtquery</artifactId>
      <version>1.0.0</version>
      <classifier>2.3.0</classifier>
      <scope>provided</scope>
    </dependency>
  </dependencies>
 
  <build>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>gwt-maven-plugin</artifactId>
        <version>2.3.0</version>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>test</goal>
              <goal>generateAsync</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <modules>
            <module>com.example.client.gwtshop.Shop</module>
          </modules>
          <compileSourcesArtifacts>
            <compileSourcesArtifact>com.example.client:api</compileSourcesArtifact>
            <compileSourcesArtifact>com.example.client:gwt-base</compileSourcesArtifact>
          </compileSourcesArtifacts>
 
          <inplace>true</inplace>
          <webappDirectory>${gwt.war.dir}</webappDirectory>
          <warSourceDirectory>${gwt.war.dir}</warSourceDirectory>
        </configuration>
      </plugin>
    </plugins>
  </build>
 
  <repositories>
    <repository>
      <id>gwtquery-plugins</id>
      <url>http://gwtquery-plugins.googlecode.com/svn/mavenrepo</url>
    </repository>
  </repositories>
 
  <properties>
    <gwt.loglevel>INFO</gwt.loglevel>
    <gwt.war.dir>${basedir}/../../GRAILS_ROOT/web-app</gwt.war.dir>
  </properties>
</project>

Das GWT-Shop Modul soll hier das letzte sein. Aber auch aus dieser pom.xml gibt es was zu lernen:

  1. Zum einen definiert dieses Modul wie erwartet eine Abhängigkeit zu dem GWT-Base Modul. Aber diese Abhängigkeit existiert zwei mal. Einmal für die eigentlichen Quellen, und das zweite mal für die Test-Quellen. Das wird mit dem <type>test-jar</type> erreicht. Auf diese Weise kann auf die Testklassen des Basismoduls zugegriffen werden.
  2. Dann gibt es in der Konfiguration des GWT Maven Plugins ein executions Block, der dafür sorgt, dass in der Übersetzungsphase nicht nur der Java-Code übersetzt wird, sondern auch der GWT-Compiler anschließend anspringt und das JavaScript Kompilat erzeugt.
  3. Unter compileSourcesArtifacts werden nicht nur die API Quellen gezogen und in die Übersetzung mit einbezogen, sondern auch die Quellen des GWT-Base Moduls.
  4. Ich baue meine Anwendungen mit TeamCity. Das Deployment funktioniert so, dass zuerst die GWT Anwendungen übersetzt werden, dann die Grails Anwendung übersetzt und gepackt und anschließend in ein Tomcat deployed wird. Das alles geschiet automatisch und da bietet es sich an, die GWT Kompilate direkt in ein Verzeichnis der Grails Anwendung zu kopieren, damit es automatisch in das WAR-Archiv kopiert wird. Das geschieht mit der Angaben von webappDirectory und warSourceDirectory, die auf das Zielverzeichnis in der Grails Anwendung zeigen.

So, wenn ich alles richtig gemacht und nichts vergessen habe, dann sollte ein mvn clean install auf dem Wurzelprojekt zuerst das API-Modul und dann das GWT-Base Modul in das lokale Maven Repository installieren. Anschließend sollte das GWT-Shop Modul kompilieren. Und wenn das alles hingehauen hat und kein Test fehlgeschlagen ist, sollte sich das Kompilat in einem Verzeichnis innerhalb der Grails Anwendung befinden. Erste Sahne 🙂

Weiter geht es in einem anderen Post um das Setup des GWT-Moduls. Ich werde zeigen wie GIN aufgesetzt und die „Ginjectoren“ über mehrere Module hinweg organisiert werden können. Außerdem geht es um RPC und den EventBus…

Lessons learned: GWT

Ich arbeite seit knapp zwei Jahren mit GWT und habe bereits einiges ausprobiert. Meiner Meinung nach ist GWT eins der genialsten Werkzeuge, die einem Webentwickler gegeben wurden. Aber auch eins, mit dem man sich leicht verrennen kann.

Meine Lieblingskombination ist GWT zusammen mit Grails. Damit bin ich bisher ganz gut gefahren und es macht echt Spaß. Bloß, es gibt immer wieder Situationen, in denen ich mir sage: Na das hättest du aber auch leichter haben können.

Mit GWT neige ich dazu die Oberfläche mit UiBinder und Co. zu erstellen. Am besten auch noch komplett mit Layout. Es ist bequem und dann auch perfekt testbar. Das Ergebnis sind GWT Kompilate in der Größe von 1MB und mehr. Diese JavaScript Dateien wollen erstmal vom Client geladen und verarbeitet werden, was eine gewisse Zeit in Anspruch nimmt. Das neue CodeSplitting Feature erlaubt zwar mittlerweile die Kompilate zu trennen und ein Nachladen zu ermöglichen, aber weitere Probleme bleiben.

Beispiel: UI Monolithen. Wenn der Browser zunächst ein Fragment mit der Größe von 500KB und mehr laden muss, um dann den DOM aufzubauen, merkt der Benutzer das. Denn es dauert länger. Zwar kann man ihm einen kleinen Spinner zeigen, der ihn drauf hinweist, dass die Anwendung gerade etwas tut, aber gut fühlt sich das nicht an. Ziel wäre es dem Benutzer Inhalt zu zeigen, der ihn für die nächsten paar Sekunden beschäftigt, während der dynamische Code im Hintergrund allerlei anderen Kram macht.

Beispiel: Suchmaschinen. Wenn sich die GWT Anwendung in den DOM lädt, dann ist er oft vorher nackt. Den wirklich interessanten Markup Code erzeugt die GWT Anwendung und dieser beleibt für die Suchmaschine unsichtbar.

Beispiel: Dynamische Links. Rendere ich mit GWT eine dynamische Ansicht und möchte daraus auf weiteren Inhalt meiner Internetseite verweisen, muss ich GWT diesen Link irgendwie übermitteln. Da gibt es mehrere Möglichkeiten. Ich kann ein Feld im DOM verstecken und ihn dann über die ID ansprechen, oder ich übertrage die URL per RPC an den Client. Ändert sich aber meine Ansicht (kommt ein weiterer Link hinzu), dann muss zwangsweise serverseitig der Code angepasst werden, was die Entwicklung erschwert.

Beispiel: Formatierung. In der ersten Shop-Version von Snäckbox habe ich eine Klasse geschrieben, die Geldbeträge, Gewichte, Datum usw. formatiert. Der Ansatz war nicht schlecht, bloß absolut überflüssig. Denn die meisten Formatierungen mussten serverseitig ebenfalls implementiert werden, wenn beispielsweise eine Email versendet oder PDF erzeugt wurde. Doppelter Code also. Ich bin dann später dazu übergegangen nur noch formatierte Strings an den Client zu übertragen.

Beispiel: Lokalisierung. Mit GWT ist es einfach zu internationalisieren. Interface definieren, ein paar Annotations verwenden, fertig. Aber, serverseitig passiert das in den meisten Fällen auch. Vielleicht müssen sogar die selben Meldungen übersetzt werden (z.B. Fehlermeldungen). In dem Fall ist es wichtig diese Meldungen bei Änderungen synchron zu halten und bei einer wachsenden Applikation ist es irgendwann nicht möglich den Überblick darüber zu behalten.

Beispiel: Änderungen an der UI. Ändere ich die Farbe einer Linie oder die Schriftgröße, und sehe mir anschließend das Ergebnis im Browser an, muss ich unter GWT wegen einer CSS Lappalie ein paar Sekunden warten. Kommt ganz auf die Anwendungsgröße an. Bei einer Grails-Anwendung ist das Ergebnis schneller da. Auf den Tag gesehen summiert sich die Differenz. Ganz abgesehen davon, sollte das Programm auf den Entwickler warten, nicht umgekehrt.

Mein Fazit: Wenn ich die Möglichkeit habe serverseitig einige der hier beschriebenen Aufgaben zu erledigen, dann würde ich das tun. Insbesondere, was die Formatierung von Werten betrifft. Natürlich ist das von Problemstellung zu Problemstellung unterschiedlich und muss im Einzelfall entschieden werden. Aber Fakt bleibt, dass GWT all die wunderbaren Dinge ermöglicht und den Entwickler „verführt“ Code zu schreiben, der z.B. in Grails viel schneller realisiert werden kann.

Lösungsvorschlag

Ich habe vor einigen Monaten begonnen die GWT Anwendungen auf eine anderen Weise zu entwickeln, als ich es bisher getan habe. Statt große Monolithe zu bauen, die eine komplette UI erstellen, setze ich auf starke Modularisierung der GWT Anwendungen, kurze Ladezeiten und so wenig UI Code, wie möglich. Zusätzlich versuche ich die Client-Anwendung so „dumm“ zu halten, wie es geht. Sie soll weder Meldungen übersetzen, noch Geldbeträge oder Datum und Uhrzeit formatieren können.

Die Stärken von GWT versuche ich aber dennoch so gut es geht zu nutzen: Typisierung, Testbarkeit und RPC.

Ich möchte eine kleine Serie von Artikeln darüber schreiben und werde sie dann nach und nach hier verlinken. Anfangen werde ich mit der Modularisierung von GWT Anwendungen.

P.S.
Die Frage nach der Tauglichkeit in der Praxis, werde ich mit Snäckbox 2.0 beantworten, das in weniger als einem Monat online gehen wird 😉