Mit Java ins Web: Eine Spring-MVC-Anwendung im Detail, Teil 1

Seite 3: Schichtentrennung

Inhaltsverzeichnis

Um die Anwendung sauber gemäß MVC aufzuteilen, sind zunächst Packages und Klassen für den jeweiligen Zweck erforderlich. Für die Anwendung wäre folgende Gliederung sinnvoll:

  • Package CityModel mit Klasse City enthält das Datenmodell als POJO (Plain Old Java Object).
  • Package CityRepository umfasst die grundsätzlichen Methoden zum Lesen, Schreiben, Suchen und Löschen.
  • Package CityController sammelt alle Requests und delegiert diese weiter.

In der Praxis käme noch mindestens ein weiteres Package CityService dazu, dessen Klassen dem Controller sozusagen "zuarbeiten". Würde der Entwickler die gesamte Logik im Controller unterbringen, wäre dieser schnell unübersichtlich, da fast jede Anwendung einige URLs bereitstellt. Insofern ist es ratsam, den Controller schlank zu halten und möglichst alle verarbeitenden Methoden in anderen Klassen zu sammeln. Darüber hinaus bringt Spring noch zahlreiche Validatoren mit, die vor allem dazu dienen, Nutzereingaben auf Gültigkeit zu prüfen. Sie könnte man im Package CityValidator sammeln.

Die Views sind in JSP-Dateien in einem eigenen Verzeichnis untergebracht, etwa unter WEB-INF/views.

Die zentrale Ablaufsteuerung der Anwendung findet im Controller statt. Hier legt der Entwickler alle URLs sowie die weitere Logik und Verarbeitung der (eingegebenen) Informationen fest. Er benötigt im Wesentlichen vier Mappings:

  • Standard-GET-Handler für die Startansicht (Liste der Städte)
  • POST-Handler für das Anlegen einer neuen Stadt (z. B. /city/add)
  • GET-Handler für das Löschen einer Stadt aus der Datenbank (z. B. /city/delete/ID)
  • GET-Handler für die Suchfunktion (z. B. /city/search?term=CITY)

Hinweis: Eine simple GET-Methode zum Löschen eines Datensatzes darf natürlich nicht in der Praxis verwendet werden – jeder Nutzer, der die ID eines Datensatzes kennt, könnte diesen dann löschen. Löschzugriffe sollten nur angemeldeten Nutzern erlaubt sein. Über Spring Security lassen sich darüber hinaus auch Regeln für bestimmte URLs und Pfade festlegen.

Im nächsten Schritt ist festzulegen, was die einzelnen Mappings als Ergebnis liefern. Für die ersten drei Mappings ist das einfach, letztlich soll einfach die (aktualisierte) Liste der Städte in einer Tabelle erscheinen. Bei einer Suche muss das Suchergebnis in derselben Tabelle erscheinen wie die Gesamtliste, damit die Anwendung einheitlich wirkt.

Die Ergebnisse der jeweiligen Operation werden in einer Java-Collection gesammelt (List-Interface). Um diese zwischen Model und View zu übertragen, definiert Spring den Datentyp ModelAndView. Im Prinzip ist das ein Sammelobjekt, das den Namen der zugehörigen View enthält und dem sich die Unterobjekte zuordnen lassen. Die Objekte sind in der View per Expression abrufbar – ein Beispiel:

ModelAndView mv = new ModelAndView("city");
mv.addObject("message", "Hallo Welt");

In der View city wird das Objekt message wie folgt aufgerufen:

<p>Die Botschaft lautet ${message}</p>

Für die Mappings sieht das Quelltext-Gerüst der Klasse CityController so aus:

@Controller
public class CityController {
    @RequestMapping(method=RequestMethod.GET, value="/")
    public ModelAndView showCities() {
        ModelAndView cities = new ModelAndView("city");
        // TODO: lese alle Städte aus der Datenbank aus
        return cities;
    }
   
    @RequestMapping(method=RequestMethod.POST, value="/city/add")
    public String addCity(@ModelAttribute("city") City city,
           BindingResult result) {
        // TODO: lege neue Stadt anhand der Parameter des POST-Requests an
        return "redirect:/";
    }
   
    @RequestMapping(method=RequestMethod.GET, value="/city/delete/{id}")
    public String deleteCity(@PathVariable("id") int id) {
        // TODO: lösche einen Datensatz anhand der übergebenen ID
        return "redirect:/";
    }
   
    @RequestMapping(method=RequestMethod.GET, value="/city/search")
    public ModelAndView getSearchResult(@RequestParam("term") String term) {       
        ModelAndView cities = new ModelAndView("city");
        // TODO: verarbeite die Suche nach dem Begriff "term"
        // und ermittle das Ergebnis
        return cities;
    }
}

Die Methoden zum Hinzufügen und Löschen von Städten führen also nach der Verarbeitung einen simplen Redirect auf die Startseite (= Ansicht aller Städte) aus.

Erwähnenswert ist noch die Unterscheidung zwischen @PathVariable und @RequestParam: Ersteres steht für einen dynamischen Teil der URL wie die ID des zu löschenden Datensatzes. Letzteres ist ein benannter Parameter, hier der Suchbegriff mit dem Bezeichner term. Eine Beispiel-URL für die Suche wäre etwa: http://localhost:8080/cities/city/search/?term=Berlin.

Zur Darstellung benötigt der Programmierer nur eine einzige View, die er unter src/main/webapp/WEB-INF/views/city.jsp ablegt. Die übergebenen Variablen und Objekte werden wie beschrieben über Expressions ausgelesen. Interessant ist noch die Ausgabe der Tabelle mit den Städtedaten, was sich leicht über eine forEach-Schleife aus der JSTL-Bibliothek (JavaServer Pages Standard Tag Library) erledigen lässt:

<table>
    <tbody>
        <c:forEach items="${cities}" var="currentcity">
            <tr>
                <td>${currentcity.name}</td>
                <td>${currentcity.country}</td>
                <td>${currentcity.district}</td>
                <td>${currentcity.population}</td>
                <td><a href="/cities/city/delete/${currentcity.id}"
                     class="deleteLink">Löschen</a></td>
            </tr>
        </c:forEach>
    </tbody>
</table>

So lässt sich in JSP recht einfach über eine Collection iterieren. Zur sauberen Unterstützung von JSTL ist die Maven-Konfiguration (im Stammverzeichnis unter pom.xml) um eine Abhängigkeit zu ergänzen

<dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.1.2</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>