<jsptutorial />

Einführung ins erste Beispiel


Die Anwendung

Im JSP-Tutorial werden wir ein durchgängiges Beispiel benutzen. Dabei handelt es sich um ein sehr einfaches, webbasiertes Vokabelprogramm. Die Funktionalität wird im Laufe des Tutorials immer weiter ausgebaut. In einem ersten Schritt soll die Anwendung zumindest die Abfrage der Vokabeln ermöglichen und am Ende der Abfrage eine Ergebnisübersicht präsentieren. Sind alle Vokabeln einer Lektion abgefragt, oder hat die Nutzerin die Abfrage unterbrochen, so wird eine Auswertung der Abfragesitzung angezeigt. Wir haben damit die folgenden nur grob benannten Anwendungsfälle:1


In einer realen Anwendung müsste sich eine Lernende vermutlich erst anmelden und dann aus den ihr zur Verfügung stehenden Unterrichtseinheiten eine auswählen. Da wir in der Beispielanwendung anfangs allerdings keine verschiedenen Nutzerinnen oder gar Rollen kennen, ist in unserer Anwendung die Einstiegsseite gleich die Seite zur Auswahl einer Lektion.

UseCase "Anzeige einer Vokabelabfrage"

Solange weitere Vokabeln zu der von der Nutzerin ausgewählten Lektion existieren, kann die Nutzerin bei jeder Vokabelabfrage eine Antwort eingeben. Sie kann das entsprechende Eingabefeld aber auch leer lassen, dann gilt diese Vokabel als nicht beantwortet. Auf der Maske zur Vokabelabfrage wird anstelle einer Vokabeleingabe auch die Möglichkeit geboten, die Lektion zu beenden und direkt zur Anzeige der Ergebnisübersicht zu gelangen.

UseCase "Initialisierung einer Lernsitzung"

Die Initialisierung kann nicht explizit durch die Nutzerin angestoßen werden, sondern wird vom UseCase "Auswahl einer Lektion" inkludiert. Hier wird eine möglicherweise bereits bestehende Lernsitzung beendet und eine neue Lernsitzung mit Eintrag des Anfangszeitpunkts erzeugt.

UseCase "Anzeige der Ergebnisübersicht"

Dieser UseCase kann von der Nutzerin explizit aufgerufen werden, indem sie "Lektion beenden" auf einer Vokabel-Abfrageseite auswählt. Dabei ist der Aufruf "Lektion beenden" jederzeit möglich, selbst wenn die Lektion noch weitere Vokabeln enthält. Enthält die Lektion keine weiteren Vokabeln, so wird nach Eingabe der letzten Vokabel automatisch dieser UseCase aufgerufen.
Alle beantworteten Vokabeln der Lektion werden in der Ergebnisübersicht tabellarisch in Originalsprache und Übersetzung wiedergegeben. Dabei wird kenntlich gemacht, welche Eingaben korrekt und welche falsch waren. Bei den falschen Antworten wird die Fehleingabe zusätzlich angezeigt.
Alle nicht explizit in den vorhergehenden UseCases "Anzeige einer Vokabelabfrage" beantworteten Vokabeln werden in einer Liste nicht beantworteter Vokabeln ausgegeben. Dabei ist es unerheblich, ob bei einer Abfrage das Antwortfeld leer gelassen wurde oder die Vokabel aufgrund einer vorzeitigen Beendigung der Lektion gar nicht angezeigt wurde.

UseCase "Auswahl einer Lektion"

Zunächst einmal prüft der UC, ob eine Lernsitzung existiert. Wenn nicht, wird zunächst der UC "Initialisierung einer Lernsitzung" ausgeführt. Sobald eine Lernsitzung existiert, wird eine Auswahlliste aller zur Verfügung stehenden Lektionen angezeigt. Eine ausgewählte Lektion wird in die Lernsitzung gespeichert.

Zum SeitenanfangZum Seitenanfang

Ein erster Design-Entwurf

