Verweise

Spring 3.0 Doku
Spring 3.0 API
Freemarker Handbuch
Buch "Spring 3"

Startseite

Web Anwendungen mit Spring und Freemarker

Wenn Sie dieses Tutorial lesen, sollten Sie sich bereits ein bischen mit klassischen Servlets auskennen. Es richtet sich an Programmierer, die größere Web-Applikationen schreiben und daher den Einsatz von Spring in Kombination mit Freemarker erwägen. Bitte berücksichtigen Sie, daß ich auf dieser Seite nur die Grundlagen beschreiben kann. Alles Weitere sollte sich aus den Online Dokumentationen der beiden Produkte ergeben.

Was ist Freemarker?

Freemarker ist eine Template Engine. Template Engines erzeugen Text Dokumente (z.B. Webseiten in HTML), indem Sie Vorlagen mit den Werten aus Variablen ausfüllen.

    Sie haben sich mit dem Namen ${username} angemeldet.

Freemarker unterstützt das Erstellen von Web Applikationen nach dem Modell 2 (auch MVC Modell genannt), indem es sich um die Darstellungsschicht kümmert. Sie als Programmierer stellen lediglich einen Satz von darzustellenden Daten bereit, die sie an Freemarker übergeben. Ein Web-Designer erstellt die Vorlage, und bestimmt damit, wie die Daten dargestellt werden sollen. Freemarker verknüpft Vorlagen und Daten miteinander, und erzeugt so die Webseiten, die an den Benutzer ausgeliefert werden.

Was ist Spring?

Spring steuert die Abläufe in Ihrem Programm, so daß die einzelnen Komponenten in der richtigen Reihenfolge geladen und aufgerufen werden.

Große Web-Anwendungen enthalten typischerweise immer wieder sehr ähnlichen Programmcode für

Spring kombiniert all diese Funktionen in ein Gesamtpaket.

Wie wirkt sich Spring auf die Performance aus?

Spring wirkt sich nur unwesentlich auf die Performance aus, darüber braucht man sich keine Gedanken zu machen. Interessanter ist ein Blick auf die Template-Engine. Das hier vorgestellte Freemarker eignet sich gut zum Erzeugen von Webseiten. Allerdings würde ich lange Listen von mehreren Megabytes doch lieber "zu fuß" ohne Template Engine erzeugen.

Was brauche ich mindestens?

Um eine kleine Spring Anwendung zu entwickeln, brauchen Sie mindestens

Laden Sie sich SpringTest.zip herunter. Das ist ein Netbeans Projekt, es kann aber auch in allen gängigen IDE's importiert werden. Die folgenden Erklärungen beziehen sich auf dieses Beispiel-Projekt.

Spring nutzt die commons-logging Library, während die Hibernate Validatoren slf4j nutzen. Das ist ein bischen doof, aber beide Logging Libraries leiten alle Meldungen standardmäßig an java.util.logging weiter, so daß es am Ende genügt, java.util.logging zu konfigurieren. Dies wiederum erledigt der Servlet Container (Tomcat).

Konfiguration

Servlets und Filter

