Mit Java ins Web: Eine Spring-MVC-Anwendung im Detail, Teil 2
Mit Spring MVC lassen sich in Kombination mit JavaScript dynamische und REST-konforme Webapplikationen mit Java umsetzen.
- Jan Petzold
Mit Spring MVC lassen sich in Kombination mit JavaScript dynamische und REST-konforme Webapplikationen mit Java umsetzen. Nach dem Erstellen eines rudimentären Grundgerüsts in einem ersten Artikel geht es nun um das Ergänzen zeitgemäßer und sinnvoller Funktionen und eine ansprechende Darstellung.
Im ersten Teil des Tutorials wurde eine rudimentäre Anwendung auf Basis von Spring 3 MVC erstellt. Der Entwickler konnte Städtedaten aus einer Datenbank auslesen sowie Städte löschen, hinzufügen und nach ihnen suchen. Um das Ganze nun etwas benutzerfreundlicher zu gestalten, kommen JavaScript und AJAX ins Spiel. Die Ausgabe der Daten in der Tabelle wird verbessert und neue Funktionen wie eine Auto-Vervollständigung sowie eine Fehlerkorrektur in der Suche werden ergänzt.
AJAX und elegante Tabellen mit JavaScript
Spring bietet mit Spring JavaScript ein eigenes Modul fĂĽr dynamische Komponenten beziehungsweise die Integration von AJAX an. Das Modul basiert auf dem Dojo Toolkit. Die gewĂĽnschten Funktionen werden direkt im DOM unterhalb des entsprechenden HTML-Elements gesetzt. Sauberer dĂĽrfte die Auslagerung von JavaScript in eine eigene Datei sein. Weiterhin verwendet der Autor im Beispiel jQuery als JavaScript-Framework. Damit verzichtet er allerdings auf eine direkte Integration mit Spring.
Bisher wurde im Projekt nur eine schlichte HTML-Tabelle erzeugt. Diese soll nun mit dem DataTables-Plug-in fĂĽr jQuery erweitert werden. DataTables kommt vor allem bei der Gestaltung und funktionalen Erweiterung von HTML-Tabellen zum Einsatz. Es bietet Funktionen wie eine Suche, automatische Paginierung, Sortierung und Filterung. DataTables basiert auf der Komponentenbibliothek jQuery UI.
Der Einsatz von DataTables ist einfach – nach dem Laden des Dokuments wird die Tabelle anhand ihrer ID (im Beispiel "myTable") im JavaScript als Zielelement gesetzt:
$("#myTable").dataTable({
   "bJQueryUI" : true,
   "bInfo" : false,
   "bAutoWidth" : false,
   "bLengthChange" : false,
   "bFilter" : false,
   "bPaginate" : true,
   "bSort" : false,
   aoColumns" :
      [ { sWidth : '250px'}, {
      sWidth : '120px'}, {
      sWidth : '175px'}, {
      sWidth : '100px'}, {
      sWidth : '65px'} ]
});
Der Aufruf ist komplizierter, als er sein mĂĽsste, zeigt aber die Konfiguration einiger Funktionen. Das Unterobjekt aoColumns ist optional, erlaubt jedoch die genaue Einstellung der Breite jeder Spalte.
Als Ausgangspunkt für die Darstellung der Tabelle bietet sich eines der Themes des jQuery UI Themeroller an. Das Beispiel verwendet das Theme "Smoothness", das sich aber jederzeit austauschen lässt.
Einbau der Auto-Vervollständigung
Die Kernfunktionen der Anwendung funktionieren, aber die Suche könnte noch benutzerfreundlicher sein. Inzwischen ist es üblich, nach der Eingabe einiger Zeichen Suchvorschläge zu erhalten. Dazu gibt es zum Beispiel das jQuery-UI-Modul Autocomplete. Es lässt sich auf jedes HTML-Eingabefeld anwenden, und dann ergänzt man es um eine Liste der Suchbegriffe. Interessant ist dabei die Datenquelle – eine automatische Vervollständigung benötigt einen gewissen Bestand an Wörtern, die für Suchvorschläge in Frage kommen.
Das Autocomplete-Modul nutzt hierfür das JSON-Format, der Wörterbestand muss also in einem JSON-Objekt vorliegen. Wenn es nur um ein paar Wörter geht, könnte man diese einfach in das JavaScript schreiben, aber für das Szenario mit etwa 4000 Städten ist das nicht praktikabel. Sinnvoller ist der Einsatz von AJAX: Sobald der Nutzer ein paar Zeichen eingegeben hat, geht ein Request an den Server, der die passenden Ergebnisse im JSON-Format zurückliefert. Mit jedem eingegebenen Buchstaben wird das genauer.
Hier der JavaScript-Quelltext fĂĽr das HinzufĂĽgen von Autocomplete zum Eingabefeld mit der ID term:
$("#term").autocomplete({
source: function(request, response) {
         $.ajax({
           url: "/cities/city/search",
           data: request,
           dataType: "json",
           type: "POST",
           success: function(data){
               response(data);
           }
         });
       },
   minLength : 2
});
Ab einer Eingabe von zwei Zeichen (Parameter minLength) setzt das Programm einen POST-Request auf die URL /cities/city/search. Im Controller wurde vorher bereits eine Methode für die URL definiert, die aber nur für GET-Requests greift, insofern kann man das einfach um eine POST-Methode erweitern. Die neue Methode liefert eine Liste der Städte zurück, die im CityRepository-Interface wie folgt definiert ist::
public interface CityRepository extends JpaRepository<City, Long> {
   @Query("select name from City where Name
       like CONCAT(:currentName, '%') order by ABS(Population) desc")
   List<String> searchCityNames(@Param("currentName") String name);
}
Um das Ergebnis in das JSON-Format zu wandeln, lässt sich die Bibliothek Gson einsetzen, die zunächst in der Maven-Konfiguration zu ergänzen ist:
<dependency>
   <groupId>com.google.code.gson</groupId>
   <artifactId>gson</artifactId>
   <version>1.7.1</version>
</dependency>
Leider gibt es ein Problem mit der Zeichenkodierung – der Server liefert die Ajax-Response immer ISO-kodiert zurück, obwohl alle Einstellungen auf UTF-8 gesetzt sind. Das lässt sich nur über eine Manipulation der Header korrigieren:
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add("Content-Type", "text/html; charset=utf-8");
// ermittle die passenden Städtenamen und speichere sie im String "json"
...
return new ResponseEntity<String>(json, responseHeaders,
   HttpStatus.CREATED);
Nun werden alle Städtenamen korrekt kodiert – im Detail ist das Problem bei Stack Overflow beschrieben.
Man sollte sich immer bewusst sein, dass die Verwendung von Autocomplete recht ressourcenintensiv ist – bei jedem eingegebenen Zeichen wird eine Datenbankabfrage gestartet. Der Entwickler sollte daher in jedem Fall in der Datenbank einen Index auf die betroffenen Tabellenspalten setzen:
CREATE INDEX citynames ON city(Name);
Für eine Datenbank mit 4000 Einträgen ist der Unterschied mess-, aber nicht spürbar. Auf dem Testrechner reduzierte sich die Query-Dauer von 0,015 auf 0,0004 Sekunden. Deutlicher wird es, beim Einsatz einer größeren Datenbank wie der Maxmind-Datenbank mit 2,8 Millionen (unbereinigten) Datensätzen. Hier dauerte eine ähnliche Abfrage zunächst 2,234 Sekunden, mit Index reduzierte sich das auf 0,093 Sekunden.
Als weitere Komfortfunktion soll die Suche noch um eine Fehlerkorrektur ergänzt werden – auch das kennt man etwa von Suchmaschinen (Google Instant) oder Online-Shops ("Meinten Sie ..."). Schnell hat sich der Nutzer vertippt, umso besser, wenn die Anwendung automatisch eine Korrektur anbietet.