Die Anwendung soll dem im Kapitel "Anwendungsarchitektur" vorgestellten Design-Muster Model View Controller in seiner Ausprägung Model 2 folgen.
Das Model besteht aus einer Anwendungsschicht, repräsentiert durch die Services und die Domänenlogik, die in unserem Fall gerade einmal die Geschäftsobjekte Vocab, VocabAnswer, Unit und LearningSession umfasst. Die Services bilden dabei die nach außen sichtbare Schnittstelle der Geschäftslogik.2 Die Geschäftslogik selbst ist bewusst simpel gehalten, da sie hier nur als Grundlage für die Beispielanwendung dient und ansonsten für das Tutorial ohne Bedeutung ist.
Als dauerhaft gespeicherte Daten existieren in unserer Anwendung ausschließlich die Vokabeln der verschiedenen Lektionen. Auf diese Daten wird über ein DataAccessObject (DAO) zugegriffen. I.a.R. werden DAOs als Interfaces deklariert und eine konkrete Implementierung für die letztlich genutzte Persistenztechnologie genutzt. Dabei kommt nahezu immer eine Datenbank zum Einsatz, doch letztlich ist für die Entwicklerin der Geschäftslogik die dahinter liegende Persistenzschicht transparent. In unserem Fall wird denn auch anstelle einer Datenbank lediglich eine simple Text-Datei mit einem definierten Schema verwendet, da für das Verständnis des Beispiels die Datenhaltung keine Rolle spielt und die Anwendung so ohne jedwede zusätzliche Installation und Konfiguration direkt lauffähig ist.
Der Controller der Anwendung teilt sich auf in ein FrontController-Servlet und einzelne Handler, die die einzelnen Anfragen der Nutzerin bearbeiten. Üblicherweise verwendet man in realen Projekten ein existierendes Framework wie JSF, Struts, Spring MVC u.ä.3 Für unsere Absicht, mit einem ersten Beispiel die Funktionsweise eines derartigen Frameworks aufzuzeigen, ist allerdings die Nutzung eines bestehenden Frameworks nicht geeignet. Diese erledigen weitaus mehr, als wir für unseren Controller benötigen und würden damit unsere Betrachtung erschweren. Erst später werden wir in eigenen Kapiteln auch auf JavaServer Faces und Struts 2 detailliert eingehen.
Da unser FrontController-Servlet nur als Beispiel dienen soll, beschränken wir es auf das Notwendigste. Nutzen Sie daher das FrontController-Servlet nicht als Vorbild für Eigenentwicklungen - es dient nur der Veranschaulichung der groben Funktionsweise entsprechender Model 2-Architekturen.
Als View-Technologie setzen wir im JSP-Tutorial natürlich auf JSPs ;-) In den nächsten Kapiteln werden die Expression Language und die Java Standard Tag Library ausführlich behandelt und Beispiele aus der Anwendung verwendet, um die Nutzung der beiden Technologien zu verdeutlichen. Ansonsten nutzen die JSPs anfänglich keine besonderen Elemente.
In der gesamten Anwendung findet keinerlei Authentifizierung und Autorisierung statt. Es gibt auch keinerlei personen- oder rollenbezogene Daten, Zugriffsrechte oder Funktionalitäten. Dies ist natürlich für reale Webanwendungen völlig unrealistisch, wäre aber für das erste Beispiel zu weitführend. Wir werden auf diese Thematik später in einem eigenen Kapitel eingehen.

Zum SeitenanfangZum Seitenanfang

Das FrontController-Servlet

Das FrontController-Servlet nimmt die folgenden Aufgaben wahr:


  1. Sicherstellung der korrekten Encodings,

  2. Ermittlung des zuständigen Handlers,

  3. Aufruf des Handlers und

  4. Ermittlung der View und Weiterleitung an diese.


Sicherstellung der korrekten Encodings

Wie im Kapitel Internationalisierung beschrieben, muss bei internationalisierten Anwendungen sichergestellt werden, dass Umlaute und Sonderzeichen korrekt eingelesen werden. Dazu muss man vor dem ersten Zugriff auf Request-Werte die Methode "setCharacterEncoding(String)" auf dem Request-Objekt aufrufen. Da unsere Anwendung UTF-8 nutzt, sieht die entsprechende Zeile im FrontController wie folgt aus:
request.setCharacterEncoding("UTF-8");