Jede Web Anwendung hat als erste Konfigurationsdatei die WEB-INF/web.xml. In unserer Beispielanwendung sieht sie so aus:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
    xmlns="http://java.sun.com/xml/ns/javaee"
    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">

    <servlet>
        <servlet-name>test1</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>test1</servlet-name>
        <url-pattern>/test1/*</url-pattern>
    </servlet-mapping>



    <servlet>
        <servlet-name>test2</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <load-on-startup>2</load-on-startup>
        </servlet>

    <servlet-mapping>
        <servlet-name>test2</servlet-name>
        <url-pattern>/test2/*</url-pattern>
    </servlet-mapping>



    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>


    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>
            org.springframework.web.filter.CharacterEncodingFilter
        </filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>

Unsere Beispiel Applikation besteht aus zwei Servlets und einem Filter. Die beiden Servlets stellen zwei Spring Kontexte dar, die unabhängig voneinander konfiguriert werden können. Eine typische Spring Applikation hat nur einen Kontext, ich möchte aber auch zeigen, dass und wie man mehrere Kontexte haben kann.

Der Filter ändert bei allen Requests das Character Encoding auf UTF-8. Unsere Beispielanwendung benötigt diesen Filter, um deutsche Umlaute in Formular-Feldern korrekt zu codieren. Ohne den Filter geht die Anwendung nämlich davon aus, daß der Web-Browser alle Benutzereingaben im ISO-8859-1 Zeichensatz codiert, was aber nur der Fall wäre, wenn die Webseiten ebenfalls diesen Zeichensdatz verwenden würden. Die Beispielanwendung ist aber komplett auf UTF-8 ausgelegt. Wer schonmal Servlets programmiert hat, dürfe diese Problematik bereits kennen.

Die beiden Servlets sind jeweils einem Pfad zugeordnet und werden vom Spring DispatcherServlet bedient. Dieses Servlet findet heraus, welche Controller-Klasse zur angeforderdeten URL gehört und ruft diese dann auf. Wenn der Controller seine Arbeit beendet hat, wird dessen Ergebnis (das sogenannte Model Objekt) an Freemarker übergeben, um eine Webseite zu erzeugen.

Kontext

Da wir zwei Dispatcher Servlets haben, haben wir auch zwei Kontext-Konfigurationsdateien. Fangen wir mit der einfacheren Datei an, das ist die WEB-INF/test2-servlet.xml. Der Dateiname setzt sich aus dem Namen des Servlets und der Endung "-servlet.xml" zusammen:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">


    <context:component-scan base-package="de.example.package2"/>


    <bean id="freemarkerConfig" class=
      "org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
        <property name="templateLoaderPath" value="/WEB-INF/templates/test2/"/>
    </bean>


    <bean id="viewResolver" class=
      "org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
        <property name="cache" value="false"/>
        <property name="contentType" value="text/html;charset=UTF-8"/>
        <property name="suffix" value=".ftl"/>
        <property name="exposeSpringMacroHelpers" value="true"/>
    </bean>


</beans>

Mit "context:component-scan" gibt man an, in welchem Package die Controller Klassen liegen, die diesem Kontext zugeordnet sind.

Das Bean "freemarkerConfig" konfiguriert Freemarker (wer hätte das gedacht). In diesem Fall geben wir den Pfad an, wo die Freemarker Templates liegen.

Das Bean "viewResolver" ordnet die Ergebnisse der Controller-Klassen den Template-Dateien zu. Wenn ein Controller festlegt, daß "Hallo" angezeigt werden soll, dann ergibt sich nach den obigen Einstellungen der Dateiename "Hallo.ftl". Sie könnten an dieser Stelle später auf eine andere Template Engine umsteigen (z.B. Velocity), ohne die Controller Klassen anzufassen.

Beim viewResolver wird außerdem noch festgelegt, daß der Web-Browser nicht cachen darf und daß es sich um text/html Dokumente in UTF-8 Codierung handelt. Bei der Ausgabe der erzeugten HTML Seiten werden entsprechende HTTP Header gesetzt.

Die Einstellung "exposeSpringMacroHelpers" macht für Freemarker eine ObjektVariable mit Namen "springMacroRequestContext" verfügbar, die unter anderem von den Freemarker Markos in spring.ftl verwendet werden. Hauptsächlich geht es darum, den Freemarker Templates Zugriff auf Meldungstexte zu verschaffen. Dazu später mehr.

Zusammenfassung: Der Kontext "test2" verwendet die Klassen aus dem Paket de.example.package2, die zugehörigen Freemarker Templates liegen im Verzeichnis /WEB-INF/templates/test2/ und es handelt sich dabei um text/html Dokumente in UTF-8 codierung.

Auf den Kontext "test1" gehe ich später ein.

Model - View - Controller

Controller

Einkommende Requests werden von Controller Klassen bearbeitet. Der Kontext "test2" hat nur eine Controller Klasse mit dem Namen de.example.package2.WelcomeController.

package de.example.package2;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/willkommen")
public class WelcomeController {

    @RequestMapping
    public String control() {
        return "welcome";
    }

}

Alle Controller Klassen sind mit der Annotation @Controller markiert. Nur so markierte Klassen werden vom DispatcherServlet gesucht und gefunden. Mit @RequestMapping bestimmt man, zu welchem URL-Pfad der Controller zugewiesen werden soll. In diesem Fall lautet die vollständige URL des Controllers: http://localhost:8080/SpringTest/test2/willkommen.

Dieser Controller hat eine Methode, die Requests beantworten soll. Solche Methoden müssen mit @RequestMapping gekennzeichnet werden. Der Name der Methode ist völlig beliebig. Wenn man hinter @RequestMapping einen Pfad angäbe, dann würde er an den Pfad der Controller-Klasse angehängt werden.

Warscheinlich ahnen sie schon, daß eine Controller Klasse beliebig viele Methoden haben kann, die mit @RequestMapping annotiert sind. Dann müssen sie natürlich irgendwie unterschieden werden können, z.B. durch unterschiedliche URL-Pfade.

@Requestmapping kann Controller Klassen und Methoden nicht nur an URL-Pfade binden, sondern auch an URL-Parameter, HTTP-Methoden und HTTP-Header. In dem Kontext "test1" wird davon mehrmals Gebrauch gemacht.

Wenn also die URL http://localhost:8080/SpringTest/test2/willkommen aufgerufen wird, dann wird die Methode control() aus der Controller Klasse WelcomeController aufgerufen.

View

Diese Methode control() gibt den Namen einer View zurück, nämlich "welcome". In der Kontext-Konfiguration test2-servlet.xml haben wir bereits festgelegt, daß Views durch Freemarker Templates realisisiert werden, indem wir an den Namen der View die Endung ".ftl" anhängen und wirk haben auch festgelegt, daß die Templates im Verzeichnis /WEB-INF/templates/test2/ liegen. Es wird also das Template /WEB-INF/templates/test2/welcome.ftl geladen aund zur Anzeige gebracht.

Nun ist dieses Template recht simpel aufgebaut, es enthält nämlich nur eine Variable, und das ist der Name der Template-Datei selbst. Diese Variable wird von Freemarker automatisch bereit gestellt.

Bildschirmfoto der View welcome

Model

Das Model ist ein Paket von Variablen, welche von Ihrem Java Programm an Freemarker übergeben werden, um damit Template Dateien auszufüllen. Im obigen Quelltext sehen Sie kein Model, weil das Template welcome.ftl keins erfordert. Nun zeige ich Ihnen eine Methode, die ein Model zurück liefert. Es handelt sich um die Klasse de.example.package1.TemplateController im Kontext "test1":

package de.example.package1;

import java.util.HashMap;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/template")
public class TemplateController {

    @RequestMapping
    public ModelAndView control() {
        HashMap<String,String> variables=new HashMap<String,String>();
        variables.put("farbe", "rot");
        variables.put("wetter","windig");
        return new ModelAndView("info1",variables); 
    }

}

Dieser Controller liefert den Namen der View "info1" und eine Map mit zwei Variablen zurück, nämlich

Zur Ausgabe lädt Freemarker also das Template /WEB-INF/templates/test/info1.ftl und fülltes mit diesen beiden Werten aus:

<html>
  <head>
    <title>Spring Test</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  </head>
  <body>
    Das ist ein Freemarker Template.
    <p>
    Farbe: ${farbe}
    <p>
    Wetter: ${wetter}
  </body>
</html>

Im Web-Browser sieht die erzeugte Webseite dann so aus:

Bildschirmfoto der View info1

Funktionen für Web Applikationen

Im Package 1 finden Sie eine Reihe Controller Klassen, mit denen ich einzelne Funktionen von Spring vorführen und erklären möchte.

Ausgabe ohne View

Der Controller de.example.package1.WelcomeController demonstriert, wie man Zeichenketten ohne Verwendung eines View ausgeben kann, also ohne Freemarker und ohne Template. Gerade für Testzwecke kann das mal nützlich sein.

package de.example.package1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/willkommen")
public class WelcomeController {

    @RequestMapping
    @ResponseBody
    public String control() {
        return "Hallo, herzlich willkommen";
    }

}

Genau wie die Klasse de.example.package2.WelcomeController gibt die control() Methode auch hier nur einen String zurück. Dieses mal ist es aber nicht der Name eines View. Entscheident ist hier die Annotation @RequestBody. Sie teilt Struts mit, daß die Ausgabe so wie sie ist an den Web Browser ausgeliefert werden soll, also quasi an Freemarker vorbei geschleust. Wir geben hier effektiv also auch kein HTML Dokument aus, sondern einfach nur eine Zeichenkette.

Weil diese Art der Ausgabe so schön einfach ist, habe ich in den folgenden Beispielen davon rege Gebrauch gemacht. In richtigen Web Anwendungen würde ich allerdings immer mit Templates (also mit Views) arbeiten.

Im Web-Browser sieht das so aus:

Bildschirmfoto des WelcomeControllers

HTTP Header auslesen

Die Controller Klasse de.example.package1.UserAgentController demonstriert, wie man HTTP Header auslesen kann.

package de.example.package1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/userAgent")
public class UserAgentController {

    @RequestMapping
    @ResponseBody
    public String control(@RequestHeader("user-agent") String userAgent) {
        return "Dein User-Agent ist: "+userAgent;
    }

}

Durch die Annotation @RequestHeader("user-agent") weisen Sie Spring an, den dahinter stehenden Parameter mit dem Wert des HTTP Headers user-agent zu füllen. Bei den Namen von HTTP-Header spielt groß-/klein-Schreibung ausnahmsweise keine Rolle, das ist in der Servlet API so spezifiziert.

Dieser Header enthält Informationen über den Aufrufenden Web-Browser. Mann verwendet diese Informationen gerne, um Browser-spezifische Eigenheiten in Templates zu berücksichtigen. Web-Designer müssen z.B. den Internet Explorer 6 besonders behandeln.

Die Ausgabe im Browser sieht so aus:

Bildschirmfoto des UserAgentControllers

Request Parameter auslesen

Request Parameter aus der URL oder aus Formularfeldern liest man fast auf die gleiche Weise aus, wie HTTP Header. Lediglich die Annotaton heisst anders, nämlich @RequestMapping. Der Controller de.example.package1.NameController zeigt das:

package de.example.package1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/name")
public class NameController {

    @RequestMapping
    @ResponseBody
    public String control(
           @RequestParam(value="name", required=false) String name) {
        return "Dein Name (aus URL Parameter) ist: "+name;
    }

}

Die @RequestMapping Annotation verlangt normalerweise, daß der Parameter auch vorhanden ist, sonst bekommt man eine Fehlerseite angezeigt. Im obigen Code wird zusätzlich zum Namen des Parameters auch angegeben, daß er nicht unbedingt erforderlich ist. Die Seite kann also auch ohne diesen Parameter aufgerufen werden, dann hat er den Wert null.

Anstelle von @RequestParam(value="name", required=true) könnte man auch @RequestParam("name") schreiben, denn required="true" ist der Vorgabewert.

Das Binden von Request Parametern an Aufrufparameter der Methoden nennt Spring übrigens "Binding". Sie werden in der Dokumentation von Spring einige Absätze finden, die erklären, wie man den Vorgang des Bindens beeinflussen kann.

Die Ausgabe im Web-Browser sieht so aus:

Bildschirmfoto des NameControllers

Typ-Konvertierung

Beim Binden von Parametern kann Spring den Typ des Wertes automatisch konvertieren. Im HTTP Protokoll sind alle Parameter naturgemäß Strings. Wenn Sie eine Parameter-Variable allerdings als Integer deklarieren, dann konvertiert Spring den String in einen Integer. Es würde auch mit vielen anderen Datentypen ebenso funktionieren, beispielsweise Integer, Long, FLoat, Double, URL.

Sie können in der Spring Dokumentation im Kapitel über den PropertyEditor nachlesen, welche weiteren Datentypen sonst noch unterstützt werden.

Die Controller Klasse de.example.package1.NumberController zeigt, wie es geht:

package de.example.package1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/zahl")
public class NumberController {

    @RequestMapping
    @ResponseBody
    public String withNumber(@RequestParam("number") Integer number) {
        return "Die Zahl ist: "+number;
    }

    @RequestMapping(params="!number")
    @ResponseBody
    public String withoutNumber() {
        return "Es wurde keine Zahl angegeben";
    }

}

Im Web-Browser sieht es so aus:

Bildschirmfoto des NumberControllers

Dieses Beispiel zeigt außerdem noch etwas ganz anderes, nämlich daß man Methoden mittels @RequestMapping Annotation nicht nur an Pfade, sondern auch an Bedingungen knüpfen kann. In diesem Fall wird die Methode withoutNumber() nur dann aufgerufen, wenn der Request-Parameter "number" fehlt, also null ist. Wenn der Parameter vorhanden ist, wird die Methode withNumber() aufgerufen.

Wie an vielen anderen Stellen auch, gilt bei Spring hier die Regel, daß die spezifischere Regel vor der weniger spezifischen bevorzugt wird. In diesem Fall hat die untere @RequestMapping Annotation die spezifischere Regel, die obere hat nämlich keine Regel.

URL-Pfade als Parameter

Die meisten Suchmaschinen (z.B. Google) funktionieren inzwischen so, daß sie URL Parameter nicht in ihren Index aufnehmen. Angenommen, Sie haben einen Shop mit vielen Produkten, und diese könnte man über die URL's

aufrufen, dann könnte man Ihre Produkte könnte man in Google nicht wieder finden, sondern nur die Startseite.

Um die Produkte für Suchmaschinen zugänglich zu machen, müssen deren Artikelnummern Bestandteil der URL sein, so daß jedes Produkt seine eigene URL hat. Zum Beispiel:

Idealerweise ist auch der Name des Artikels in der URL enthalten. Das ist zwar technisch unrelevant, da die Artikelnummer genügt, aber Suchmaschinen bevorzugen soche URL's.

Spring hat dafür eine Lösung parat, und die wird in der Controller Klasse de.example.package1.AnimalController angewendet:

package de.example.package1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/tier")
public class AnimalController {

    @RequestMapping("/{animal}")
    @ResponseBody
    public String withAnimal(@PathVariable("animal") String animal) {
        return "Das Tier ist ein "+animal;
    }

    @RequestMapping
    @ResponseBody
    public String withoutAnimal() {
        return "Es wurde kein Tier angegeben";
    }

}

Die Methode withAnimal() wird nur dann aufgerufen, wenn hinter /tier ein weiterer Pfad kommt. Und dieser wird mittels @PathVariable Annotation der Methode als Parameter übergeben.

Die Methode withoutAnimal() wird aufgerufen, wenn kein Tier angegeben wurde.

Im Web-Browser sieht das so aus:

Bildschirmfoto des AnimalControllers

Internationalisierung

Wenn Sie eine mehrsprachige Web-Anwendung schreiben, legen Sie von jedem Template für jede Sprache eine eigene Version an. In der Beispielanwendung wird dies beim Template WEB-INF/templates/test/language.ftl gemacht:

Wenn der Web-Browser auf deutsch eingestellt ist, wird Freemarker das deutsche Template laden. Wenn der Web-Browser auf englisch eingestellt ist, wird Freemarker das englische Template laden. Und bei allen anderen Sprachen, für die es kein eigenes Template gibt, wird language.ftl verwendet.

Damit das Funktioniert, benötigen Sie einen Eintrag in der Konfigurationsdatei des Kontextes, also in diesem Fall die in der Datei WEB-INF/test1-servlet.xml:

    <mvc:interceptors>
        <bean class=
        "org.springframework.web.servlet.i18n.LocaleChangeInterceptor" />
    </mvc:interceptors>

Im Quelltext ihrer Controller Klassen brauchen Sie nichts zu ändern.

Zusätzlich zur automatischen Erkennung der Sprache, können Sie dem Benutzer auch ein paar Buttons oder Links anbieten, mit denen er die Sprache selbst umstellen kann. Praktisch jede mehrsprachige Website hat solche Buttons.

Auch dazu brauchen Sie keine einzige Zeile Java Code zu programmieren. Rufen Sie einfach irgendeine Seite der Web-Anwendung mit dem Parameter "locale=en" auf, so wird die Seite in englisch angezeigt - vorausgesetzt es gibt auch ein englisches Template.

Darüber hinaus ist es sinnvoll, diese Spracheinstellung in der Session zu speichern, damit auch alle Folgeseiten mit der gerade gewählten Sprache angezeigt werden ohne daß jedesmal dieser URL Parameter nötig ist. Dazu fügen Sie die folgende Zeile in die Kontext-Konfiguration ein:

    <bean id="localeResolver"
        class="org.springframework.web.servlet.i18n.SessionLocaleResolver"/>

Im Web-Browser sieht die URL zur Umschaltung so aus:

Bildschirmfoto der View language

Exceptions abfangen

Im Fall von Fehlern zeigen Java Programme typischerweise Exceptions an. Für den Programmierer ist das eine feine Sache, aber für den Besucher der Webseite sind Exceptions eher irritierend. Also fängt man alle Exceptions irgendwo ab, schreibt sie in Protokolldateien und zeigt dem Benutzer anständige (meist weniger detaillierte) Fehlermeldungen an.

Der Controller de.example.package1.OopsController löst absichtlich immer eine Exception aus, um dies demonstrieren:

package de.example.package1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/oops")
public class OopsController {

    @RequestMapping
    @ResponseBody
    public String withoutAnimal() throws Exception {
        throw new Exception("The OopsController has been called");
    }

}

Bildschirmfoto des OopsControllers

Zweifellos wollen Sie Ihren Kunden solche Fehlermeldungen nicht zumuten. Also fangen Sie sie ab.

Exceptions von Controllern und Interceptioren

Exception von Controller und von Interceptoren fängt man mit einem ExceptionResolver ab, der als Bean mit der ID "exceptionResolver" in die Kontext-Konfigurationsdatei eingetragen werden muss:

    <bean id="exceptionResolver" class="de.example.MyExceptionResolver"/>

Der Quelltext dieser Klasse sieht so aus:

package de.example;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

public class MyExceptionResolver implements HandlerExceptionResolver {

    public ModelAndView resolveException(HttpServletRequest request,
           HttpServletResponse response, Object arg2, Exception exception) {
        exception.printStackTrace();
        return new ModelAndView("oops","exception",exception);
    }

}

Mit exception.printStackTrace() wird der Strack-Trace in eine Protokolldatei geschrieben. Tomcat schreibt dies in die Datei logs/localhost-<Datum>.log. Dann wird das Template oops.ftl, geladen welches eine Fehlermeldung anzeigt. In diesem Fall zeigen wir die Exception weniger detailliert an. Wir könnten noch extremer vorgehen, und etwa "Leider funktionierte diese Seite nicht" anzeigen, ohne technische Details zu nennen.

Dies ist das Template WEB-INF/templates/test/oops.ftl:

<html>
  <head>
    <title>Spring Test</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  </head>
  <body>
    Ooops, da ist etwas schief gelaufen:<br>
    ${exception}
  </body>
</html>

Im Web-Browser sieht die Fehlerseite so aus:

Bildschirmfoto der View oops

Exceptions in Interceptoren werden ebenso abgefangen. Die Beispiel-Applikation enthält einen Interceptor, der sich mit einer Exception beschwert, wenn die URL den Parameter "scheisse" enthält. Ich erkläre diesen Interceptor weiter unten.

Exceptions von Freemarker Templates

Wenn in einem Freemarker Template ein Fehler erkannt wird, dann gibt Freemarker eine detaillierte Fehlermeldung an der Stelle aus, wo der Fehler vorkommt. So kann es dann passieren, daß mitten in einer Webseite eine Fehlermeldung steht.

Bildschirmfoto eiens fehlerhaften Templates

Hässlicher geht es kaum. Wer dem Kunden solche Webseiten nicth zumuten möchte, fängt diese Meldungen durch einen TemplateExeptionHandler ab, indem er Freemarker in der Kontext-Konfigurationsdatei WEB-INF/test1-servlet.xml entsprechend konfiguriert:

    <bean id="freemarkerConfig" class=
     "org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
        <property name="freemarkerSettings">
            <props>
                <prop key="template_exception_handler">
                    de.example.MyTemplateExceptionHandler
                </prop>
            </props>
        </property>
    </bean>

Der Quelltext dieser Klasse sieht so aus:

package de.example;

import freemarker.core.Environment;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import java.io.IOException;
import java.io.Writer;

public class MyTemplateExceptionHandler implements TemplateExceptionHandler {

    public void handleTemplateException(TemplateException exception,
           Environment env, Writer out) throws TemplateException {
        try {
            out.write("[Error]");
        } catch (IOException ex) {
            throw new TemplateException("Cannot write error message",env);
        }
    }

}

Immer, wenn in einem Template eine fehlerhafte Stelle gefunden wird, geben wir anstelle der Fehlermeldung nun einfach "[Error]" aus. Wir könnten auch einfach gar nichts ausgeben. Unabhängig davon kümmert sich Freemarker bereits darum, daß die Exception in einer Protokolldatei geloggt wird.

Im Web-Browser sieht das dann so aus:

Bildschirmfoto eines fehlerhaften Templates mit eigenem TemplateExceptionHandler

Der Text vor "[Error]" ist der reguläre Inhalt der Webseite, und die vorherige Fehlermeldung wurde durch "[Error]" ersetzt.

Formulare

Formulare spielen eine besondere Rolle, weil im Fall von Eingabefehlern das Formular erneut erscheinen soll, aber mit eingebetteter Fehlermeldung, sowie den zuvor eingegebenen Daten.

Anstatt die einzelnen Request Parameter in ein Model (eine HashMap) zu kopieren, können Sie die @ModelAttribute Annotation benutzen, die genau dies erledigt.

Die Beispiel Applikation verwendet folgendes Formular:

Bildschirmfoto der Eingabeformulars für eine Zahl, einen Namen und eine Farbe

Die Template Datei dazu heisst WEB-INF/templates/test/formInput.ftl:

<html>
  <head>
    <title>Spring Test</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  </head>
  <body>
      <font color="red"><b>${errorMsg!}</b></font>
      <form method="post">
          Du hast um ${start?time} begonnen, das Formular auszufüllen.
          <p>
          Gib mal eine Zahl ein, die kleiner als 100 ist:<br>

          <#-- Fehlermeldung, falls number ungültig ist -->
          <#assign status=
              springMacroRequestContext.getBindStatus("data.number")>
          <#if status.isError()>
              <font color="red">
                  ${status.errorMessage} (Code: ${status.errorCode})
              </font><br>
          </#if>

          <input type="text" name="number" value="${(data.number?c)!}">
          <p>
          Gib jetzt noch Deinen Namen ein:<br>

          <#-- Fehlermeldung, falls name ungültig ist -->
          <#assign status=
              springMacroRequestContext.getBindStatus("data.name")>
          <#if status.isError()>
              <font color="red">
                  ${status.errorMessage} (Code: ${status.errorCode})
              </font><br>
          </#if>


          <input type="text" name="name" value="${data.name!}">
          <p>
          Wähle eine Farbe:<br>

          <#-- Fehlermeldung, falls farbe ungültig ist -->
          <#assign status=
              springMacroRequestContext.getBindStatus("data.farbe")>
          <#if status.isError()>
              <font color="red">
                  ${status.errorMessage} (Code: ${status.errorCode})
              </font><br>
          </#if>

          <select name="farbe" size="4">
              <option <
                  #if "grütze"= (data.farbe)!>selected</#if> >grütze</option>
              <option <
                  #if "rot" = (data.farbe)!>selected</#if> >rot</option>
              <option <
                  #if "gelb"= (data.farbe)!>selected</#if> >gelb</option>
              <option <
                  #if "grün"= (data.farbe)!>selected</#if> >grün</option>
          </select>
          <p>
          <input type="submit">
      </form>
  </body>
</html>

Die Eingaben des Formulars müssen in einer Bean gespeichert werden. Struts nennt diese Beans "Commands". Im einfachsten Fall sieht sie so aus:

package de.example.package1;

    public class FormData {
        private Integer number;
        private String name;
        private String farbe;

        @Override
        public String toString() {
            return number+","+name+","+farbe;
        }

        public void setNumber(Integer number) {
            this.number=number;
        }

        public void setName(String name) {
            this.name=name;
        }

        public void setFarbe(String farbe) throws Exception {
                this.farbe=farbe;
        }

        public Integer getNumber() {
            return number;
        }

        public String getName() {
            return name;
        }

        public String getFarbe() {
            return farbe;
        }
    }

Ein Command Objekt dient schlicht dazu, die Eingaben aus einem Formular zu speichern und sie ggf. auch zur Ausgabe (also für die View) zu verwenden. Es spielt auch bei der Validierung eine wichtige Rolle, auf die ich weiter unten eingehe. Momentan brauchen Sie nur zu wissen, daß ein Command Objekt für jedes Formularfeld einen Setter und einen Getter braucht, deren Name zum Formularfeld passt. Die Methoden setFarbe() und getFarbe() gehören also zum Formularfeld "farbe".

Die Controller Klasse de.example.package1.FormController dient der Anzeige und Verarbeitung des Formulares. Ich zeige hier eine vereinfachte Variante der Klasse, die zunächst ohne Validierung arbeitet:

package de.example.package1;

import java.util.Date;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;

@Controller
@RequestMapping("/formular")
@SessionAttributes({"start"})
public class FormController {

    @ModelAttribute("data")
    public FormData createData() {
        return new FormData();
    }

    @RequestMapping(method=RequestMethod.GET)
    public String handleGet(Model model) {
        model.addAttribute("start",new Date());
        System.out.println("model="+model.asMap());
        return "formInput";
    }

    @RequestMapping(method=RequestMethod.POST)
    public String handlePost(Model model,
            @ModelAttribute("data") FormData data,
            BindingResult result
        ) {

        System.out.println("model="+model.asMap());
        // Hier könnte man die Eingaben verarbeiten,
        // z.B. irgendwo abspeichern.

        if (result.hasErrors())
            return "formInput";
        else
            return "formSuccess";
    }

}

Ganz oben finden Sie eine neue Annotation mit Namen @SessionAttributes({"start"}). Sie legt fest, daß das Model Attribute mit Name "start" in der Session gespeichert werden soll. Da bedeutet, alle folgenden Aufrufe dieses Controllers liefern den zuvor gesetzten Wert an Freemarker, sofern man ihn nicht überschreibt. In diesem Controller wird das Session-Attribut genutzt, um sich zu merken, wann der Benutzer das Formular aufgerufen hat. Wenn er bei der Eingabe Fehler macht, und es daher erneut sieht, dann wird das vorherige Datum angezeigt.

Der Controller hat eine Methode, die mit @ModelAttribute annotiert ist. Der Name der Methode ist übrigens beliebig. Die Annotation legt fest, unter welchem Namen die Command Bean in den Model-Attributen gespeichert werden soll. Die Methode darunter hat die Aufgabe, eine Instanz von dem Command Bean zu erzeugen.

Um es etwas praktischer formulieren: An dieser Stelle wird eine Instanz von der Klasse FormData erzeugt. Die Farbe, die der Kunde in das Formular eingetippt hat, befindet sich später in diesem Objekt, umd bei der Ausgabe wird sie unter dem Namen "data.farbe" verfügbar sein. Gleiches gilt auch für den Namen und der Nummer.

Der Controller hat zwei Methoden, die aufgrund der URL /formular aufgerufen werden, und zwar die Methode handleGet() für GET-Requests, und die Methode handlePut() für PUT-Requests. Web-Browser fordern Seiten immer mit er GET methode an. Formular-Eingaben werden mit der POST-Methode gesendet.

Die Methode handleGet() soll also ein neues leeres Formular anzeigen. Um die Template-Variable "start" zu füllen, setzt sie ein Model-Attribute mit diesem Namen und dem aktuellen Datum (was die Uhrzeit einschliesst). Beachten Sie, daß hier nicht eine eigene HashMap verwendet werden darf, wie in der Klasse TemplateController, sonst würden die @ModelAttribute Annotationen nicht funktionieren. Stattdessen verwendet man die Map, die Spring über den Parameter "Model model" bereit stellt.

Die Methode handlePut() wird aufgerufen, wenn der Benutzer den Absenden-Knopf drückt. Sie empfängt alle Formular- Eingaben in der Command Bean data, welche von Spring bereits vor Aufruf der Methode zu den Model Attributen hinzugefügt wurde.

In obigem Beispiel wird der Inhalt dieses Bean einfach geloggt und dann wird das Template formSuccess.ftl angezeigt, sofern beim Binden der Eingabeparameter kein Fehler auftrat. Wenn jedoch ein Fehler auftrat, dann wird das Formular erneut angezeigt, und dann enthält es auch Fehlermeldungen.

Das kann zum Beispiel passieren, wenn der Benutzer in das Feld für die Nummer versehentlich ein Wort eintippt, anstatt eine Zahl.

Fehlermeldungen der Validierung

Bei dem obigen Formular können Sie schon die erste Validierung ausprobieren. Geben Sie mal etwas ungültiges ein:

Bildschirmfoto mit einer Fehlermeldung im Eingabeformular

Sie sehen, daß Spring ganz automatisch eine Fehlermeldung erzeugt hat, die im Formular durch folgende Freemarker Tags angezeigt werden:

  <#assign status=springMacroRequestContext.getBindStatus("data.number")>
  <#if status.isError()>
      <font color="red">
          ${status.errorMessage} (Code: ${status.errorCode})
      </font><br>
  </#if>

Alternativ könnte man auch Makros aus der Datei spring.ftl verwenden:

<#import "spring.ftl" as spring/>
<html>
   <body>
      ...
      <font color="red">
          <@spring.bind "data.number"/>
          <@spring.showErrors "<br>"/>
      </font><br>
      ....
    </body>
</html>

Ich mag dieses Marko aber nicht so, weil es unter Umständen mehrere Fehlermelung pro Feld anzeigt und weil es den Fehlercode nicht anzeigt, was aber speziell während der Entwicklung- und Testphase hilfreich ist. Man braucht diese Codes nämlich, um bessere Meldungstexte zu konfigurieren.

Meldungstexte konfigurieren

Für jeden Code können Sie eigene Meldungstexte definieren. Dazu legen Sie Hauptverzeichnis der Klassen eine Properties-Datei an, oder mehrere für einzelne Sprachen. Zum Beispiel:

Nach dem Compilieren befinden sich diese Dateien im Verzeichnis WEB-INF/classes.

Damit Spring die Datein verwendet, bedarf es folgenden Eintrages in der Kontext-Konfigurationsdatei:

    <bean id="messageSource" class=
      "org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="messages"/>
    </bean>

Die ID muss "messageSource" sein und der Wert des Property "basename" entspricht dem Anfang des Dateinamen. Wenn Sie ihre Meldungen über mehrere Dateien verteilen wollen, geht das so:

   <bean id="messageSource" class=
      "org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>messages</value>
                <value>errors</value>
            </list>
        </property>
    </bean>

In diesem Fall gibt es eine zwei Dateien: messages.properties und errors.properties, deren Inhalt so verwendet wird, als stände er in einer einzigen Datei. Auch in diesem Fall kann man von allen Dateien sprachspezifische Varianten anlegen.

In der deutschen messages.properties Datei stehen für die obige Fehlermeldung folgende Ersatz-Texte:

typeMismatch=Ungültige Eingabel im Feld {0}
typeMismatch.java.lang.Integer=Ungültige Zahl im Feld {0}
typeMismatch.number=Die Eingabe im Feld Nummer ist keine gültige Zahl
typeMismatch.data.number=Du musst im Feld Nummer eine Zahl eingeben

Dabei ist nur eine dieser Zeilen erforderlich. Fals Spring mehrere Zeilen findet, verwendet es stets die am stärksten spezifische, also typeMismatch.data.number. Dabei ist typeMismatch der eigentliche Fehlercode und {0} wird durch denen Namen des Feldes ersetzt. Die Entscheidung, ob Sie {0} im Meldungstext verwenden, bleibt Ihnen selbst überlassen.

Nach diesem Schema geht man für alle Fehlercodes vor, die auftreten können. Wenn Spring für einen Fehlercode keinen Meldungstext findet, wird der häßliche hardcodierte Standard-Text verwendet, den sie oben gesehen haben. Bitte bachten Sie, daß Validatoren nicht zwingend einen Standard-Text liefern müssen. Es kann auch passieren, daß irgendein Validator nur einen Fehlercode aber keinen Text liefert.

Durch das Hinzügen dieser Properties, erhalten Sie eine viel schönere Fehlermeldung:

Bildschirmfoto mit einer Fehlermeldung im Eingabeformular

Spring prüft nur, ob die Eingabe in den vom Bean verlangten Typ konvertiert werden kann. Jede Integer Zahl wäre also in Ordnung. In den nächsten Absätzen erkläre ich, wie sie darüber hinaus eigene Überprüfungen einbauen können.

Validierung mit Exceptions

Die einfachste Variante, eigene Validierungen hinzu zu fügen, ist der Einsatz von Exceptions. Die Klasse FormData (unserer Command Bean zeigt, wie das geht:

public class FormData {
    ...
    private String farbe;
    ...
    public void setFarbe(String farbe) throws Exception {
        if (farbe==null || farbe.equals("rot") ||
            farbe.equals("gelb") || farbe.equals("grün"))
            this.farbe=farbe;
        else
            throw new Exception("Ungültige Farbe");
    }

    public String getFarbe() {
        return farbe;
    }
}

Wenn jemand versucht, eine ungültige Farbe zu speichern, wird die Exception geworfen, was dann im Web-Browser so aussieht:

Bildschirmfoto mit einer Fehlermeldung im Eingabeformular

Auch hier kann man wieder eigene Meldungstexte in die messages.properties schreiben.:

methodInvocation=ungültige Eingabe in Feld {0}
methodInvocation.java.lang.String=Die Eingabe ist im Feld {0} ist ungültig
methodInfocation.farbe=Diese Farbe gibt es nicht
methodInvocation.data.farbe=Diese Farbe ist nicht zulässig

Wieder würde eine einzige dieser Zeilen genügen.

Bildschirmfoto mit einer Fehlermeldung im Eingabeformular

Die Methode, mit Exceptions zu validieren hat einen gravierenden Nachteil: Alle Exceptions werden auf den selben Fehlercoe abgebildet, nämlich methodInvocation. Sie können also nur eine einzige Fehlermeldung pro Eingabefeld definieren. Sie können z.B. bei Zahlen nicht unterscheiden, ob die eingegebene Zahl zu groß oder zu klein ist, da beide Fälle den seblen Fehlercode hätten.

Zwar können Sie den Meldungstext der Exception anzeigen, aber der sieht nicht schön aus, ist nicht konfigurierbar und typischerweise englisch.

Die beiden folgenden Absätze beschreiben bessere Methoden zur Validierung.

Validierung mit Klassen

Validierungs-Klassen müssen das Interface org.springframework.validation.Validator implementieren. Die Klasse de.example.validators.FormDataValidators zeigt, wie es geht:

package de.example.validators;

import de.example.package1.FormData;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class FormDataValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return FormData.class.equals(clazz);
    }

    @Override
    public void validate(Object obj, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "leer",
            "Null or empty String is not allowed");
        FormData data=(FormData)obj;
        if (data.getName()!=null) {
            if ( data.getName().length()<3)
               errors.rejectValue("name", "zuKurz","String is to short");
        }
    }

}

Die Methode supports() wird von Spring verwendet, um zu erfragen, ob dieser Validator zur Überprüfung einer bestimmten Klasse geeignet ist. Wenn ja, liefert er true zurück.

Die Methode validate() überprüft das übergebene Objekt obj und meldet ggf. gefundete Fehler über errors zurück. Dabei kann jedes Objekt mehrere Fehler enthalten.

Mit ValidationUtils.rejectIfEmpty() wird zuerst geprüft, ob das Feld "name" in dem übergebenen Objekt (eine Instanz von FormData) null oder leer ist. ValidationUtils enthält ein paar Methoden für Schreibfaule. Man könnte genau so gut mit if() Ausdrücken arbeiten. Der Parameter "leer" ist der Fehlercode, den man erzeugen möchte und dahinter kommt optional ein Meldungstext. Dieser Meldungstext kann wie bereits geschrieben durch message Properties ersetzt werden.

Mit der Methode errors.rejectValue() erzeugt man eine Fehlermeldung für dan angegebene Feld, in diesem Fall wieder das Feld "name". Der Fehlercode ist "zuKurz" und dahinter folgt optional ein Meldungstext.

Dieser Validator wird von der Controller Klasse aufgerufen:

@Controller
@RequestMapping("/formular")

public class FormController {

    @Autowired
    Validator formDataValidator;

    ...

    @RequestMapping(method=RequestMethod.POST)
    public String handlePost(Model model,
            @ModelAttribute("data") FormData data,
            BindingResult result
        ) {

        formDataValidator.validate(data, result);
        System.out.println("model="+model.asMap());

        // Hier könnte man die Eingaben verarbeiten,
        // z.B. irgendwo abspeichern.

        if (result.hasErrors())
            return "formInput";
        else
            return "formSuccess";
    }
}

Durch den Aufruf formDataValidator.validate(data, result) wird das data Objekt geprüft und die Fehlermeldungen in result hinein geschrieben. Auch Spring schreibt seine Meldungen vom Binder in result hinein.

Eine Instanz des Validator befindet sich in der Variable

    @Autowired
    Validator formDataValidator;

Die Annotation @Autowired sorgt dafür, daß diese Variable automatisch mit einer Instanz der Klasse FormDataValidator bestückt wird, entsprechend der Konfiguration in test1-servlet.xml:

<bean id="formDataValidator"
    class="de.example.validators.FormDataValidator" />

Hier ist "formDataValidator" der Name der Variable und "de.example.validators.FormDataValidator" ist die Klasse, von der eine einzige Instanz erzeugt wird und die dann allen @Autowired Variablen mit diesem Namen zugewiesen wird.

Validierung mit Annotationen

Anhand des Formulares demonstriert die Beispielanwendung noch eine weitere Variante der Validierung, die ich persönlich besonders schick finde. Und zwar die Validierung mittels Annotation gemäß JSR-303 Standard.

Dieser Standard wurde von Sun im Jahre 2009 freigegeben, aber leider noch nicht umgesetzt. Java 1.6 enthält noch keine Validierungs-Annotationen. Die aktuelle Referenz-Implementierung findet man im Hibernate-Projekt unter dem Namen hibernate-validator. Ohne diese Library kommt man nicht herum, nicht einmal, wenn man ausschließlich selbst geschriebene Validatoren benutzen will.

Damit Validierungs-Annotationen funktionieren, muß in die Kontext-Konfigurationsdatei test1-servlet.xml eine Zeile eingefügt werden:

   <mvc:annotation-driven/>

Die Beispielanwendung validiert das Feld "number" in der Command Bean de.example.package1.FormData folgendermaßen:

    public class FormData {

        ...

        @Range(min=0, max=100) 
        private Integer number;

        ...

        public void setNumber(Integer number) {
            this.number=number;
        }

        public Integer getNumber() {
            return number;
        }
    }

Die Annotation @Range befindet sich in der hibernate-validator Library. Sie beschränkt den Integer Wert auf den Bereich 0 bis 100. Man kann über den Setter noch andere Werte hinein schreiben, aber im Rahmen des Bindens ruft Spring den Annotation-Validator auf, und dann wird er meckern, wenn die Zahl kleiner oder größer ist.

Damit Spring den Anootation-Validator aufruft, muß man die Annotation @Valid hier einbauen:

@Controller
@RequestMapping("/formular")
public class FormController {   ...

    @RequestMapping(method=RequestMethod.POST)
    public String handlePost(Model model,
            @ModelAttribute("data") @Valid FormData data,
            BindingResult result
        ) {

        System.out.println("model="+model.asMap());
        // Hier könnte man die Eingaben verarbeiten,
        // z.B. irgendwo abspeichern.

        if (result.hasErrors())
            return "formInput";
        else
            return "formSuccess";
    }
}

Nur die ModelAttribute, die zusätzlich mit @Valid annotiert werden, werden von JSR-303 Validatoren überprüft. Wieder kann man eigene Fehlermeldungen in der messages.properties definieren.

Die Beispielanwendung hat keine neuen Meldungen definiert, und der Hibernate Validator liefert auch keinen standard Meldungstext. Darum sieht die Webseite so aus:

Bildschirmfoto mit einer Fehlermeldung im Eingabeformular

Eigene Validirungs-Annotationen schreiben

Früher oder später werden sie eigene Validatoren schreiben wollen oder müssen. Dazu erstellen Sie ein Interface nach dem Vorbild von de.example.validators.Maximum. Diese Annotation soll den Wert einer Integer Zahl auf einen einstellbaren maximalen Wert begrenzen:

package de.example.validators;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy={MaximumValidator.class})
@Documented
public @interface Maximum {

    String message() default "Number is to large";

    int maxValue();

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

Die Angabe "validatedBy" bestimmt die Klasse, in der der Programmcode des Validators steht. Die "message" ist der Meldungstext, der zurück geliefert wird, wenn die validierung fehlschlägt. "maxValue" ist ein Parameter für die Validierung, in diesem Fall der maximal erlaubte Zahlenwert.

Die Klasse de.example.validators.MaximumValidator implementiert diese Annotation:

package de.example.validators;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class MaximumValidator
      implements ConstraintValidator<Maximum,Integer> {

    private int maximum;

    @Override
    public void initialize(Maximum constraint) {
        maximum=constraint.maxValue();
    }

    @Override
    public boolean
          isValid(Integer value, ConstraintValidatorContext context) {
        return value==null || value<=maximum;
    }
}

Der Code des Validators ist ganz simpel. Die Methode initialize() speichert den Parameter maximum in eine lokale private Variable. Die Methdode isValid() meldet zurück, ob der zu prüfende Wert in Ordnung ist. In diesem Fall lassen wir null und alle Werte, die kleiner oder gleich dem Maximum sind, zu.

Vollständiger Text von FromController und FormData

Nachdem ich die beiden Klassen FormController und FormData stückweise erklärt habe, folgt nun deren vollständiger Quelltext:

package de.example.package1;

import java.util.Date;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;

@Controller
@RequestMapping("/formular")
@SessionAttributes({"start"})
public class FormController {

    @Autowired
    Validator formDataValidator;

    @ModelAttribute("data")
    public FormData createData() {
        return new FormData();
    }

    @RequestMapping(method=RequestMethod.GET)
    public String handleGet(Model model) {
        model.addAttribute("start",new Date());
        System.out.println("model="+model.asMap());
        return "formInput";
    }

    @RequestMapping(method=RequestMethod.POST)
    public String handlePost(Model model,
            @ModelAttribute("data") @Valid FormData data,
            BindingResult result
        ) {

        formDataValidator.validate(data, result);
        System.out.println("model="+model.asMap());
        // Hier könnte man die Eingaben verarbeiten,
        //z.B. irgendwo abspeichern.

        if (result.hasErrors())
            return "formInput";
        else
            return "formSuccess";
    }

}

Und nun das Command Bean FormData:

package de.example.package1;

import de.example.validators.Maximum;

    public class FormData {

        //@Range(min=0, max=100) // Hibernate Validator
        @Maximum(maxValue=100)   // Selbst gebauter Validator
        private Integer number;

        private String name;

        private String farbe;


        @Override
        public String toString() {
            return number+","+name+","+farbe;
        }

        public void setNumber(Integer number) {
            this.number=number;
        }

        public void setName(String name) {
            this.name=name;
        }

        public void setFarbe(String farbe) throws Exception {
            if (farbe==null || farbe.equals("rot") ||
                farbe.equals("gelb") || farbe.equals("grün"))
                this.farbe=farbe;
            else
                throw new Exception("Ungültige Farbe");
        }

        public Integer getNumber() {
            return number;
        }

        public String getName() {
            return name;
        }

        public String getFarbe() {
            return farbe;
        }
    }

Interceptoren

Interceptoren werden bei hereinkommenden HTTP Requests aufgerufen, bevor die dazu gehörenden Controller den Request bearbeiten. Sie haben bereits den org.springframework.web.servlet.i18n.LocaleChangeInterceptor kennen gelernt, der die Darstellungs-Sprache umschalten kann.

Ich zeige Ihnen nun, wie sie eigene Interceptoren schreiben können, und zwar am Beispiel der Klasse de.example.MyInterceptor:

package de.example;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class MyInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request,
            HttpServletResponse response,Object handler) throws Exception {
        String queryString=request.getQueryString();
        System.out.println("Received HTTP Request: "+
            request.getRequestURI()+"?"+queryString);
        if (queryString!=null &&
            queryString.contains("scheisse")) {
            throw new Exception(
               "MyInterceptor hat ein böses Wort in der URL gefunden");
        }
        return true;
    }
}

Dieser Interceptor soll die URL und Parameter aller hereinkommenden Requests protokollieren, und er soll sich mit einer Exception beschweren, wenn in den URL Parametern das Wort "scheisse" vorkommt.

Wenn der Interceptor mit true endet, dann wird der Controller aufgerufen. Ansonsten wird der Controller nicht aufgerufen. Interceptoren können also die normale Bearbeitung von Requests verhindern.

Damit der Interceptor in die Verarbeitngskette eingebaut wird, bedarf es ein paar Zeilen in der Kontext Konfiguration test1-servlet.xml:

    <mvc:interceptors>
        <bean class=
            "org.springframework.web.servlet.i18n.LocaleChangeInterceptor" />
        <bean class="de.example.MyInterceptor"/>
    </mvc:interceptors>

Wenn Sie jetzt das böse Wort eintippen, beschwert sich der Interceptor wie gewünscht:

Bildschirmfoto mit Meldung vom Interceptor

Freemarker Konfiguration

Struts Anwendungen konfigurieren Freemarker in der Kontext Konfigurationsdatei:

    <bean id="freemarkerConfig" class=
      "org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
        <property name="templateLoaderPath" value="/WEB-INF/templates/test/"/>
        <property name="freemarkerSettings">
            <props>
                <prop key="template_exception_handler">
                    de.example.MyTemplateExceptionHandler
                </prop>
                <prop key="default_encoding">UTF-8</prop>
                <prop key="template_update_delay">10</prop>
            </props>
        </property>
    </bean>

Das default_encoding gibt an, in welchem Zeichensatz die Templates geschrieben sind. Wenn man diese Einstellung weg lässt, geht Freemarker davon aus, daß die Templates im Standard-Zeichensatz des Betriebssytems geschrieben wurden. Bei Windows ist dies üblicherweise ISO-8859-1 und bei neueren Linux Distributionen ist es üblicherweise UTF-8.

Das template_update_delay legt fest, nach wie vielen Sekunden Freemarker nachschauen soll, ob die Template Datei inzwischen verändert wurde.

Den MyTemplateExceptionHandler habe ich bereits weiter oben erklärt.

Eigenen Template Loader einbinden

Freemarker kann Templates nur aus dem WEB-INF Verzeichnis laden. Wenn Sie Ihre Templates außerhalb der Web-Applikation ablegen wollen, brauchen Sie einen eigenen Template-Loader. Als Kopiervorlage habe ich Ihnen einen simplen Template-Loader geschrieben, der die Funktion des Standard Loader nachahmt:

package de.example;

import freemarker.cache.TemplateLoader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.servlet.ServletContext;
import org.springframework.stereotype.Repository;
import org.springframework.web.context.ServletContextAware;

@Repository
public class MyTemplateLoader
         implements TemplateLoader, ServletContextAware {

    /** Servlet Kontext, enthält alle Ressourcen */
    private ServletContext context;

    /** Pfad, wo die Template Dateien liegen */
    private String path;

    /**
     * Diese Methode soll ein Template suchen. Wir loggen hier mit,
     * welches Template gesucht wurde. So kann man schön sehen,
     * wie Freemarker die Sprache-Spezifischen Templates sucht.
     * @param filename Dateiname
     * @return Das Template (in diesem Fall als Stream,
     *         kann aber auch etwas anderes sein)
     * @throws IOException
     */
    public Object findTemplateSource(String filename)
           throws IOException {
        System.out.println("findTemplateSource("+path+filename+")");
        return context.getResourceAsStream(path+filename);
    }

    /**
     * Wird von Cache verwendet, um die Datei ggf. neu zu laden.
     * Da wir hier mit Streams arbeiten und keinen solchen
     * Zeitstempel haben, müssen wir -1 zurück geben. Die Datei
     * wird dadurch regelmäßig neu geladen.
     * @param stream not used
     * @return Always -1
     */
    public long getLastModified(Object stream) {
        return -1;
    }

    /**
     * Diese Methode soll einen Reader auf das Template liefern,
     * welches zuvor über findTemplateSource geöffnet wurde.
     * @param stream Template, wie von findTemplateSource() geliefert.
     * @param encoding Das Encoding der Templates (siehe
     *        Property in the Kontext Konfiguration)
     * @return Ein Reader
     * @throws IOException
     */
    public Reader getReader(Object stream, String encoding)
           throws IOException {
        return new InputStreamReader((InputStream) stream, encoding);
    }

    /**
     * Diese Methode soll das Template nach dem Lesen schließen.
     * @param stream Template, wie von findTemplateSource() geliefert.
     * @throws IOException
     */
    public void closeTemplateSource(Object stream) throws IOException {
        ((InputStream) stream).close();
    }

    /**
     * Wird von Spring aufgerufen, weil diese Klasse ServletContextAware
     * implementiert. Wir gebraucht, um an den Servlet Kontext
     * heran zu kommen, aus dem wir die Templates laden.
     * @param servletContext
     */
    public void setServletContext(ServletContext servletContext) {
        context=servletContext;
    }

    /**
     * Wird über ein Bean Property aus der Kontext-Konfiguration gesetzt.
     * @param templatePath Pfad, wo die Templates liegen.
     */
    public void setPath(String templatePath) {
        path=templatePath;
    }

}