Ermittlung des zuständigen Handlers

Die benötigten Handler (s. dazu den nächsten Abschnitt) werden anhand der URL erkannt. Im Falle der Beispielanwendung werden alle URLs nach dem folgenden Schema aufgebaut:

http://localhost:8084/firstExample/FrontController/HandlerName

Der Host, Port und Context-Pfad hängen natürlich von ihrer konkreten Umgebung ab, in die Sie die Beispielanwendung installiert haben. Anhand des Bestandteils "/FrontController" direkt hinter dem jeweiligen Servlet-Kontext wird das FrontController-Servlet aufgerufen. Dazu muss das entsprechende Mapping in der Konfigurationsdatei web.xml wie im Anhang II: Servlets - Die Grundlagen beschrieben vorgenommen werden:
<servlet>
   <servlet-name>FrontController</servlet-name>
   <servlet-class>org.jsptutorial.examples.firstexample.frontcontroller.FrontController</servlet-class>
</servlet>
<servlet-mapping>
   <servlet-name>FrontController</servlet-name>
   <url-pattern>/FrontController/*</url-pattern>
</servlet-mapping>

Die Angabe "/HandlerName" bietet zusätzliche Pfadinformationen zu diesem Servlet, die von dem Servlet geparst und zur Bildung des Handler-Klassennamens verwendet werden:
// the pathInfo contains the information which handler to call
String pathInfo = request.getPathInfo();
if (pathInfo == null || "/".equals(pathInfo)) {
   response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}

// ...

// construct the fully qualified class name for the handler class
// that maps to the given request uri
String className = Character.toUpperCase(pathInfo.charAt(1)) + pathInfo.substring(2);
String handlerName = handlerPackage + '.' + className + "Handler";

Aufruf des Handlers

Anschließend wird der Handler aufgerufen und als Rückgabe der Name der nächsten View erwartet. Wird null zurück gegeben, so wird als Defaultwert HandlerName aus der Anfrage-URL genutzt:
Handler handler;
try {
   // instantiate the class and cast it
   Class clazz = Class.forName(handlerName);
   handler = (Handler) clazz.newInstance();
} catch (ClassNotFoundException ex) {
   throw new ServletException(ex);
} catch (InstantiationException ex) {
   throw new ServletException(ex);
} catch (IllegalAccessException ex) {
   throw new ServletException(ex);
}

// finally call the handlers process request method which should do all the work
String view = handler.processRequest(request, response);

Ermittlung der View und Weiterleitung an diese

Schlussendlich wird ein Objekt vom Typ javax.servlet.RequestDispatcher genutzt, um die Anfrage an die View per Forward weiterzureichen. Damit wir die Werte, die wir zur Präsentation in der View in Request-Attributen abgelegt haben, auch in der JSP nutzen können, müssen wir Forward nutzen. Ein Redirect würde nicht funktionieren, da dann zunächst eine Antwort an den Browser gesendet würde, mit der Aufforderung an diesen, automatisch eine andere Seite (eben die Redirect-Zieladresse) aufzurufen. Bei Redirects wäre zudem das Verstecken der JSPs unterhalb des Verzeichnisses WEB-INF nicht möglich, da dieses Verzeichnis gemäß Servlet-Spezifikation nach außen hin nicht sichtbar ist.
Das entsprechende Codefragment zur Weiterleitung an die View sieht wie folgt aus:
String view = handler.processRequest(request, response);

if (!response.isCommitted()) {
   // only forward, if we did not forward or redirect previously
   // otherwise an exception would be thrown
   if (view == null) {
      view = className;
   }
   RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/jsp/" + view + ".jsp");
   dispatcher.forward(request, response);
}

Zum SeitenanfangZum Seitenanfang

Die Handler

Wie im Kapitel zur Architektur beschrieben, wird der FrontController durch spezielle Handler-Objekte unterstützt, die für jeweilig bestimmte Anfragen zuständig sind (zumeist wird der konkrete Handler - bei Struts sind dies die Action-Objekte, bei Spring MVC die Controller-Objekte - anhand der URL bestimmt). In unserer Beispiel-Anwendung müssen diese unterstützenden Objekte das Interface org.jsptutorial.examples.firstexample.handlers.Handler implementieren. Dieses Interface definiert die Methode processRequest(HttpServletRequest, HttpServletResponse), in der die einzelnen Handler die Request-Parameter auswerten, ggf. Vorbedingungen überprüfen, die jeweilige Geschäftslogik aufrufen und anschließend die für die Anzeige benötigten Werte in den Request in Form von Attributen ablegen.
Wir haben in der Beispielanwendung drei Handler:


Zum SeitenanfangZum Seitenanfang

Die JSPs

Die Ausgabe mittels JSPs stützt sich stark auf die Expression Language und die Tags der Java Standard Tag Library. In den beiden Kapiteln zum Thema, wird die Nutzung dieser Technologien ausführlich dargestellt und anhand von Beispielen aus unserer Vokabel-Anwendung erläutert. An dieser Stelle wird daher lediglich darauf hingewiesen, dass die JSPs in dem Verzeichnis "jsp" unterhalb des Verzeichnisses "WEB-INF" liegen. Damit sind diese JSPs zwar für unseren FrontController per Forward erreichbar, allerdings - wie im Kapitel Verzeichnisstruktur von Java-Webanwendungen beschrieben - nicht direkt von einem Browser aus aufrufbar. Letzteres ist auch unbedingt notwendig, da die JSPs ohne die vorherige Aufbereitung der Daten im FrontController und den Handlern keinerlei sinnvolle Daten ausgeben könnten. Bei der Verwendung einer Model2-Architektur sollte man die JSPs immer im WEB-INF-Verzeichnis verstecken.

Zum SeitenanfangZum Seitenanfang

Fehlerbehandlung

Die Anwendung ist nicht gerade fehlerrobust gestrickt. Zum Beispiel wird die Vokabel-Datei für eine Datei ausschließlich valider Einträge gehalten. Ist dies nicht der Fall, scheitert die Anwendung beim Einlesen der Datei. In einer echten Anwendung würde man die Abhängigkeit von derartigen externen Ressourcen, seien es nun Dateien, Datenbanken oder Services anderer Anwendungen robuster gestalten und auf mögliche Fehlerfälle geeignet reagieren. Da das Auslesen der Datei für das Verständnis des Aufbaus einer Webanwendung mit Servlets und JSPs jedoch irrelevant ist, haben wir dort auch keine Fehlerbehandlung eingebaut.
Allerdings hat die Anwendung Fehlerseiten für einige typischerweise auftretende Fehler. Diese sind in der web.xml definiert und auf JSPs im Verzeichnis WEB-INF/jsp gemappt:

<error-page>
   <error-code>403</error-code>
   <location>/WEB-INF/jsp/403_forbidden.jsp</location>
</error-page>
<error-page>
   <exception-type>javax.servlet.ServletException</exception-type>
   <location>/WEB-INF/jsp/errorState.jsp</location>
</error-page>

Zum SeitenanfangZum Seitenanfang

Cross-Site-Scripting-Attacken durch Eingabevalidierung verhindern

Im Kapitel Session-Handling, Cookies und URL-Rewriting weisen wir darauf hin, wie wichtig es aus Sicherheitsgründen ist, Eingabewerte auf einen bestimmten Satz an Zeichen zu begrenzen und alle Eingaben auf die Befolgung dieser Regeln zu prüfen. Dadurch wird die Anwendung vor sogenannten Cross-Site-Scripting-Attacken geschützt, die von Angreiferinnen vor allem genutzt werden, um die eindeutige Session-ID einer Nutzerin zu erobern.
In unserer Anwendung werden ausschließlich die Übersetzungen eingegeben. Da im Beispiel englische Vokabeln ins Deutsche übersetzt werden sollen, akzeptieren wir alle ASCII-Zeichen, die deutschen Umlaute, Ziffern, Leerzeichen und Kommata (es könnten ja mehrere Bedeutungen vorkommen). Diese Prüfung wird mittels eines regulären Ausdrucks vorgenommen. Werden andere Zeichen eingegeben, so wird der Nutzerin erneut die Vokabel-Eingabeseite mit der gleichen Vokabel präsentiert, um einen Warnhinweis ergänzt, der auf die Fehlersituation aufmerksam macht und die zugelassenen Zeichen auflistet.
Dazu wird zunächst im entsprechenden Handler die Überprüfung vorgenommen und ggf. der Zustand zur erneuten Anzeige der Abfrageseite vorbereitet:

// verify that only allowed characters were entered by the user
// as a protection against XSS-attacks
// we allow only ascii characters, German umlaut characters,
// numbers, the space-character and commas; we also accept
// an empty string (which represents no anwser)
if (!answerParam.matches("[A-Za-züöäÜÖÄß0-9 ,]*")) {
   Vocab vocab = learningSession.getLastVocab();
   request.setAttribute("vocab", vocab);
   request.setAttribute("hasNext", learningSession.hasNextVocab());
   request.setAttribute("invalidCharacter", Boolean.TRUE);
   return "requestNextVocab";
}

Schlußendlich wird in der JSP geprüft, ob die Fehlersituation vorliegt und ggf. darauf reagiert. Die Prüfung und die Ausgabe der internationalisierten Texte nutzt dabei die JSTL, die im Kapitel Java Standard Tag Library erläutert wird:
<c:if test="${not empty invalidCharacter}">
   <h2 class="message"><fmt:message key="requestNextVocabPage.message.invalidCharacter.header" /></h2>
   <p class="message"><fmt:message key="requestNextVocabPage.message.invalidCharacter" /></p>
</c:if>

Zum SeitenanfangZum Seitenanfang

Internationalisierung

Die Anwendung nutzt die Java-Standardtechnologie, um mittels ResourceBundles die Texte internationalisiert anzuzeigen. Konkret werden die beiden Sprachen Englisch (als Default-Sprache) und Deutsch unterstützt. Der Aufbau dieser Dateien ist durchaus einiges an Überlegung wert. Normalerweise empfiehlt es sich, immer wiederkehrende Elemente (in unserem Fall bspw. der wiederkehrende Teil des Seitentitels) in einem gesonderten Bereich zusammen zu fassen und durch eine eindeutige Punktnotation als solchen erkennbar zu machen. Wir nutzen hier bspw. general.title, als Key für den gleichbleibenden Bestandteil des Seitentitels. Darüber hinaus orientieren wir uns an den Masken. D.h. am Anfang des Keys steht immer der Maskenname. Danach die konkreten Elemente der Maske, ggf. nochmals in Bereiche zusammengefasst. Für den Submit-Button des Eingabeformulars auf der Vokabelabfrage-Seite wird so der Key requestNextVocabPage.form.go verwendet. Die Internationalisierung richtet sich somit stark an der View aus, wo sie für die Nutzerin auch sichtbar wird. Häufig müssen aber auch Daten der Business-Objekte internationalisiert werden. Typische Vertreter sind Auswahllisten (bspw. bei einer Reisebuchungsanwendung die Art der Unterkunft und die gewünschte Verpflegung), bei denen die Texte den Datenbank-IDs zugeordnet sind. In unserem Fall sind es die Bezeichnungen für die Lektionen. Da diese häufig auf mehreren Masken vorkommen können (bei unserer Anwendung allerdings nicht), ist eine Zuordnung dieser Texte zu Masken nicht sinnvoll. Es bietet sich daher an, diese an den Bezeichner oder Klassennamen des Geschäftsobjektes anzulehnen. Ideal wäre es natürlich, die Übersetzungen lägen in der Datenbank und könnten aus dieser heraus direkt internationalisiert ausgelesen werden.
Zur Internationalisierung auf den Masken nutzen wir die JSTL-Tags der fmt-Bibliothek. Dazu mehr im Kapitel Java Standard Tag Library (JSTL). Um die gewünschte Sprache zu setzen, verwenden wir ausschließlich die erste vom Browser der Nutzerin gelieferte Locale-Information. Dies erleichtert das Auswerten internationalisierter Eingaben im Handler (nur nötig um herauszufinden, ob der "Lektion beenden"-Button gedrückt wurde). Hier weichen wir geringfügig vom Standardverhalten der JSTL ab, weswegen wir im StartupHandler ausdrücklich die zu verwendende Locale-Einstellung für die Nutzung in den JSTL-Tags setzen. Zusätzlich wird dort das ResourceBundle für die Nutzung in anderen Handlern in der Session abgelegt. Wir legen dieses Objekt nicht in die LearningSession, sondern direkt in die HttpSession, da die Internationalisierung Aufgabe der View ist. Eine Platzierung im Geschäftsobjekt wäre daher unpassend. Eine Swing-basierte View müsste hier eine entsprechend angepasste Lösung finden.

Zum SeitenanfangZum Seitenanfang

Download der Beispielanwendung

Die Beispielanwendung liegt als Zip-Datei vor. Die Dateien liegen darin alle in Verzeichnissen unterhalb des obersten Projekt-Rootverzeichnisses "firstExample". Das Projektverzeichnis ist direkt vom Netbeans-Projekt übernommen, inklusive der Netbeans-spezifischen Dateien. Wer das Projektverzeichnis in Netbeans nutzen will, muss lediglich in den Projekt-Properties einige Bibliothekspfade ändern.
Im Verzeichnis "/firstExample/dist" liegt die Anwendung desweiteren als eine fertige WAR-Datei vor, die unmittelbar auf einen Servlet-Container deployed werden kann.

Das Projekt nutzt folgende externe Open-Source-Bibliotheken:


Alle benutzten Bibliotheken stehen unter der Apache License, Version 2.0 der Apache Software Foundation. Eine Kopie der Lizenz liegt als Datei "ASL_LICENSE" im Wurzelverzeichnis "firstExample". Mehr Informationen zu den jeweiligen Projekten sowie Downloads der Binär- und Quell-Dateien finden Sie unter den oben angegebenen Links. Die aufgelisteten JAR-Dateien sind im ZIP-File unterhalb des Verzeichnisses "/firstExample/build/web/WEB-INF/lib/" zu finden.

Zum SeitenanfangZum Seitenanfang

Anmerkungen:

1) Diese Anforderungen sind extrem unspezifisch und in dieser Form völlig praxis-untauglich. Als Grundlage für unsere Beispiel sind sie dennoch ausreichend, dienen sie doch nur dazu, die Anwendung zu skizzieren. (zurück)

2) Geschäftslogik ist im Falle dieses Vokabelprogramms sicherlich etwas hochgegriffen, aber die Anwendung dient ja auch nur als Beispiel ;-) (zurück)

3) Wir raten davon ab, in Projekten ein eigenes MVC-Framework zu schreiben. Zu den meisten existierenden Frameworks - insbesondere den vier großen Struts 1.x, Struts 2, Spring MVC und JavaServer Faces - findet man online in Foren, Blogs, Newsgroups und Mailingslisten fast immer Antworten auf auftretende Fragen und Hilfe bei Problemen. Bei selbstgeschriebenen Frameworks ist man ausschließlich auf das eigene, garantiert kleinere Team gestellt. Bedenken sollte man auch, dass existierende Frameworks bereits zigfach getestet sind und dadurch i.a.R. robuster als eine Eigenentwicklung sind, deren Praxistauglichkeit am Ende erstmalig von den eigenen Kundinnen getestet wird. Ein riskantes Spiel! Schlussendlich kommt noch hinzu, dass man bei selbstgeschriebenen Frameworks, die anschließend von anderen Entwicklerinnen genutzt werden, die einzig mögliche Support- und Dokumentationsquelle darstellt. (zurück)


www.jsptutorial.org
© 2005, 2006, 2007