Sie müssten ihn umschreiben, um Dateien von außerhalb des WEB-INF Verzeichnisses zu laden, zum Beispiel mit der File Klasse oder mit der URL Klasse anstelle von context.getResourceAsStream().

Um den eigenen Template Loader zu aktiviere, fügen Sie in die Kontext Konfigurationsdatei test1-servlet.xml die folgende Zeile ein:

    <bean id="freemarkerConfig" class=
      "org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
        <property name="preTemplateLoaders">
            <ref bean="myTemplateLoader"/>
        </property>
        ...
    </bean>

    <bean name="myTemplateLoader" class="de.example.MyTemplateLoader">
        <property name="path" value="/WEB-INF/templates/test/"/>
    </bean>

MessageBundles in Freemarker Templates

In den obigen Beispielen wurden MessageBundles (messges.properties) verwendet, um die Fehlermeldungen der Validatoren zu internationalisieren. Diese MessageBundles können Sie auch in Templates verwenden:

<#import "spring.ftl" as spring/>
<html>
  <head>
    <title>Spring Test</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  </head>
  <body>
    <@spring.message "hello"/>
  </body>
</html>

Dazu müssen Sie die Datei spring.ftl in Ihr Template importieren, die sie übrigens in der Library spring-servlet.jar finden, und zwar im Klassenpfad org.springframework.web.servlet.view.freemarker. Sie müssen die Datei dort heraus kopieren und in das Verzeichnis mit Ihren Templates ablegen.

Das Makro spring.message gibt den Meldungstext aus, der unter dem dem Schlüssel "hello" in der Datei messages.properties Properties steht:

hello=Hallo

Nachwort

Sie wissen jetzt genug, um eine vorzeigbare Web Anwendung mit Spring zu erstellen. Erweitern Sie Ihre Kenntnisse anhand der Online Dokumentationen von Spring und Freemarker.