3. Artikel 2 – Beispiele für dreischichtige Webarchitekturen
Ziele des Artikels:
- 3-Schichten-Architekturen
- Einfache Webarchitektur MVC
- Struts-Architektur MVC
- Spring-Architektur MVC
Verwendete Tools:
- Spring: http://www.springframework.org/
- iBatis SqlMap: http://www.ibatis.com/
- JUnit: http://www.junit.org/index.htm
- Eclipse: http://www.eclipse.org/
- Struts: http://struts.apache.org/
- Firebird: http://firebird.sourceforge.net/: SGBD, Treiber JDBC. Eigentlich ist jede Quelle mit der Nummer JDBC geeignet.
- IBExpert, Personal Edition: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (März 2005). Mit IBExpert lässt sich der SGBD Firebird grafisch verwalten.
- Tomcat: http://jakarta.apache.org/tomcat/
- Tomcat-Plugin für Eclipse: http://www.sysdeo.com/eclipse/tomcatPlugin.html. Siehe auch das Dokument https://tahe.developpez.com/java/eclipse/
Zum Verständnis dieses Dokuments sind verschiedene Vorkenntnisse erforderlich. Einige davon können in Dokumenten erworben werden, die ich verfasst habe. In diesem Fall verweise ich darauf. Es versteht sich von selbst, dass dies nur ein Vorschlag ist und der Leser seine bevorzugten Dokumente verwenden kann.
- Java-Sprache: [https://tahe.developpez.com/java/cours]
- Webprogrammierung mit Java: [https://tahe.developpez.com/java/web/]
- Webprogrammierung mit Java, Eclipse, Tomcat: [https://tahe.developpez.com/java/eclipse/]
- Webprogrammierung mit Struts: [https://tahe.developpez.com/java/struts/]
- Verwendung des Spring-Aspekts IoC: [https://tahe.developpez.com/java/springioc]
- Tag-Bibliothek: JSTL (teilweise)
- Ibatis-Dokumentation SqlMap: [https://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
- Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf] (März 2005).
Die Ideen in diesem Dokument stammen aus einem Buch, das ich im Sommer 2004 gelesen habe, einem großartigen Werk von Rod Johnson: J2EE „Development without EJB“, erschienen im Wrox-Verlag.
3.1. Die Webanwendung „webarticles“
Wir möchten hier einige Elemente einer E-Commerce-Webanwendung vorstellen. Diese ermöglicht es Webkunden
- , eine Liste von Artikeln aus einer Datenbank einzusehen
- bestimmte Artikel in einen elektronischen Warenkorb zu legen
- und diesen zu bestätigen. Diese Bestätigung hat lediglich zur Folge, dass die Bestände der gekauften Artikel in der Datenbank aktualisiert werden.
Dem Benutzer werden folgende Ansichten angezeigt:
- die Ansicht [LISTE], die eine Liste der zum Verkauf stehenden Artikel anzeigt

- die Ansicht [INFOS], die zusätzliche Informationen zu einem Produkt liefert:

- die Ansicht [PANIER], die den Inhalt des Warenkorbs des Kunden anzeigt

- die Ansicht [PANIERVIDE] für den Fall, dass der Warenkorb des Kunden leer ist

- die Ansicht [ERREURS], die alle Anwendungsfehler meldet

3.2. Allgemeine Architektur der Anwendung
Wir möchten eine Anwendung mit der folgenden dreischichtigen Struktur erstellen:
- Die drei Schichten werden durch die Verwendung von Java-Schnittstellen voneinander unabhängig gemacht
- Die Integration der verschiedenen Schichten erfolgt über Spring
- Jede Schicht ist in separaten Paketen untergebracht: „web“ (Benutzeroberfläche), „domain“ (Geschäftslogik) und „dao“ (Datenzugriffsschicht).
Wir gehen hier davon aus, dass die Schichten [domain] und [dao] bereits vorhanden sind. Wir konzentrieren uns ausschließlich auf die Schicht [web], die wir auf verschiedene Arten entwickeln möchten:
- mithilfe einer klassischen Servlet-Controller-Technologie – Seiten JSP
- mithilfe der Struts-Technologie MVC
- mithilfe der Spring-Technologie MVC
In allen Fällen wird die Anwendung einer MVC-Architektur (Model-View-Controller) folgen. Wenn wir das obige Schichtmodell heranziehen, fügt sich die MVC-Architektur wie folgt ein:
Die Bearbeitung einer Anfrage eines Kunden erfolgt in folgenden Schritten:
- Der Kunde sendet eine Anfrage an den Controller. Dieser Controller ist ein Servlet, über das alle Kundenanfragen laufen. Er ist das Eingangstor der Anwendung. Er entspricht dem C in MVC.
- Der Controller verarbeitet diese Anfrage. Dazu benötigt er möglicherweise Unterstützung durch die Geschäftslogik-Schicht, die in der Struktur MVC als Modell M bezeichnet wird.
- Der Controller erhält eine Antwort von der Geschäftslogik. Die Anfrage des Kunden wurde bearbeitet. Dies kann zu mehreren möglichen Antworten führen. Ein klassisches Beispiel ist
- eine Fehlerseite, falls die Anfrage nicht korrekt verarbeitet werden konnte
- ansonsten eine Bestätigungsseite
- Der Controller wählt die Antwort (= Ansicht) aus, die an den Client gesendet werden soll. Dabei handelt es sich meist um eine Seite mit dynamischen Elementen. Der Controller stellt diese der Ansicht zur Verfügung.
- Die Ansicht wird an den Client gesendet. Dies ist das „V“ in MVC.
3.3. Das Modell
Wir befassen uns hier mit dem M von MVC. Das Modell besteht hier aus folgenden Elementen:
- die Geschäftsklassen
- die Datenzugriffsklassen
- die Datenbank
3.3.1. Die Datenbank
Die Datenbank enthält nur eine Tabelle namens ARTICLES. Diese wurde mit den folgenden Befehlen SQL generiert:
| CREATE TABLE ARTICLES (
ID INTEGER NOT NULL,
NOM VARCHAR(30) NOT NULL,
PRIX NUMERIC(15,2) NOT NULL,
STOCKACTUEL INTEGER NOT NULL,
STOCKMINIMUM INTEGER NOT NULL
);
/* Einschränkungen */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
/* Primärschlüssel */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
|
| Primärschlüssel, der einen Artikel eindeutig identifiziert |
| Name des Artikels |
| sein Preis |
| Aktueller Lagerbestand |
| der Lagerbestand, bei dessen Unterschreitung eine Nachbestellung erfolgen muss |
In den folgenden Tests wurde eine Datenbank mit dem Namen [Firebird] verwendet. [Firebird] ist eine „Open-Source“-Version von SGBD. Der Treiber JDBC [firebirdsql-full.jar] befindet sich im Ordner [WEB-INF/lib] der Webanwendung.
3.3.2. Die Pakete der Vorlage
Das Modell M wird hier in Form von drei Archiven bereitgestellt:
- istia.st.articles.dao: enthält die Klassen für den Datenzugriff auf die Ebene [dao]
- istia.st.articles.exception: enthält eine Ausnahmeklasse für diese Artikelverwaltung
- istia.st.articles.domain: enthält die Fachklassen der Schicht [domain]
archive | contenu | rôle |
istia.st.articles.dao | - enthält das Paket [istia.st.articles.dao], das wiederum folgende Elemente enthält: - [IArticlesDao]: die Schnittstelle für den Zugriff auf die Dao-Schicht. Dies ist die einzige Schnittstelle, die die Schicht [domain] sieht. Sie sieht keine andere. - [Article]: Klasse zur Definition eines Artikels - [ArticlesDaoSqlMap]: Implementierungsklasse der Schnittstelle [IArticlesDao] mit dem Tool SqlMap | Datenzugriffsebene – befindet sich vollständig in der Schicht [dao] der 3-Tier-Architektur der Webanwendung |
istia.st.articles.domain | – enthält das Paket [istia.st.articles.domain], das wiederum folgende Elemente enthält: - [IArticlesDomain]: die Schnittstelle für den Zugriff auf die Schicht [domain]. Dies ist die einzige Schnittstelle, die die Webschicht sieht. Sie sieht keine andere. - [AchatsArticles]: eine Klasse, die [IArticlesDomain] implementiert - [Achat]: Klasse, die den Einkauf eines Kunden repräsentiert - [Panier]: Klasse, die die Gesamtheit der Einkäufe eines Kunden darstellt | stellt das Modell der Online-Einkäufe dar – befindet sich vollständig in der Schicht [domain] der 3-Tier-Architektur der Webanwendung |
istia.st.articles.exception | - enthält das Paket [istia.st.articles.exception], das wiederum folgende Elemente enthält: - [UncheckedAccessArticlesException]: Klasse, die eine Ausnahme vom Typ [RuntimeException] definiert. Dieser Ausnahmetyp wird von der Schicht [dao] ausgelöst, sobald ein Problem beim Datenzugriff auftritt. | |
3.3.3. Das Paket [istia.st.articles.dao]
Die Klasse, die einen Artikel definiert, lautet wie folgt:
| package istia.st.articles.dao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
/**
* @author ST - ISTIA
*
*/
public class Article {
private int id;
private String nom;
private double prix;
private int stockActuel;
private int stockMinimum;
/**
* constructeur par défaut
*/
public Article() {
}
public Article(int id, String nom, double prix, int stockActuel,
int stockMinimum) {
// Instanzattribute initialisieren
setId(id);
setNom(nom);
setPrix(prix);
setStockActuel(stockActuel);
setStockMinimum(stockMinimum);
}
// Getter – Setter
public int getId() {
return id;
}
public void setId(int id) {
// Gültige ID?
if (id < 0)
throw new UncheckedAccessArticlesException("id[" + id + "] invalide");
this.id = id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
// Gültiger Name?
if(nom==null || nom.trim().equals("")){
throw new UncheckedAccessArticlesException("Le nom est [null] ou vide");
}
this.nom = nom;
}
public double getPrix() {
return prix;
}
public void setPrix(double prix) {
// Gültiger Preis?
if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
this.prix = prix;
}
public int getStockActuel() {
return stockActuel;
}
public void setStockActuel(int stockActuel) {
// Gültiger Lagerbestand?
if (stockActuel < 0)
throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
this.stockActuel = stockActuel;
}
public int getStockMinimum() {
return stockMinimum;
}
public void setStockMinimum(int stockMinimum) {
// Gültiger Lagerbestand?
if (stockMinimum < 0)
throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
this.stockMinimum = stockMinimum;
}
public String toString() {
return "[" + id + "," + nom + "," + prix + "," + stockActuel + ","
+ stockMinimum + "]";
}
}
|
Diese Klasse bietet:
- einen Konstruktor, mit dem die 5 Informationen eines Artikels festgelegt werden können
- Zugriffsmethoden, die oft als Getter/Setter bezeichnet werden und zum Lesen und Schreiben der 5 Informationen dienen. Die Namen dieser Methoden entsprechen dem Standard JavaBean. Die Verwendung von Objekten der Klasse JavaBean in der Schicht DAO zur Anbindung an die Daten der Klasse SGBD ist gängige Praxis.
- eine Überprüfung der in den Artikel eingegebenen Daten. Bei fehlerhaften Daten wird eine Ausnahme ausgelöst.
- Eine Methode toString, mit der der Wert eines Artikels als Zeichenkette abgerufen werden kann. Dies ist häufig für die Fehlersuche in einer Anwendung nützlich.
Die Schnittstelle [IArticlesDao] ist wie folgt definiert:
| package istia.st.articles.dao;
import istia.st.articles.domain.Article;
import java.util.List;
/**
* @author ST-ISTIA
*
*/
public interface IArticlesDao {
/**
* @return : liste de tous les articles
*/
public List getAllArticles();
/**
* @param unArticle :
* l'article à ajouter
*/
public int ajouteArticle(Article unArticle);
/**
* @param idArticle :
* id de l'article à supprimer
*/
public int supprimeArticle(int idArticle);
/**
* @param unArticle :
* l'article à modifier
*/
public int modifieArticle(Article unArticle);
/**
* @param idArticle :
* id de l'article cherché
* @return : l'article trouvé ou null
*/
public Article getArticleById(int idArticle);
/**
* vide la table des articles
*/
public void clearAllArticles();
/**
*
* @param idArticle id de l'article dont on change le stock
* @param mouvement valeur à ajouter au stock (valeur signée)
*/
public int changerStockArticle(int idArticle, int mouvement);
}
|
Die verschiedenen Methoden der Schnittstelle haben folgende Funktionen:
| gibt alle Artikel aus der Tabelle ARTICLES in einer Liste von Objekten [Article] zurück |
| leert die Tabelle ARTICLES |
| gibt das Objekt [Article] zurück, das durch seinen Primärschlüssel identifiziert wird |
| ermöglicht das Hinzufügen eines Artikels zur Tabelle ARTICLES |
| ermöglicht das Ändern eines Artikels in der Tabelle [ARTICLES] |
| ermöglicht das Löschen eines Artikels aus der Tabelle [ARTICLES] |
| ermöglicht die Änderung des Bestands eines Artikels in der Tabelle [ARTICLES] |
Die Schnittstelle stellt Client-Programmen eine Reihe von Methoden zur Verfügung, die ausschließlich durch ihre Signaturen definiert sind. Sie kümmert sich nicht darum, wie diese Methoden tatsächlich implementiert werden. Dies sorgt für Flexibilität in einer Anwendung. Das Client-Programm ruft die Schnittstelle auf und nicht eine bestimmte Implementierung davon.
Die Auswahl einer bestimmten Implementierung erfolgt über eine Spring-Konfigurationsdatei. Wir schlagen vor, hier die Schnittstelle IArticlesDao mithilfe eines Open-Source-Produkts namens SqlMap zu implementieren. Damit können wir alle Anweisungen SQL aus dem Java-Code entfernen.
Die Implementierungsklasse [ArticlesDaoSqlMap] ist wie folgt definiert:
| package istia.st.articles.dao;
// Importe
import com.ibatis.sqlmap.client.SqlMapClient;
import istia.st.articles.domain.Article;
import java.util.List;
public class ArticlesDaoSqlMap implements IArticlesDao {
// Felder
private SqlMapClient sqlMap;
// Konstruktoren
public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }
// Methoden
public SqlMapClient getSqlMap() {}
public void setSqlMap(SqlMapClient sqlMap) { }
public synchronized List getAllArticles() {}
public synchronized int ajouteArticle(Article unArticle) {}
public synchronized int supprimeArticle(int idArticle) {}
public synchronized int modifieArticle(Article unArticle) {}
public synchronized Article getArticleById(int idArticle) {}
public synchronized void clearAllArticles() { }
public synchronized int changerStockArticle(int idArticle, int mouvement) {}
}
|
Alle Methoden für den Datenzugriff wurden synchronisiert, um Probleme durch gleichzeitigen Zugriff auf die Datenquelle zu vermeiden. Zu einem bestimmten Zeitpunkt hat jeweils nur ein Thread Zugriff auf eine bestimmte Methode.
Die Klasse [ArticlesDaoSqlMap] nutzt das Tool [Ibatis SqlMap]. Der Vorteil dieses Tools besteht darin, dass es ermöglicht, den Datenzugriffscode SQL aus dem Java-Code auszulagern. Dieser wird dann in eine Konfigurationsdatei geschrieben. Wir werden später noch darauf zurückkommen. Zum Erstellen benötigt die Klasse [ArticlesDaoSqlMap] eine Konfigurationsdatei, deren Name als Parameter an den Konstruktor der Klasse übergeben wird. Diese Konfigurationsdatei definiert die erforderlichen Informationen, um:
- den Zugriff auf die Datenbank SGBD, in der sich die Artikel befinden
- die Verwaltung eines Verbindungspools
- die Verwaltung von Transaktionen
In unserem Beispiel heißt sie [sqlmap-config-firebird.xml] und definiert den Zugriff auf eine Firebird-Datenbank:
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<transactionManager type="JDBC">
<dataSource type="SIMPLE">
<property name="JDBC.Driver" value="org.firebirdsql.jdbc.FBDriver"/>
<property name="JDBC.ConnectionURL"
value="jdbc:firebirdsql:localhost/3050:D:/data/Databases/firebird/dbarticles.gdb"/>
<property name="JDBC.Username" value="sysdba"/>
<property name="JDBC.Password" value="masterkey"/>
<property name="JDBC.DefaultAutoCommit" value="true"/>
</dataSource>
</transactionManager>
<sqlMap resource="articles.xml"/>
</sqlMapConfig>
|
Die oben genannte Konfigurationsdatei [articles.xml] ermöglicht es, festzulegen, wie eine Instanz der Klasse [istia.st.articles.dao.Article] aus einer Zeile der Tabelle [ARTICLES] des SGBD erstellt wird. Außerdem definiert sie die Abfragen SQL, mit denen die Schicht [dao] die Daten aus der Firebird-Datenquelle abrufen kann.
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">
<!-- ein Alias für die Klasse istia.st.articles.dao.Article -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- das Mapping OR M: Tabellenzeile ARTICLES – Instanz der Klasse „Artikel“ -->
<resultMap id="article" class="article">
<result property="id" column="ID"/>
<result property="nom" column="NOM"/>
<result property="prix" column="PRIX"/>
<result property="stockActuel" column="STOCKACTUEL"/>
<result property="stockMinimum" column="STOCKMINIMUM"/>
</resultMap>
<!-- die Abfrage SQL zum Abrufen aller Artikel -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- die Abfrage SQL zum Löschen aller Artikel -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- die Abfrage SQL zum Einfügen eines Artikels -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#(id#,#name#,#preis#,#aktuellerBestand#,#Mindestbestand#)
</statement>
<!-- die Abfrage SQL zum Löschen eines bestimmten Artikels -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- die Abfrage SQL zum Ändern eines bestimmten Artikels -->
<statement id="modifyArticle">
update ARTICLES set nom=#Name#,
prix=#Preis#,aktuellerBestand=#aktuellerBestand#,Mindestbestand=#Mindestbestand# where
id=#id#
</statement>
<!-- die Abfrage SQL zum Abrufen eines bestimmten Artikels -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- die Abfrage SQL zum Ändern des Bestands eines bestimmten Artikels -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#Bewegung#
where id=#id# und stockActuel+#Bewegung#>=0
</statement>
</sqlMap>
|
Der Code des Pakets [dao] ist im Anhang zu finden.
3.3.4. Das Paket [istia.st.articles.domain]
Die Schnittstelle [IArticlesDomain] entkoppelt die Schicht [métier] von der Schicht [web]. Letztere greift über diese Schnittstelle auf die Schicht [métier/domain] zu, ohne sich um die Klasse zu kümmern, die sie tatsächlich implementiert. Die Schnittstelle definiert die folgenden Aktionen für den Zugriff auf die Geschäftsschicht:
| package istia.st.articles.domain;
// Eingänge
import java.util.ArrayList;
import java.util.List;
public abstract interface IArticlesDomain {
// Methoden
void acheter(Panier panier);
List getAllArticles();
Article getArticleById(int idArticle);
ArrayList getErreurs();
}
|
| gibt die Objektliste [Article] zurück, die dem Kunden angezeigt werden soll |
Article getArticleById(int idArticle)
| gibt das durch [idArticle] identifizierte Objekt [Article] zurück |
void acheter(Panier panier)
| validiert den Warenkorb des Kunden, indem die Bestände der gekauften Artikel um die gekaufte Menge verringert werden – kann fehlschlagen, wenn der Bestand nicht ausreicht |
| gibt die Liste der aufgetretenen Fehler zurück – leer, wenn keine Fehler vorliegen |
Hier wird die Schnittstelle [IArticlesDomain] durch die folgende Klasse [AchatsArticles] implementiert:
| package istia.st.articles.domain;
// Importe
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.List;
public class AchatsArticles implements IArticlesDomain {
// Felder
private IArticlesDao articlesDao;
private ArrayList erreurs;
// Konstruktoren
public AchatsArticles(IArticlesDao articlesDao) { }
// Methoden
public ArrayList getErreurs() {}
public List getAllArticles() {}
public Article getArticleById(int id) {}
public void acheter(Panier panier) { }
}
|
Diese Klasse implementiert die vier Methoden der Schnittstelle [IArticlesDomain]. Sie verfügt über zwei private Felder:
| das von der Datenzugriffsebene bereitgestellte Datenzugriffsobjekt |
| die Liste eventueller Fehler |
Um eine Instanz der Klasse zu erstellen, muss das Objekt bereitgestellt werden, das den Zugriff auf die Daten von SGBD ermöglicht:
public AchatsArticles(IArticlesDao articlesDao)
| Konstruktor |
Die Klasse [Achat] repräsentiert einen Kauf des Kunden:
| package istia.st.articles.domain;
public class Achat {
// Felder
private Article article;
private int qte;
// Konstruktoren
public Achat(Article article, int qte) { }
// Methoden
public double getTotal() {}
public Article getArticle() {}
public void setArticle(Article article) { }
public int getQte() {}
public void setQte() { }
public String toString() {}
}
|
Die Klasse [Achat] ist eine JavaBean mit den folgenden Feldern und Methoden:
| der gekaufte Artikel |
| die gekaufte Menge |
| gibt den Kaufbetrag an |
| Identitätszeichenfolge des Objekts |
Die Klasse [Panier] repräsentiert alle Einkäufe des Kunden:
| package istia.st.articles.domain;
// Importe
import java.util.ArrayList;
public class Panier {
// Felder
private ArrayList achats;
// Konstruktoren
public Panier() { }
// Methoden
public ArrayList getAchats() {}
public void ajouter(Achat unAchat) { }
public void enlever(int idAchat) { }
public double getTotal() {}
public String toString() { }
}
|
Die Klasse [Panier] ist eine JavaBean mit den folgenden Feldern und Methoden:
| die Liste der Einkäufe des Kunden – Liste von Objekten vom Typ [Achat] |
void ajouter(Achat unAchat)
| Fügt einen Einkauf zur Einkaufsliste hinzu |
void enlever(int idArticle)
| Entfernt den Kauf des Artikels idArticle |
| berechnet den Gesamtbetrag der Einkäufe |
| gibt die Identitätszeichenfolge des Warenkorbs zurück |
| gibt die Liste der Einkäufe zurück |
Der Code des Pakets [domain] ist im Anhang zu finden.
3.3.5. Das Paket [istia.st.articles.exception]
Dieses Paket enthält die Klasse, die die Ausnahme definiert, die von der Schicht [dao] ausgelöst wird, wenn ein Problem beim Zugriff auf die Datenquelle auftritt:
| package istia.st.articles.exception;
public class UncheckedAccessArticlesException
extends RuntimeException {
public UncheckedAccessArticlesException() {
super();
}
public UncheckedAccessArticlesException(String mesg) {
super(mesg);
}
public UncheckedAccessArticlesException(String mesg, Throwable th) {
super(mesg, th);
}
}
|
3.3.6. Tests des Modells
Das Modell M wurde unter Eclipse mit folgender Konfiguration getestet:

Anmerkungen:
- In [WEB-INF/lib] befinden sich:
- die für das Tool [ibatis SqlMap] erforderlichen Archive, das für den Zugriff auf die Firebird-Datenbank SGBD zuständig ist: ibatis-*.jar
- die für das Tool [spring] erforderliche: spring.jar
- den Treiber JDBC für SGBD und [Firebird]: firebirdsql-full.jar
- die für die Protokolle erforderlichen Archive: log4-*.jar, commons-logging.jar
- die drei Archive des getesteten Modells: istia.st.articles.*.jar
- das für das Testtool erforderliche Archiv: [junit]
- In [WEB-INF/src] befinden sich die Konfigurationsdateien, die von Eclipse automatisch in [WEB-INF/classes] kopiert werden:
- die Konfigurationsdateien des Tools [sqlmap]: sqlmap-config-firebird.xml, articles.xml
- die des Tools [spring]: spring-config-test-dao.xml, spring-config-test-domain.xml
- die Konfigurationsdatei des Tools [log4j]: log4j.properties
- Im Paket [istia.st.articles.tests] befinden sich die Testklassen des Modells
3.3.6.1. Tests der Schicht [dao]
Die Testklasse JUnit der Schicht [dao] sieht wie folgt aus. Anhand ihres Inhalts lässt sich nachvollziehen, wie die Methoden der Schnittstelle [IArticlesDao] verwendet werden:
| package istia.st.articles.tests.dao;
import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.dao.Article;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
// Test der Klasse ArticlesDaoSqlMap
public class JunitModeleDaoArticles extends TestCase {
// eine Instanz der getesteten Klasse
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// Ruft eine Instanz für den Datenzugriff ab
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-dao.xml"))).getBean("articlesDao");
}
public void testGetAllArticles() {
// zeigt die Artikel an
listArticles();
}
public void testClearAllArticles() {
// leert die Artikeltabelle
articlesDao.clearAllArticles();
// liest die Tabelle ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
}
public void testAjouteArticle() {
// Inhalt von ARTICLES löschen
articlesDao.clearAllArticles();
// liest die Tabelle ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// Einfügen
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// liest die Tabelle ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
//zeigt sie an
listArticles();
}
public void testSupprimeArticle() {
// Löschung des Inhalts von ARTICLES
articlesDao.clearAllArticles();
// liest die Tabelle ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// Einfügen
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// liest die Tabelle ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// Löschen
articlesDao.supprimeArticle(4);
// liest die Tabelle ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(1, articles.size());
// zeigt die Tabelle an
listArticles();
}
public void testModifieArticle() {
// Löschung des Inhalts von ARTICLES
articlesDao.clearAllArticles();
// liest die Tabelle ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// Einfügen
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// liest die Tabelle ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
// Änderung
articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
// getById
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getPrix(), 44, 1e-6);
// zeigt die Tabelle an
listArticles();
}
public void testGetArticleById() {
// Löschung des Inhalts von ARTICLES
articlesDao.clearAllArticles();
// liest die Tabelle ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// Einfügen
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// liest die Tabelle ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
private void listArticles() {
// liest die Tabelle ARTICLES
List articles = articlesDao.getAllArticles();
// Die gelesenen Artikel werden angezeigt
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
public void testChangerStockArticle() throws InterruptedException {
// Löschung des Inhalts von ARTICLES
articlesDao.clearAllArticles();
// Einfügen
int nbArticles = articlesDao.ajouteArticle(new Article(3, "article3",
30, 101, 3));
assertEquals(nbArticles, 1);
nbArticles = articlesDao.ajouteArticle(new Article(4, "article4", 40,
40, 4));
assertEquals(nbArticles, 1);
// Erstellung von 100 Threads zur Aktualisierung des Lagerbestands von Artikel 3
Thread[] taches = new Thread[100];
for (int i = 0; i < taches.length; i++) {
taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
taches[i].start();
}
// Warten auf das Ende der Threads
for (int i = 0; i < taches.length; i++) {
taches[i].join();
}
// Artikel 3 abrufen und dessen Lagerbestand prüfen
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
assertEquals(1, unArticle.getStockActuel());
// Bestand von Artikel 4 ändern
boolean erreur = false;
int nbLignes = articlesDao.changerStockArticle(4, -100);
assertEquals(0, nbLignes);
// Zeigt die Tabelle an
listArticles();
}
}
|
Anmerkungen:
- Die Testklasse speichert mithilfe ihrer Methode setUp eine Instanz der zu testenden Klasse:
| // eine Instanz der getesteten Klasse
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// Ruft eine Instanz für den Datenzugriff ab
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-dao.xml"))).getBean("articlesDao");
}
|
- Das zu testende Objekt wird von [Spring] bereitgestellt. Oben wird die Spring-Bean mit dem Namen [articlesDao] angefordert. Diese Bean ist in der Spring-Konfigurationsdatei [spring-config-test-dao.xml] definiert:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- die Datenzugriffsklasse -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
</beans>
|
Wie oben zu sehen ist, handelt es sich bei der Bean „[articlesDao]“ um eine Instanz der Klasse „[istia.st.articles.dao.ArticlesDaoSqlMap]“. Diese Klasse verfügt über einen Konstruktor, der als Parameter den Namen der Konfigurationsdatei des Tools „[SqlMap]“ erwartet. Dieser Name wird hier angegeben. Es handelt sich um [sqlmap-config-firebird.xml]. Letzteres wurde bereits beschrieben. Es liefert alle notwendigen Informationen für den Zugriff auf die Daten von SGBD.
Die Methode [testChangerStockArticle] erstellt 100 Threads, die den Lagerbestand eines bestimmten Artikels verringern sollen. Hier geht es darum, den gleichzeitigen Zugriff auf SGBD zu testen. Da die Methode [changerStockArticle] der Klasse [istia.st.articles.dao.ArticlesDaoSqlMap] synchronisiert wurde, besteht dieser Test. Entfernt man die Synchronisation, besteht er nicht mehr. Die Klasse, die für die Aktualisierung des Bestands zuständig ist, lautet wie folgt:
| package istia.st.articles.tests;
import istia.st.articles.dao.IArticlesDao;
public class ThreadMajStock extends Thread {
/**
* nom du thread
*/
private String name;
/**
* objet d'accès aux données
*/
private IArticlesDao articlesDao;
/**
*
* @param name
* le nom du thread afin de l'identifier
* @param articlesDao
* l'objet d'accès aux données du sgbd
*/
public ThreadMajStock(String name, IArticlesDao articlesDao) {
this.name = name;
this.articlesDao = articlesDao;
}
/**
* décrémente le stock de l'article 3 d'une unité fait un suivi écran des
* opérations
*/
public void run() {
// Weiterverfolgung
System.out.println(name + " lancé");
// Bestandsänderung Artikel 3
articlesDao.changerStockArticle(3, -1);
// Nachverfolgung
System.out.println(name + " terminé");
}
}
|
- Die oben genannte Klasse verringert den Bestand des Artikels Nr. 3 um 1
3.3.6.2. Tests der Ebene [domain]
Die Testklasse JUnit der Schicht [domain] lautet wie folgt:
| package istia.st.articles.tests.domain;
import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.Article;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
// Test der Klasse ArticlesDaoSqlMap
public class JunitModeleDomainArticles extends TestCase {
// eine Instanz der Domänenzugriffsklasse
private IArticlesDomain articlesDomain;
// eine Instanz der Datenzugriffsklasse
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// ruft eine Instanz des Domänenzugriffs ab
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
// ruft eine Instanz des Datenzugriffs ab
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
}
// Abruf eines bestimmten Artikels
public void testGetArticleById() {
// Löschung des Inhalts von ARTICLES
articlesDao.clearAllArticles();
// liest die Tabelle ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// Einfügen
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// liest die Tabelle ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDomain.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
// Bildschirmanzeige
private void listArticles() {
// liest die Tabelle ARTICLES
List articles = articlesDomain.getAllArticles();
// die gelesenen Artikel werden angezeigt
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
// Artikel kaufen
public void testAchatPanier(){
// Löschen des Inhalts von ARTICLES
articlesDao.clearAllArticles();
// liest die Tabelle ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// Einfügen
Article article3=new Article(3, "article3", 30, 30, 3);
articlesDao.ajouteArticle(article3);
Article article4=new Article(4, "article4", 40, 40, 4);
articlesDao.ajouteArticle(article4);
// liest die Tabelle ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// Erstellung eines Warenkorbs mit zwei Artikeln
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// Prüfungen
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// Warenkorb bestätigen
articlesDomain.acheter(panier);
// Prüfungen
assertEquals(0,articlesDomain.getErreurs().size());
assertEquals(0,panier.getAchats().size());
// Artikel Nr. 3 suchen
article3=articlesDomain.getArticleById(3);
assertEquals(20,article3.getStockActuel());
// Artikel Nr. 4 suchen
article4=articlesDomain.getArticleById(4);
assertEquals(30,article4.getStockActuel());
// Neuer Warenkorb
panier.ajouter(new Achat(article3,100));
// Warenkorb bestätigen
articlesDomain.acheter(panier);
// Überprüfung – zu viele Artikel gekauft
// Es muss ein Fehler vorliegen
assertEquals(1,articlesDomain.getErreurs().size());
// Artikel Nr. 3 suchen
article3=articlesDomain.getArticleById(3);
// Der Lagerbestand hat sich wohl nicht geändert
assertEquals(20,article3.getStockActuel());
}
// Einkäufe stornieren
public void testRetirerAchats(){
// Inhalt von ARTICLES löschen
articlesDao.clearAllArticles();
// liest die Tabelle ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// Einfügen
Article article3=new Article(3, "article3", 30, 30, 3);
articlesDao.ajouteArticle(article3);
Article article4=new Article(4, "article4", 40, 40, 4);
articlesDao.ajouteArticle(article4);
// liest die Tabelle ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// Erstellung eines Warenkorbs mit zwei Artikeln
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// Prüfungen
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// einen bereits gekauften Artikel hinzufügen
panier.ajouter(new Achat(article3,10));
// Überprüfungen
// Der Gesamtbetrag muss auf 1000 geändert werden
assertEquals(1000.0,panier.getTotal(),1e-6);
// Immer noch 2 Artikel im Warenkorb
assertEquals(2,panier.getAchats().size());
// Die Stückzahl von Artikel 3 muss auf 20 geändert werden
Achat achat=(Achat)panier.getAchats().get(0);
assertEquals(20,achat.getQte());
// Artikel 3 wird aus dem Warenkorb entfernt
panier.enlever(3);
// Überprüfungen
// Der Gesamtbetrag muss auf 400 geändert werden
assertEquals(400.0,panier.getTotal(),1e-6);
// Nur 1 Artikel im Warenkorb
assertEquals(1,panier.getAchats().size());
// Es muss sich um Artikel Nr. 4 handeln
assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
}
}
|
Anmerkungen:
- Die Testklasse speichert mithilfe ihrer Methode setUp eine Instanz der zu testenden Klasse sowie eine Instanz der Datenzugriffsklasse. Dieser letzte Punkt ist umstritten. Theoretisch sollte die Testklasse keinen Zugriff auf die Schicht [dao] benötigen, die sie gar nicht kennen dürfte. Hier haben wir diese „Ethik“ außer Acht gelassen, deren Einhaltung uns dazu gezwungen hätte, neue Methoden in unserer Schnittstelle [IArticlesDomain] zu erstellen.
| // eine Instanz der Domänenzugriffsklasse
private IArticlesDomain articlesDomain;
// eine Instanz der Klasse für den Datenzugriff
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// ruft eine Instanz für den Domänenzugriff ab
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
// ruft eine Instanz der Datenzugriffsklasse ab
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
}
|
- Das zu testende Objekt wird von [Spring] bereitgestellt. Oben wird die Spring-Bean mit dem Namen [articlesDomain] angefordert. Diese Bean ist in der Spring-Konfigurationsdatei [spring-config-test-domain.xml] definiert:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- die Datenzugriffsklasse -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- die Fachklasse -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
Wie oben zu sehen ist, handelt es sich bei der Bean [articlesDomain] um eine Instanz der Klasse [istia.st.articles.domain.AchatsArticles]. Diese Klasse verfügt über einen Konstruktor, der als Parameter ein Zugriffsobjekt auf die Schicht [dao] vom Typ [IArticlesDao] erwartet. Hier gibt die Konfigurationsdatei an, dass es sich bei diesem Objekt um die Bean mit dem Namen [articlesDao] handelt. Dies veranlasst Spring, diese Bean zu instanziieren. Die Instanziierung der Bean [articlesDao] wurde bereits zuvor erläutert. Letztendlich wurden also zwei Beans instanziiert:
- [articlesDao] vom Typ [istia.st.articles.dao.ArticlesDaoSqlMap]
- [articlesDomain] vom Typ [ istia.st.articles.domain.AchatsArticles]
Diese beiden Instanziierungen werden durch den ersten Aufruf von Spring ausgelöst:
| // ruft eine Instanz für den Domänenzugriff ab
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
|
Dabei wird die Bean [articlesDomain] abgerufen. Beim zweiten Aufruf von Spring:
| // ruft eine Instanz des Datenzugriffs ab
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
|
gibt [Spring] lediglich eine Referenz auf die Bean [articlesDao] zurück, die bereits beim vorherigen Aufruf erstellt wurde. Das ist das Prinzip des Singletons. Wenn man eine Bean von Spring anfordert, instanziiert Spring sie, falls sie noch nicht existiert; andernfalls gibt Spring eine Referenz auf die bereits vorhandene Bean zurück.
3.4. Dreischichtige Webanwendung MVC
Im Folgenden wollen wir die folgende dreischichtige Webanwendung erstellen:
Die Anwendung wird eine Architektur MVC aufweisen. Das Modell M wurde bereits geschrieben und getestet. Es handelt sich um das zuvor beschriebene Modell. Es wird uns in drei Archiven [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception] zur Verfügung gestellt. Wir müssen nun den Controller C und die Views V schreiben.
Betrachten wir zunächst einen klassischen Ansatz, bei dem:
- der Controller C durch ein einziges Servlet bereitgestellt wird
- die Ansichten V durch Seiten JSP bereitgestellt werden
3.5. MVC-Architektur auf Basis eines Controller-Servlets und von JSP-Seiten
Die Architektur MVC der Anwendung sieht wie folgt aus:
| die Fachklassen, die Datenzugriffsklassen und die Datenbank |
| die Seiten JSP |
| das Servlet zur Verarbeitung von Client-Anfragen |
3.5.1. Das Modell
Es wurde bereits zuvor vorgestellt. Es besteht aus den Java-Archiven [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.5.2. Die Ansichten
Die Ansichten entsprechen denen, die zu Beginn des Dokuments vorgestellt wurden:
| liste.jsp | Die Ansichten sind im Ordner [vues] der Anwendung zusammengefasst  |
| infos.jsp |
| panier.jsp |
| paniervide.jsp |
| erreurs.jsp |
3.5.3. Der Controller
Der Controller besteht aus einem einzigen Servlet namens [WebArticles]. Er verarbeitet die verschiedenen Anfragen der Clients. Diese werden durch das Vorhandensein eines Parameters [action] in der Anfrage HTTP des Clients formalisiert:
| Bedeutung | Aktion des Controllers | Mögliche Antworten |
| Der Kunde möchte die Liste der Artikel | – fordert die Liste der Artikel bei der an | – [LISTE] - [ERREURS] |
| Der Kunde fragt Informationen zu einem der in der Ansicht angezeigten Artikel [LISTE] | - fordert den Artikel bei der Geschäftsebene an | - [INFOS] - [ERREURS] |
| Der Kunde kauft einen Artikel | - fordert den Artikel bei der Geschäftsebene an und legt ihn in den Warenkorb des Kunden | - [INFOS] bei Mengenfehler - [LISTE], wenn kein Fehler vorliegt |
| Der Kunde möchte einen Artikel aus seinem Warenkorb entfernen | - ruft den Warenkorb aus der Sitzung ab und ändert ihn | - [PANIER] - [PANIERVIDE] - [ERREURS] |
| Der Kunde möchte seinen Warenkorb | - ruft den Warenkorb aus der Sitzung ab | - [PANIER] - [PANIERVIDE] - [ERREURS] |
| Der Kunde hat seinen Einkauf abgeschlossen und wechselt zur Zahlungsphase | - aktualisiert in der Datenbank die Bestände der gekauften Artikel - entfernt aus dem Warenkorb des Kunden die Artikel, deren deren Kauf bestätigt wurde | - [LISTE] - [ERREURS] |
3.5.4. Konfiguration der Anwendung
Wir werden versuchen, die Anwendung so zu konfigurieren, dass sie gegenüber Änderungen wie den folgenden so flexibel wie möglich ist:
- Änderung der URLs der verschiedenen Ansichten
- Änderung der Klassen, die die Schnittstellen [IArticlesDao] und [IArticlesDomain] implementieren
- Änderung von SGBD, der Datenbank und der Artikeltabelle
3.5.5. Änderungen der URLs
Die Namen der URLs der Ansichten werden zusammen mit einigen weiteren Parametern in der Anwendungskonfigurationsdatei [web.xml] gespeichert:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>webarticles</servlet-name>
<servlet-class>istia.st.articles.web.WebArticles</servlet-class>
<init-param>
<param-name>springConfigFileName</param-name>
<param-value>spring-config-sqlmap-firebird.xml</param-value>
</init-param>
<init-param>
<param-name>urlMain</param-name>
<param-value>/main</param-value>
</init-param>
<init-param>
<param-name>urlErreurs</param-name>
<param-value>/vues/erreurs.jsp</param-value>
</init-param>
<init-param>
<param-name>urlListe</param-name>
<param-value>/vues/liste.jsp</param-value>
</init-param>
<init-param>
<param-name>urlInfos</param-name>
<param-value>/vues/infos.jsp</param-value>
</init-param>
<init-param>
<param-name>urlPanier</param-name>
<param-value>/vues/panier.jsp</param-value>
</init-param>
<init-param>
<param-name>urlPanierVide</param-name>
<param-value>/vues/paniervide.jsp</param-value>
</init-param>
<init-param>
<param-name>urlDebug</param-name>
<param-value>/vues/debug.jsp</param-value>
</init-param>
</servlet>
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
<servlet-mapping>
<servlet-name>webarticles</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
</web-app>
|
In der Datei [web.xml]
- die URLs der verschiedenen Ansichten der Anwendung
- der Name [springConfigFileName] der Spring-Konfigurationsdatei, die die Erstellung von Singleton-Objekten für den Zugriff auf die Geschäftslogik- und DAO-Schichten ermöglicht
- die Ansicht [/vues/index.jsp], die angezeigt wird, wenn die vom Client angeforderte URL /<context> lautet, wobei <context> der Kontext der Anwendung ist
3.5.6. Die Änderung der Klassen zur Implementierung der Schnittstellen
Im Sinne der dreischichtigen Architektur müssen die Schichten voneinander isoliert sein. Diese Isolation wird wie folgt erreicht:
- Die Schichten kommunizieren untereinander über Schnittstellen und nicht über konkrete Klassen
- Der Code einer Schicht instanziiert niemals selbst die Klasse einer anderen Schicht, um sie zu verwenden. Er fordert lediglich von einem externen Tool – in diesem Fall [Spring] – eine Instanz der Implementierung der Schnittstelle der Schicht an, die er verwenden möchte. Dazu muss sie, wie wir wissen, nicht den Namen der Implementierungsklasse kennen, sondern lediglich den Namen des Spring-Beans, auf das sie verweisen möchte.
In unserer Anwendung könnte die Spring-Konfigurationsdatei wie folgt aussehen:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- die Datenzugriffsklasse -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- die Geschäftsklasse -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
Um Zugriff auf die Schicht [métier] zu erhalten, kann eine Klasse der Schicht [Interface utilisateur, UI] den Bean [articlesDomain] anfordern. Spring instanziiert dann ein Objekt vom Typ [istia.st.articles.domain.AchatsArticles]. Für diese Instanziierung benötigt es ein Bean vom Typ [articlesDao], d. h. ein Objekt vom Typ [istia.st.articles.dao.ArticlesDaoSqlMap]. Spring instanziiert dann ein solches Objekt. Diese Instanziierung erfolgt anhand der Informationen in der Datei [sqlmap-config-firebird.xml], einer Konfigurationsdatei für den Datenzugriff durch SqlMap. Am Ende des Vorgangs verfügt die Klasse [UI], die den Bean [articlesDomain] angefordert hat, über die gesamte Kette, die sie mit den Daten von SGBD verbindet:
3.5.7. Änderungen im Zusammenhang mit SGBD oder der Datenbank
Die Unabhängigkeit der Webanwendung von Änderungen im Zusammenhang mit SGBD oder der Datenbank wird hier durch die Konfigurationsdateien von SqlMap gewährleistet. Es gibt zwei davon:
- die Datei [sql-map-config-firebird.xml]
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<transactionManager type="JDBC">
<dataSource type="SIMPLE">
<property name="JDBC.Driver" value="org.firebirdsql.jdbc.FBDriver"/>
<property name="JDBC.ConnectionURL"
value="jdbc:firebirdsql:localhost/3050:d:/data/databases/firebird/dbarticles.gdb"/>
<property name="JDBC.Username" value="sysdba"/>
<property name="JDBC.Password" value="masterkey"/>
<property name="JDBC.DefaultAutoCommit" value="true"/>
</dataSource>
</transactionManager>
<sqlMap resource="articles.xml"/>
</sqlMapConfig>
|
Diese Datei verweist hier auf eine Firebird-Datenbank. Es reicht aus, den Namen des Treibers JDBC zu ändern, um mit einem anderen Treiber, z. B. SGBD, zu arbeiten.
- die Datei [articles.xml], die die verschiedenen für die Anwendung erforderlichen Anweisungen SQL enthält:
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">
<!-- ein Alias für die Klasse istia.st.articles.dao.Article -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- das Mapping OR M: Tabellenzeile ARTICLES – Instanz der Klasse „Artikel“ -->
<resultMap id="article" class="article">
<result property="id" column="ID"/>
<result property="nom" column="NOM"/>
<result property="prix" column="PRIX"/>
<result property="stockActuel" column="STOCKACTUEL"/>
<result property="stockMinimum" column="STOCKMINIMUM"/>
</resultMap>
<!-- die Abfrage SQL zum Abrufen aller Artikel -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- die Abfrage SQL zum Löschen aller Artikel -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- die Abfrage SQL zum Einfügen eines Artikels -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#(id#,#name#,#preis#,#aktuellerBestand#,#Mindestbestand#)
</statement>
<!-- die Abfrage SQL zum Löschen eines bestimmten Artikels -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- die Abfrage SQL zum Ändern eines bestimmten Artikels -->
<statement id="modifyArticle">
update ARTICLES set nom=#Name#,
prix=#Preis#,aktuellerBestand=#aktuellerBestand#,Mindestbestand=#Mindestbestand# where
id=#id#
</statement>
<!-- die Abfrage SQL zum Abrufen eines bestimmten Artikels -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- die Abfrage SQL zum Ändern des Bestands eines bestimmten Artikels -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#Bewegung#
where id=#id# und stockActuel+#Bewegung#>=0
</statement>
</sqlMap>
|
Sollten sich die Namen der Artikeltabelle oder der Spalten ändern, müssten die Abfragen in dieser Konfigurationsdatei neu geschrieben werden, ohne dass der Java-Code geändert werden müsste. Dies wäre auch der Fall, wenn eine Abfrage aus Leistungsgründen durch eine gespeicherte Prozedur ersetzt würde.
3.5.8. Die Gesamtarchitektur der Anwendung [webarticles]
Eine Java-Webanwendung ist wie ein Puzzle mit vielen Teilen. Durch die Einführung einer MVC-Architektur erhöht sich in der Regel die Anzahl dieser Teile. Die Struktur der Anwendung [webarticles] unter [eclipse] sieht wie folgt aus:
structure générale - on voit ci-dessous les
archives Java utilisées par le projet
Eclipse.
spring : pour Spring
ibatis : pour SqlMap
log4j, commons-logging : pour les logs
de Spring et Sqlmap
firebird : pour le SGBD firebird
mysql : pour le SGBD MySQL
jstl, standard : pour la bibliothèque
de balises JSTL
|
le dossier des sources java : contient le code Java
ainsi que les fichiers de configuration
spring et sqlmap. Eclipse recopie
automatiquement ces fichiers
dans [WEB-INF/classes].
C'est là que l'application les trouvera.
|
 |  |
le dossier [WEB-INF] de l'application : contient le
descripteur [web.xml] de l'application ainsi que le
fichiers de définition de la bibliothèques JSTL
| |
 |  |
3.5.9. Die Ansichten JSP
Die Ansichten JSP verwenden die Tag-Bibliothek JSTL.
3.5.9.1. entete.jsp
Um den verschiedenen Ansichten eine gewisse Einheitlichkeit zu verleihen, werden diese denselben Kopfbereich verwenden, in dem der Name der Anwendung zusammen mit dem Menü angezeigt wird:
Das Menü ist dynamisch und wird vom Controller festgelegt. Dieser fügt in die an die Seite JSP übermittelte Anfrage ein Schlüsselattribut „actions“ ein, dessen zugehöriger Wert ein Array vom Typ Hastable[] ist. Jedes Element dieses Arrays ist ein Dictionary, das dazu dient, eine Option des Kopfmenüs zu generieren. Jedes Dictionary hat zwei Schlüssel:
- href: die mit der Menüoption verknüpfte URL
- lien: der Text der Menüoption
Die anderen Ansichten der Anwendung verwenden die durch [entete.jsp] definierte Kopfzeile mithilfe des folgenden Tags JSP:
<jsp:include page="entete.jsp"/>
Bei der Ausführung bewirkt dieses Tag, dass der Code der Seite JSP, die es enthält, den Code der Seite [entete.jsp] einbindet. Da es sich bei der URL der Seite um eine relative URL handelt (ohne „/“), wird die Seite „[entete.jsp]“ im selben Ordner gesucht wie die Seite, die das Tag <jsp:include> enthält.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>webarticles</title>
</head>
<body>
<table>
<tr>
<td><h2>Magasin virtuel</h2></td>
<c:forEach items="${actions}" var="action">
<td>|</td>
<td><a href="<c:out value="${action.href}"/>"><c:out value="${action.lien}"/></a></td>
</c:forEach>
</tr>
</table>
<hr>
|
3.5.9.2. liste.jsp
Diese Ansicht zeigt die Liste der zum Verkauf stehenden Artikel an:
Sie wird nach einer Anfrage an /main?action=liste oder /main?action=validationpanier angezeigt. Die Elemente der Controller-Anfrage lauten wie folgt:
| Objekt „Hashtable[]“ – das Array der Menüoptionen |
| ArrayList Objekte vom Typ [Article] |
| String-Objekt – am Seitenende anzuzeigende Meldung |
Jeder Link [Infos] im Array HTML der Artikel hat eine URL der Form [?action=infos&id=ID], wobei ID das ID-Feld des angezeigten Artikels ist.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Liste des articles</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th>
</tr>
<c:forEach var="article" items="${listarticles}">
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><a href="<c:out value="?action=infos&id=${article.id}"/>">Infos</a></td>
</tr>
</c:forEach>
</table>
<p>
<c:out value="${message}"/>
</body>
</html>
|
3.5.9.3. infos.jsp
Diese Ansicht zeigt Informationen zu einem Artikel an und ermöglicht zudem dessen Kauf:

Sie wird nach einer Anfrage an /main?action=infos&id=ID oder einer Anfrage an /main?action=achat&id=ID angezeigt, wenn die gekaufte Menge falsch ist. Die Elemente der Controller-Anfrage lauten wie folgt:
| Objekt „Hashtable[]“ – das Array der Menüoptionen |
| Objekt vom Typ [Article] – anzuzeigender Artikel |
| String-Objekt – Meldung, die bei einem Fehler bezüglich der Menge angezeigt werden soll |
| String-Objekt – im Eingabefeld anzuzeigender Wert [Qte] |
Die Felder [msg] und [qte] werden bei einem Eingabefehler bei der Menge verwendet:

Diese Seite enthält ein Formular, das über die Schaltfläche [Acheter] übermittelt wird. Die Ziel-URL von POST lautet [?action=achat&id=ID], wobei ID die ID des gekauften Artikels ist.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Article d'id [<c:out value="${article.id}"/>]</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th><th>STOCK ACTUEL</th><th>STOCK MINIMUM</th>
</tr>
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><c:out value="${article.stockActuel}"/></td>
<td><c:out value="${article.stockMinimum}"/></td>
</tr>
</table>
<p>
<form method="post" action="?action=achat&id=<c:out value="${article.id}"/>"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value="<c:out value="${qte}"/>"></td>
<td><c:out value="${msg}"/></td>
</tr>
</table>
</form>
</body>
</html>
|
3.5.9.4. panier.jsp
Diese Ansicht zeigt den Inhalt des Warenkorbs an:

Sie wird nach einer Anfrage an /main?action=warenkorb oder /main?action=retirerachat&id=ID angezeigt. Die Elemente der Controller-Anfrage lauten wie folgt:
| Objekt „Hashtable[]“ – das Array der Menüoptionen |
| Objekt vom Typ [Panier] – der anzuzeigende Warenkorb |
Jeder Link [Retirer] im Array HTML der Warenkorb-Artikel hat eine URL der Form [?action=retirerachat&id=ID], wobei ID das Feld [id] desArtikels, den man aus dem Warenkorb entfernen möchte.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<table border="1">
<tr>
<td>Article</td><td>Qte</td><td>Pu</td><td>Total</td>
</tr>
<c:forEach var="achat" items="${panier.achats}">
<tr>
<td><c:out value="${achat.article.nom}"/></td>
<td><c:out value="${achat.qte}"/></td>
<td><c:out value="${achat.article.prix}"/></td>
<td><c:out value="${achat.total}"/></td>
<td><a href="<c:out value="?action=retirerachat&id=${achat.article.id}"/>">Retirer</a></td>
</tr>
</c:forEach>
</table>
<p>
Total de la commande : <c:out value="${panier.total}"/> euros
</body>
</html>
|
3.5.9.5. paniervide.jsp
Diese Ansicht zeigt die Information an, dass der Warenkorb leer ist:

Sie wird nach einer Anfrage an /main?action=warenkorb oder /main?action=retirerachat&id=ID angezeigt. Die Elemente der Controller-Anfrage lauten wie folgt:
| Objekt „Hashtable[]“ – das Array der Menüoptionen |
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<p>
Votre panier est vide.
</body>
</html>
|
3.5.9.6. erreurs.jsp
Diese Ansicht wird bei Fehlern angezeigt:

Sie wird nach jeder Anfrage angezeigt, die zu einem Fehler führt, mit Ausnahme der Kaufaktion mit einer falschen Menge, die von der Ansicht [INFOS] verarbeitet wird. Die Elemente der Controller-Anfrage lauten wie folgt:
| Objekt „Hashtable[]“ – das Array der Menüoptionen |
| ArrayList – eine Reihe von String-Objekten, die die anzuzeigenden Fehlermeldungen darstellen |
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
<c:forEach var="erreur" items="${erreurs}">
<li><c:out value="${erreur}"/></li>
</c:forEach>
</ul>
</body>
</html>
|
3.5.9.7. index.jsp
Diese Seite ist in der Anwendungsdatei [web.xml] als Startseite der Anwendung definiert:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
....
</servlet>
<servlet-mapping>
....
</servlet-mapping>
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
Die Ansicht [index.jsp] leitet den Client lediglich zum Einstiegspunkt der Anwendung weiter:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main?action=liste"/>
|
3.5.10. Der Controller
Nun muss noch das Herzstück unserer Webanwendung geschrieben werden: der Controller. Seine Aufgabe besteht darin:
- die Anfrage des Clients entgegenzunehmen,
- die von ihm angeforderte Aktion mithilfe der Fachklassen bearbeiten,
- die entsprechende Ansicht als Antwort zurückgeben.
3.5.10.1. Initialisierung des Controllers
Wenn die Controller-Klasse vom Servlet-Server geladen wird, wird ihre Methode [init] ausgeführt. Dies geschieht nur einmal. Sobald der Controller in den Arbeitsspeicher geladen ist, verbleibt er dort und verarbeitet die Anfragen der verschiedenen Clients. Für jeden Client wird ein eigener Ausführungs-Thread erstellt, sodass die Methoden des Controllers gleichzeitig von verschiedenen Threads ausgeführt werden. Es sei daran erinnert, dass der Controller aus diesem Grund keine Felder enthalten darf, die von seinen Methoden geändert werden könnten. Seine Felder müssen schreibgeschützt sein. Sie werden von der Methode [init] initialisiert, was deren Hauptaufgabe ist. Diese Methode hat nämlich die Besonderheit, dass sie nur einmal von einem einzigen Thread ausgeführt wird. Es gibt daher in dieser Methode keine Probleme mit konkurrierendem Zugriff auf die Felder des Controllers. Die Methode [init] dient dazu, die für die Webanwendung erforderlichen Objekte zu initialisieren, die von allen Client-Threads im schreibgeschützten Modus gemeinsam genutzt werden. Diese gemeinsam genutzten Objekte können an zwei Stellen platziert werden:
- in den privaten Feldern des Controllers
- im Ausführungskontext der Anwendung (ServletContext)
Die Methode [init] der Anwendung [webarticles] führt folgende Aktionen durch:
- Sie überprüft, ob in der Datei [web.xml] die für den ordnungsgemäßen Betrieb der Anwendung erforderlichen Parameter vorhanden sind. Diese wurden in Abschnitt 3.5.5 beschrieben.
- ein privates Feld [ArrayList erreurs] mit der Liste eventueller Fehler setzt. Diese Liste ist leer, wenn keine Fehler vorliegen, existiert aber dennoch.
- Wenn Fehler aufgetreten sind, bricht die Methode [init] an dieser Stelle ab. Andernfalls erstellt sie ein Objekt vom Typ [IArticlesDomain], das als Geschäftsobjekt dient, das der Controller für seine Zwecke verwendet. Wie in 3.5.6 erläutert, fordert der Controller das benötigte Bean vom Spring-Framework an. Bei dieser Instanziierung können verschiedene Fehler auftreten. Ist dies der Fall, werden diese wiederum im Feld [erreurs] des Controllers gespeichert.
3.5.10.2. Methoden doGet, doPost
Diese beiden Methoden verarbeiten die Client-Anfragen HTTP, GET und POST. Diese werden unterschiedslos verarbeitet. Die Methode [doPost] kann somit an die Methode [doGet] weiterleiten oder umgekehrt. Die Kundenanfrage wird wie folgt verarbeitet:
- Das Feld [erreurs] wird überprüft. Ist es nicht leer, bedeutet dies, dass bei der Initialisierung der Anwendung Fehler aufgetreten sind und diese nicht funktionieren kann. Als Antwort wird dann die Ansicht [ERREURS] gesendet.
- Der Parameter [action] der Anfrage wird abgerufen und überprüft. Entspricht er keiner bekannten Aktion, wird die Ansicht [ERREURS] mit einer entsprechenden Fehlermeldung gesendet.
- Ist der Parameter [action] gültig, wird die Anfrage des Clients zur Bearbeitung an eine aktionsspezifische Prozedur weitergeleitet. Die Prozedur zur Bearbeitung der Aktion [uneAction] hat folgende Signatur:
| /**
* @param request la requête du client
* @param response la réponse au client
* @throws IOException
* @throws ServletException
*/
private void doUneAction(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
|
3.5.10.3. Verarbeitung der verschiedenen Aktionen
Die Methoden zur Verarbeitung der verschiedenen möglichen Aktionen der Anwendung lauten wie folgt:
Methode | Anfrage | Verarbeitung | Mögliche Antworten |
| GET /main?action=liste | - Liste der Artikel bei der Fachklasse - anzeigen | [LISTE] oder [ERREURS] |
| GET /main?action=infos&id=ID | - den Artikel mit der ID „ID“ bei der Fachklasse - anzeigen | [INFOS] oder [ERREURS] |
| POST /main?action=achat&id=ID - Die gekaufte Menge ist Teil der übermittelten Parameter | - den Artikel mit der ID=ID bei der Fachklasse abfragen - in den Warenkorb aufnehmen in der Kundensitzung | [LISTE] oder [INFOS] oder [ERREURS] |
| GET /main?action=retirerachat&id=ID | - Artikel mit der ID=ID aus der Einkaufsliste des Warenkorbs der Kundensitzung | [PANIER] |
| GET /main?action=warenkorb | - Warenkorb der Kundensitzung | [PANIER] oder [PANIERVIDE] |
| GET /main?action=warenkorbbestätigung | - die Lagerbestände aller Artikel in der Datenbank verringern Bestände aller Artikel , die sich im Warenkorb der Kunden-Sitzung | [LISTE] oder [ERREURS] |
3.5.10.4. Der Code
| package istia.st.articles.web;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import istia.st.articles.domain.Achat;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
/**
* @author ST
*
*/
public class WebArticles extends HttpServlet {
// private Felder
private ArrayList erreurs = new ArrayList();
private IArticlesDomain articlesDomain = null;
private final String URL_MAIN = "urlMain";
private final String URL_ERREURS = "urlErreurs";
private final String URL_LISTE = "urlListe";
private final String URL_INFOS = "urlInfos";
private final String URL_PANIER = "urlPanier";
private final String URL_PANIER_VIDE = "urlPanierVide";
private final String URL_DEBUG = "urlDebug";
private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
private final String[] parameters =
{
URL_MAIN,
URL_ERREURS,
URL_LISTE,
URL_INFOS,
URL_PANIER,
URL_PANIER_VIDE,
URL_DEBUG,
SPRING_CONFIG_FILENAME };
private ServletConfig config;
private final String ACTION_LISTE = "liste";
private final String ACTION_PANIER = "panier";
private final String ACTION_ACHAT = "achat";
private final String ACTION_INFOS = "infos";
private final String ACTION_RETIRER_ACHAT = "retirerachat";
private final String ACTION_VALIDATION_PANIER = "validationpanier";
private String urlActionListe;
private final String lienActionListe = "Liste des articles";
private String urlActionPanier;
private final String lienActionPanier = "Voir le panier";
private String urlActionValidationPanier;
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
public void init() {
// Die Initialisierungsparameter des Servlets werden abgerufen
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// Der Fehler wird gespeichert
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// Gibt es Fehler?
if (erreurs.size() != 0) {
return;
}
// Es wird ein Objekt IArticlesDomain für den Zugriff auf die Geschäftsebene angelegt
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// Der Fehler wird protokolliert
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// Es werden bestimmte URLs der Anwendung gespeichert
hActionListe.put("href", "?action=" + ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", "?action=" + ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put(
"href",
"?action=" + ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// Fertig
return;
}
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// Es wird überprüft, wie die Initialisierung des Servlets verlaufen ist
if (erreurs.size() != 0) {
// Liegt die URL der Fehlerseite vor?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// Ende
return;
}
// Die Aktion wird verarbeitet
String action = request.getParameter("action");
if (action == null) {
// Liste der Artikel
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// Liste der Artikel
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// Informationen zu einem Artikel
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// Artikel kaufen
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// Warenkorb anzeigen
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// Artikel aus dem Warenkorb entfernen
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// Warenkorb bestätigen
doValidationPanier(request, response);
return;
}
// unbekannte Aktion
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// Die Fehlerseite wird angezeigt
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// Ende
return;
}
private void doValidationPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// Der Käufer hat seinen Warenkorb bestätigt
Panier panier = (Panier) request.getSession().getAttribute("panier");
// Dieser Warenkorb wird bestätigt
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// nicht normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// Fehler werden abgerufen
ArrayList erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionPanier });
afficheErreurs(request, response, erreurs);
return;
}
// die Artikelliste wird angezeigt
request.setAttribute("message", "Votre panier a été validé");
doListe(request, response);
// Ende
return;
}
private void doRetirerAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// einen Artikel aus dem Warenkorb entfernen
try {
Panier panier =
(Panier) request.getSession().getAttribute("panier");
String strIdAchat = request.getParameter("id");
panier.enlever(Integer.parseInt(strIdAchat));
} catch (NumberFormatException ignored) {
} catch (NullPointerException ignored) {
}
// Warenkorb anzeigen
doPanier(request, response);
}
private void doPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// Warenkorb anzeigen
Panier panier = (Panier) request.getSession().getAttribute("panier");
// Warenkorb leer?
if (panier == null || panier.getAchats().size() == 0) {
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
.forward(request, response);
// Ende
return;
}
// Es befindet sich etwas im Warenkorb
request.setAttribute("panier", panier);
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionValidationPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER))
.forward(request, response);
// Ende
return;
}
private void doAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// Kauf eines Artikels
// Menge abrufen
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// falsche Menge
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
String url =
config.getInitParameter(URL_MAIN)
+ "?action=infos&id="
+ request.getParameter("id");
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// Ende
return;
}
// Kundensitzung abrufen
HttpSession session = request.getSession();
// Kauf wird angelegt
Article article = (Article) session.getAttribute("article");
Achat achat = new Achat(article, qté);
// Der Kauf wird in den Warenkorb des Kunden gelegt
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// Rückkehr zur Artikelliste
String url = config.getInitParameter(URL_MAIN) + "?action=liste";
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// Ende
return;
}
private void afficheDebugInfos(
HttpServletRequest request,
HttpServletResponse response,
ArrayList infos)
throws ServletException, IOException {
// Die Artikelliste wird angezeigt
request.setAttribute("infos", infos);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_DEBUG))
.forward(request, response);
// Ende
return;
}
public void doPost(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// wie get
doGet(request, response);
}
private void doInfos(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// die Fehlerliste
ArrayList erreurs = new ArrayList();
// die angeforderte ID wird abgerufen
String strId = request.getParameter("id");
// Irgendetwas?
if (strId == null) {
// nicht normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// strId wird vollständig umgewandelt
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// nicht normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// Der Schlüssel-ID-Eintrag wird angefordert
Article article = null;
try {
article=articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// nicht normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
if (article == null) {
// nicht normal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// Der Artikel wird in die Sitzung aufgenommen
request.getSession().setAttribute("article", article);
// Die Informationsseite wird angezeigt
request.setAttribute("actions", new Hashtable[] { hActionListe });
/ / request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_INFOS))
.forward(request, response);
// Ende
return;
}
private void afficheErreurs(
HttpServletRequest request,
HttpServletResponse response,
ArrayList erreurs)
throws ServletException, IOException {
// die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreurs);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// Ende
return;
}
private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// die Fehlerliste
ArrayList erreurs = new ArrayList();
// die Artikelliste wird angefordert
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// Der Fehler wird gespeichert
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// Gibt es Fehler?
if (erreurs.size() != 0) {
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// Ende
return;
}
// die Artikelliste wird angezeigt
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// Ende
return;
}
/**
* suivi console pour débogage
* @param message : le message à afficher
*/
private void affiche(String message) {
System.out.println(message);
}
}
|
Wir lassen dem Leser Zeit, diesen Code in Ruhe zu lesen und zu verstehen. Wir hoffen, dass die Kommentare dabei helfen.
3.5.10.5. Tests der Anwendung
Sehen wir uns einige Screenshots aus den Tests an. Zunächst die Startseite der Anwendung:

Die angeforderte URL lautete tatsächlich [http://localhost:8080/webarticles]. Der Leser wird feststellen, dass wir in der Datei [web.xml] eine Startseite für die Anwendung definieren:
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
Die Ansicht [index.jsp] ist wie folgt definiert:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main?action=liste"/>
Es erfolgte also eine Weiterleitung zur URL [http://localhost:8080/webarticles/main?action=liste], wie die Browser-URL auf dem Screenshot zeigt. Die URL [/main?action=liste] wurde also aufgerufen. Ebenfalls unter [web.xml] ist die URL /main dem Servlet [webarticles] zugeordnet:
<servlet-mapping>
<servlet-name>webarticles</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
Ebenfalls in [web.xml] ist das Servlet [webarticles] mit dem Servlet [ istia.st.articles.web.WebArticles] verknüpft:
<servlet-name>webarticles</servlet-name>
<servlet-class>istia.st.articles.web.WebArticles</servlet-class>
Das Servlet [ istia.st.articles.web.WebArticles] wird daher vom Tomcat-Servlet-Container geladen, falls dies noch nicht geschehen ist, und seine Methode [init] wird ausgeführt:
| public void init() {
// die Initialisierungsparameter des Servlets werden abgerufen
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// Der Fehler wird gespeichert
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// Gibt es Fehler?
if (erreurs.size() != 0) {
return;
}
// Ein Objekt IArticlesDomain für den Zugriff auf die Geschäftslogik wird erstellt
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// Der Fehler wird gespeichert
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// bestimmte URLs der Anwendung werden gespeichert
hActionListe.put("href", "?action=" + ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", "?action=" + ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put(
"href",
"?action=" + ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// Fertig
return;
}
|
Anmerkungen: Die Methode [init]
- überprüft das Vorhandensein bestimmter Konfigurationsparameter
- instanziiert mithilfe von Spring einen Dienst für den Zugriff auf die Anwendungsdomäne
- legt eine Fehlerliste an, um eventuelle Initialisierungsfehler anzuzeigen
- eine Reihe privater Felder:
- Fehler: die Liste der von [init] erkannten Fehler
- [hActionListe, hActionPanier, hActionValidationPanier]: Wörterbücher. Jedes davon enthält die erforderlichen Informationen zur Anzeige einer Option des Hauptmenüs, das von der Ansicht [entete.jsp] angezeigt wird
- acticlesDomain: Der Dienst für den Zugriff auf das Anwendungsmodell
Die Methode [init] wird nur einmal ausgeführt, und zwar beim ersten Laden des Servlets. Anschließend wird je nach Typ [GET, POST] der Client-Anfrage eine der Methoden [doGet, doPost] ausgeführt. Hier führen beide Methoden dasselbe aus, und der Code wurde in [doGet] platziert:
| public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// Es wird überprüft, wie die Initialisierung des Servlets verlaufen ist
if (erreurs.size() != 0) {
// Liegt die URL der Fehlerseite vor?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// Ende
return;
}
// Die Aktion wird verarbeitet
String action = request.getParameter("action");
if (action == null) {
// Liste der Artikel
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// Liste der Artikel
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// Informationen zu einem Artikel
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// Artikel kaufen
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// Warenkorb anzeigen
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// Artikel aus dem Warenkorb entfernen
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// Warenkorb bestätigen
doValidationPanier(request, response);
return;
}
// unbekannte Aktion
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// Die Fehlerseite wird angezeigt
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// Ende
return;
}
|
- Die Methode [doGet] prüft zunächst, ob nach Ausführung der Methode [init] Initialisierungsfehler aufgetreten sind. Wenn ja, lässt sie die Ansicht [ERREURS] anzeigen, und damit ist der Vorgang beendet.
- Andernfalls ruft sie den Parameter [action] aus der Client-Anfrage ab. Zur Erinnerung: Die Anwendung wurde so konzipiert, dass sie nur Anfragen bearbeitet, die zwingend einen Parameter [action] enthalten.
- Sie lässt die mit der Aktion verknüpfte Methode ausführen. In diesem Fall ist das die Methode [doListe].
Die Methode [doListe] lautet wie folgt:
| private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// die Fehlerliste
ArrayList erreurs = new ArrayList();
// die Artikelliste wird angefordert
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// Der Fehler wird gespeichert
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// Gibt es Fehler?
if (erreurs.size() != 0) {
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// Ende
return;
}
// Artikelübersicht anzeigen
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// Ende
return;
}
|
- Zur Erinnerung: Die Methode [init] hat den Dienst für den Zugriff auf das Anwendungsmodell (Domain-Schicht) in einem privaten Feld des Servlets gespeichert:
// private Felder
private IArticlesDomain articlesDomain = null;
- Über diesen Zugriffsdienst kann die Liste der Artikel abgefragt werden:
| // die Fehlerliste
ArrayList erreurs = new ArrayList();
// Artikelübersicht anfordern
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// Der Fehler wird gespeichert
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
|
- Sollten Fehler auftreten, wird die Ansicht [ERREURS] gesendet:
| // Gibt es Fehler?
if (erreurs.size() != 0) {
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// Ende
return;
}
|
- Andernfalls wird die Ansicht [LISTE] gesendet:
| // Artikelübersicht anzeigen
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
|
In unserem Beispiel ist alles gut gelaufen und wir haben die Ansicht [LISTE] erhalten. Der Leser sei gebeten, den Code der Ansicht [LISTE] noch einmal durchzulesen, um festzustellen, dass die von dieser Ansicht erwarteten dynamischen Parameter oben vom Controller tatsächlich bereitgestellt werden. Die gleiche Überprüfung ist für jede Ansicht durchzuführen:
- die dynamischen Parameter der Ansicht ermitteln
- sicherstellen, dass der Controller diese tatsächlich in die Attribute der an die Ansicht übermittelten Anfrage einfügt
Wir geben nun einfach den Ablauf der Bildschirme wieder, die ein Benutzer der Anwendung durchläuft. Der Leser ist aufgefordert, jedes Mal eine ähnliche Überlegung wie zuvor anzustellen:
Ausgehend von der Artikelliste kann der Benutzer einen Artikel auswählen:
Der Käufer kann hier den Artikel Nr. 3 kaufen. Machen wir einen Eingabefehler bei der Menge:
Der Fehler wurde gemeldet. Kaufen wir nun einige Artikel:
Der Kauf wurde registriert und die Artikelliste erneut angezeigt. Schauen wir uns den Warenkorb an:
Der Artikel befindet sich im Warenkorb. Entfernen wir ihn:
Der Artikel wurde aus dem Warenkorb entfernt und dieser erneut angezeigt. Hier ist er leer.
Kaufen wir 100 Artikel Nr. 3 und 2 Artikel Nr. 4:
Der Kauf des Artikels Nr. 3 war nicht möglich, da 100 Stück gekauft werden sollten, aber nur 30 auf Lager waren. Dieser Artikel blieb im Warenkorb:
Artikel Nr. 4 wurde hingegen gekauft, wie sein neuer Lagerbestand von 39 (40-1) zeigt:
3.6. Architektur MVC mit Struts
3.6.1. Allgemeine Architektur der Anwendung
Kommen wir noch einmal auf die Architektur MVC der Anwendung zurück:
In der vorherigen Version:
- wurde die Steuerung durch ein Servlet übernommen
- wurden die Ansichten durch JSP-Seiten bereitgestellt
- Das Modell wurde durch drei .jar-Dateien bereitgestellt
In der Struts-Version:
- wird der Controller durch ein Servlet bereitgestellt, das vom generischen Struts-Servlet [ActionServlet] abgeleitet ist
- Die Ansichten werden – bis auf wenige Details – von denselben JSP-Seiten wie zuvor bereitgestellt
- Das Modell wird durch dieselben drei Archive bereitgestellt
Wir werden sehen, dass die Umstellung der vorherigen Anwendung auf Struts aus den folgenden Aufgaben besteht:
- Aktionen, die zuvor in bestimmten Methoden des Servlets/Controllers verarbeitet wurden, werden nun von Instanzen von Klassen verarbeitet, die von der Struts-Klasse „[Action]“ abgeleitet sind
- Erstellen Sie die Konfigurationsdateien [web.xml] und [struts-config.xml]
- Nehmen Sie einige Änderungen an den Seiten JSP vor
Zur Erinnerung: Die von STRUTS verwendete generische Architektur MVC:
| die Fachklassen, die Datenzugriffsklassen und die Datenbank |
| die Seiten JSP |
| das Servlet zur Verarbeitung von Client-Anfragen, die Objekte [Action] und die mit den Formularen verknüpften Beans [ActionForm]. |
- Der Controller ist das Herzstück der Anwendung. Alle Client-Anfragen laufen über ihn. Es handelt sich um ein generisches Servlet, das von STRUTS bereitgestellt wird. In bestimmten Fällen kann es erforderlich sein, davon eine Unterklasse abzuleiten. In einfachen Fällen ist dies jedoch nicht notwendig. Dieses generische Servlet bezieht die benötigten Informationen aus einer Datei, die meist den Namen struts-config.xml trägt.
- Enthält die Anfrage des Kunden Formularparameter, werden diese vom Controller in ein Bean-Objekt übertragen. Die im Laufe der Zeit so erstellten Bean-Objekte werden in der Sitzung oder in der Anfrage des Kunden gespeichert. Dieser Punkt ist konfigurierbar. Sie müssen nicht neu erstellt werden, wenn sie bereits vorhanden sind.
- In der Konfigurationsdatei struts-config.xml werden jedem URL, das programmgesteuert verarbeitet werden soll (und somit keiner Ansicht JSP entspricht, die direkt angefordert werden könnte), bestimmte Informationen zugeordnet:
- den Namen der Action-Klasse, die für die Bearbeitung der Anfrage zuständig ist. Auch hier kann das instanziierte Action-Objekt in der Sitzung oder der Anfrage gespeichert werden.
- Wenn die angeforderte URL parametrisiert ist (z. B. beim Absenden eines Formulars an den Controller), wird der Name des Beans angegeben, das für die Speicherung der Formularinformationen zuständig ist.
- Ausgestattet mit diesen Informationen aus seiner Konfigurationsdatei kann der Controller beim Empfang einer Anfrage nach URL durch einen Client feststellen, ob und welche Bean erstellt werden muss. Nach der Instanziierung kann die Bean überprüfen, ob die von ihr gespeicherten Daten, die aus dem Formular stammen, gültig sind oder nicht. Eine Methode der Bean namens „validate“ wird automatisch vom Controller aufgerufen. Die Bean wird vom Entwickler erstellt. Dieser fügt daher in die Methode „validate“ den Code ein, der die Gültigkeit der Formulardaten überprüft. Erweisen sich die Daten als ungültig, fährt der Controller nicht fort. Er übergibt die Kontrolle an eine Ansicht, deren Namen er in seiner Konfigurationsdatei findet. Der Austausch ist damit beendet. Es ist zu beachten, dass der Entwickler festlegen kann, dass die Gültigkeit des Formulars nicht überprüft wird. Dies erfolgt ebenfalls in der Datei „struts-config.xml“. In diesem Fall ruft der Controller die Methode „validate“ der Bean nicht auf.
- Sind die Daten der Bean korrekt, findet keine Überprüfung statt oder ist keine Bean vorhanden, übergibt der Controller die Kontrolle an das Action-Objekt, das mit der Datei URL verknüpft ist. Dies geschieht, indem er die Ausführung der Methode `execute` dieses Objekts anfordert und dabei die Referenz der Bean übergibt, die er gegebenenfalls erstellt hat. An dieser Stelle führt der Entwickler die erforderlichen Schritte aus: Er muss gegebenenfalls auf Geschäftsklassen oder Datenzugriffsklassen zurückgreifen. Am Ende der Verarbeitung übergibt das Action-Objekt dem Controller den Namen der Ansicht, die er als Antwort an den Client senden soll.
- In seiner Konfigurationsdatei findet der Controller das Objekt URL, das dem Namen der Ansicht zugeordnet ist, die er anzeigen soll. Er sendet diese dann. Der Austausch mit dem Client ist beendet.
In unserer Anwendung werden wir keine Objekte vom Typ [Bean] als Pufferobjekte zwischen dem Client und den Klassen vom Typ [Action] verwenden. Das Objekt [Action] wird die Parameter der Client-Anfrage direkt aus dem Objekt [HttpServletRequest] abrufen, das es erhält. Dies erleichtert die Portierung unserer ursprünglichen Anwendung. Die endgültige Architektur unserer Anwendung sieht daher wie folgt aus:
| die Fachklassen, die Datenzugriffsklassen und die Datenbank |
| die Seiten JSP |
| das Servlet zur Verarbeitung von Client-Anfragen, die Objekte [Action] |
3.6.2. Das Modell
Es wurde bereits zuvor vorgestellt. Es besteht aus den Java-Archiven [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.6.3. Konfiguration der Anwendung
3.6.3.1. Allgemeine Architektur
Die allgemeine Architektur des Eclipse-Projekts sieht wie folgt aus:

3.6.3.2. Konfiguration des Datenzugriffs
Da sich die Datenzugriffsschnittstelle nicht ändert, sind die zugehörigen Konfigurationsdateien dieselben wie in der vorherigen Version. Sie sind in [WEB-INF/src] definiert:

Auf dem obigen Screenshot stammen die Dateien [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] aus der vorherigen Version.
3.6.3.3. Das Archivverzeichnis
In [WEB-INF/lib] befinden sich dieselben Archive wie in der vorherigen Version sowie das für Struts erforderliche Archiv:

3.6.3.4. Konfiguration der Anwendung
Die Anwendung wird mithilfe von zwei Dateien konfiguriert: [web.xml, struts-config.xml] im Ordner [WEB-INF]:

Die Datei [web.xml] sieht wie folgt aus:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>strutswebarticles</servlet-name>
<servlet-class>istia.st.articles.web.struts.MainServlet</servlet-class>
<init-param>
<param-name>config</param-name>
<param-value>/WEB-INF/struts-config.xml</param-value>
</init-param>
<init-param>
<param-name>springConfigFileName</param-name>
<param-value>spring-config-sqlmap-firebird.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>strutswebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
Was steht in dieser Datei?
- Die Startseite der Anwendung ist [vues/index.jsp] (welcome-file)
- URL-Anfragen der Form *.do werden an das Servlet [strutswebarticles] weitergeleitet (Servlet-Mapping)
- Das Servlet [strutswebarticles] ist eine Instanz der Klasse [ istia.st.articles.web.struts.MainServlet] (Servlet-Name, Servlet-Klasse)
- Dieses Servlet akzeptiert zwei Initialisierungsparameter
- den Namen der Struts-Konfigurationsdatei (config)
- den Namen der Spring-Konfigurationsdatei (springConfigFileName)
Die Datei [struts-config.xml] sieht wie folgt aus:
| <?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE struts-config PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
"http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">
<struts-config>
<action-mappings>
<action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action path="/liste" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action
path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherListeArticles" path="/main.do"/>
</action>
<action
path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
<forward name="afficherPanier" path="/vues/panier.jsp"/>
<forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
</action>
<action
path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
<forward name="afficherPanier" path="/panier.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action
path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
<forward name="afficherListeArticles" path="/main.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
</action-mappings>
<message-resources parameter="ApplicationResources" null="false" />
</struts-config>
|
Was besagt diese Konfigurationsdatei?
- Dass unser Controller die folgenden URLs verarbeitet:
| um die Liste der Artikel anzuzeigen |
| um die Artikelliste anzuzeigen |
| um Informationen zu einem bestimmten Artikel anzuzeigen |
| um einen bestimmten Artikel zu kaufen |
| um den Warenkorb anzuzeigen |
| um einen Artikel aus dem Warenkorb zu entfernen |
| zum Bestätigen eines Warenkorbs |
- Die oben genannten Aktionen entsprechen eins zu eins den Aktionen, die vom Servlet in der vorherigen Version verarbeitet wurden. Für jede davon werden folgende Informationen angegeben:
- den Namen der Klasse, die für die Verarbeitung dieser Aktion zuständig ist
- die möglichen Antworten (= Ansichten) nach der Verarbeitung der Aktion. Der Controller wählt jeweils nur eine davon aus.
- den Namen einer Meldungsdatei für die Anwendung (message-resources). Hier existiert die Datei zwar, ist aber leer. Sie wird nicht verwendet. Sie muss im Verzeichnis [ClassPath] der Anwendung abgelegt werden. Hier wird sie im Verzeichnis [WEB-INF/classes] abgelegt. Unter Eclipse erhält man dieses Ergebnis, indem man sie im Verzeichnis [WEB-INF/src] ablegt:

3.6.4. Die Ansichten JSP
Die verwendeten Ansichten JSP stammen ebenfalls aus der vorherigen Version. Es wurden nur sehr wenige Änderungen vorgenommen: Es handelt sich um die URLs der Form [?action=XX?id=YY& ...], die nun zu [/XX.do?id=YY&....] werden. Wir greifen hier bereits gegebene Erläuterungen wieder auf, um dem Benutzer das Zurückblättern zu ersparen. Es ist wichtig zu verstehen, dass die vom Controller an die Ansicht übermittelten Informationen in beiden Versionen exakt dieselben sind. In diesem Punkt wurde nichts geändert.
3.6.4.1. entete.jsp
Um den verschiedenen Ansichten eine gewisse Einheitlichkeit zu verleihen, werden diese denselben Kopfbereich verwenden, in dem der Name der Anwendung zusammen mit dem Menü angezeigt wird:
Das Menü ist dynamisch und wird vom Controller festgelegt. Dieser fügt in die an die Seite JSP übermittelte Anfrage ein Schlüsselattribut „actions“ ein, dessen zugehöriger Wert ein Array vom Typ Hastable[] ist. Jedes Element dieses Arrays ist ein Dictionary, das dazu dient, eine Option des Kopfmenüs zu generieren. Jedes Dictionary hat zwei Schlüssel:
- href: die mit der Menüoption verknüpfte URL
- lien: der Text der Menüoption
Die anderen Ansichten der Anwendung verwenden die durch [entete.jsp] definierte Kopfzeile mithilfe des folgenden Tags JSP:
<jsp:include page="entete.jsp"/>
Bei der Ausführung bewirkt dieses Tag, dass der Code der Seite JSP, die es enthält, den Code der Seite [entete.jsp] einbindet. Da es sich bei der URL der Seite um eine relative URL handelt (ohne „/“), wird die Seite [entete.jsp] im selben Ordner gesucht wie die Seite, die das Tag <jsp:include> enthält.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>webarticles</title>
</head>
<body>
<table>
<tr>
<td><h2>Magasin virtuel</h2></td>
<c:forEach items="${actions}" var="action">
<td>|</td>
<td><a href="<c:out value="${action.href}"/>"><c:out value="${action.lien}"/></a></td>
</c:forEach>
</tr>
</table>
<hr>
|
Anmerkungen: Keine Änderungen gegenüber der vorherigen Version
3.6.4.2. liste.jsp
Diese Ansicht zeigt die Liste der zum Verkauf stehenden Artikel an:
Sie wird nach einer Abfrage mit /main.do oder /validerpanier.do angezeigt. Die Elemente der Controller-Abfrage lauten wie folgt:
| Objekt „Hashtable[]“ – das Array der Menüoptionen |
| ArrayList Objekte vom Typ [Article] |
| String-Objekt – am Seitenende anzuzeigende Meldung |
Jeder Link [Infos] im Array HTML der Artikel hat eine URL der Form [/infos.do?id=ID], wobei ID das ID-Feld des angezeigten Artikels ist.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Liste des articles</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th>
</tr>
<c:forEach var="article" items="${listarticles}">
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><a href="<c:out value="infos.do?id=${article.id}"/>">Infos</a></td>
</tr>
</c:forEach>
</table>
<p>
<c:out value="${message}"/>
</body>
</html>
|
Anmerkungen: eine Änderung (oben hervorgehoben)
3.6.4.3. infos.jsp
Diese Ansicht zeigt Informationen zu einem Artikel an und ermöglicht auch dessen Kauf:

Sie wird nach einer Anfrage /infos.do?id=ID oder einer Anfrage /achat.do?id=ID angezeigt, wenn die gekaufte Menge falsch ist. Die Elemente der Anfrage des Controllers lauten wie folgt:
| Objekt „Hashtable[]“ – das Array der Menüoptionen |
| Objekt vom Typ [Article] – anzuzeigender Artikel |
| String-Objekt – Meldung, die bei einem Fehler bezüglich der Menge angezeigt werden soll |
| Objekt String – Wert, der im Eingabefeld [Qte] angezeigt werden soll |
Die Felder [msg] und [qte] werden bei einem Eingabefehler bei der Menge verwendet:

Diese Seite enthält ein Formular, das über die Schaltfläche [Acheter] übermittelt wird. Die Ziel-URL von POST lautet [/achat.do?id=ID], wobei ID die ID des gekauften Artikels ist.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Article d'id [<c:out value="${article.id}"/>]</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th><th>STOCK ACTUEL</th><th>STOCK MINIMUM</th>
</tr>
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><c:out value="${article.stockActuel}"/></td>
<td><c:out value="${article.stockMinimum}"/></td>
</tr>
</table>
<p>
<form method="post" action="achat.do?id=<c:out value="${article.id}"/>"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value="<c:out value="${qte}"/>"></td>
<td><c:out value="${msg}"/></td>
</tr>
</table>
</form>
</body>
</html>
|
Anmerkungen: eine Änderung (oben hervorgehoben)
3.6.4.4. panier.jsp
Diese Ansicht zeigt den Inhalt des Warenkorbs an:

Sie wird nach einer Anfrage an /panier.do oder /retirerachat.do?id=ID angezeigt. Die Elemente der Controller-Anfrage lauten wie folgt:
| Objekt „Hashtable[]“ – das Array der Menüoptionen |
| Objekt vom Typ [Panier] – der anzuzeigende Warenkorb |
Jeder Link [Retirer] im Array HTML der Warenkorb-Artikel hat eine URL der Form [retirerachat.do?id=ID], wobei ID das Feld [id] desArtikels, den man aus dem Warenkorb entfernen möchte.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<table border="1">
<tr>
<td>Article</td><td>Qte</td><td>Pu</td><td>Total</td>
</tr>
<c:forEach var="achat" items="${panier.achats}">
<tr>
<td><c:out value="${achat.article.nom}"/></td>
<td><c:out value="${achat.qte}"/></td>
<td><c:out value="${achat.article.prix}"/></td>
<td><c:out value="${achat.total}"/></td>
<td><a href="<c:out value="retirerachat.do?id=${achat.article.id}"/>">Retirer</a></td>
</tr>
</c:forEach>
</table>
<p>
Total de la commande : <c:out value="${panier.total}"/> euros
</body>
</html>
|
Anmerkungen: Eine Änderung (oben hervorgehoben)
3.6.4.5. paniervide.jsp
Diese Ansicht zeigt die Information an, dass der Warenkorb leer ist:

Sie wird nach einer Anfrage /panier.do oder /retirerachat.do?id=ID angezeigt. Die Elemente der Controller-Anfrage lauten wie folgt:
| Objekt „Hashtable[]“ – das Array der Menüoptionen |
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<p>
Votre panier est vide.
</body>
</html>
|
Kommentare: keine Änderungen.
3.6.4.6. erreurs.jsp
Diese Ansicht wird bei Fehlern angezeigt:

Sie wird nach jeder Anfrage angezeigt, die zu einem Fehler führt, mit Ausnahme der Kaufaktion mit einer falschen Menge, die von der Ansicht [INFOS] verarbeitet wird. Die Elemente der Controller-Anfrage lauten wie folgt:
| Objekt „Hashtable[]“ – das Array der Menüoptionen |
| ArrayList – String-Objekte, die die anzuzeigenden Fehlermeldungen darstellen |
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
<c:forEach var="erreur" items="${erreurs}">
<li><c:out value="${erreur}"/></li>
</c:forEach>
</ul>
</body>
</html>
|
Kommentare: keine Änderungen.
3.6.4.7. index.jsp
Diese Seite ist in der Anwendungsdatei [web.xml] als Startseite der Anwendung definiert:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
....
</servlet>
<servlet-mapping>
....
</servlet-mapping>
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
Die Ansicht [index.jsp] leitet den Client lediglich zum Einstiegspunkt der Anwendung weiter:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main.do"/>
Anmerkungen: eine Änderung (oben hervorgehoben)
3.6.5. Der Struts-Controller
Struts verfügt über einen generischen Controller namens [ActionServlet]. Es ist bekannt, dass ein Servlet über eine Methode namens [init] verfügt, mit der die Anwendung beim Start initialisiert werden kann. Wenn wir den generischen Struts-Controller [ActionServlet] verwenden, haben wir keinen Zugriff auf dessen Methode [init]. Hier müssen wir beim Start der Anwendung einige Aufgaben erledigen, im Wesentlichen ein Objekt für den Zugriff auf das Modell instanziieren. Daher benötigen wir eine Methode [init]. Wir leiten also die Klasse [Actionservlet] in der folgenden Klasse [MainServlet] ab:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import org.apache.struts.action.ActionServlet;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
/**
* @author ST - ISTIA
*
*/
public class MainServlet extends ActionServlet {
// Private Felder
private ArrayList erreurs = new ArrayList();
private IArticlesDomain articlesDomain = null;
private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
private final String[] parameters = { SPRING_CONFIG_FILENAME };
private ServletConfig config;
private final String ACTION_LISTE = "liste.do";
private final String ACTION_PANIER = "panier.do";
private final String ACTION_ACHAT = "achat.do";
private final String ACTION_INFOS = "infos.do";
private final String ACTION_RETIRER_ACHAT = "retirerachat.do";
private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
private String urlActionListe;
private final String lienActionListe = "Liste des articles";
private String urlActionPanier;
private final String lienActionPanier = "Voir le panier";
private String urlActionValidationPanier;
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
// Getter – Setter
public IArticlesDomain getArticlesDomain() {
return articlesDomain;
}
public void setArticlesDomain(IArticlesDomain articlesDomain) {
this.articlesDomain = articlesDomain;
}
public ArrayList getErreurs() {
return erreurs;
}
public void setErreurs(ArrayList erreurs) {
this.erreurs = erreurs;
}
public Hashtable getHActionListe() {
return hActionListe;
}
public void setHActionListe(Hashtable actionListe) {
hActionListe = actionListe;
}
public Hashtable getHActionPanier() {
return hActionPanier;
}
public void setHActionPanier(Hashtable actionPanier) {
hActionPanier = actionPanier;
}
public Hashtable getHActionValidationPanier() {
return hActionValidationPanier;
}
public void setHActionValidationPanier(Hashtable actionValidationPanier) {
hActionValidationPanier = actionValidationPanier;
}
public void init() throws ServletException{
// Initialisierung der übergeordneten Klasse
super.init();
// die Initialisierungsparameter des Servlets werden abgerufen
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// Der Fehler wird gespeichert
erreurs.add("Paramètre [" + parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// Gibt es Fehler?
if (erreurs.size() != 0) {
return;
}
// Ein Objekt IArticlesDomain für den Zugriff auf die Geschäftslogik wird erstellt
try {
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource((String) config
.getInitParameter(SPRING_CONFIG_FILENAME))))
.getBean("articlesDomain");
} catch (Exception ex) {
// Der Fehler wird gespeichert
erreurs.add("Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// bestimmte URLs der Anwendung werden gespeichert
hActionListe.put("href", ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// Fertig
return;
}
}
|
Anmerkungen:
- Der Nutzen der Klasse liegt in ihrer Methode [init] und ihren privaten Feldern
- Die Methode [init] führt dieselbe Aufgabe aus wie die Methode [init] des Controllers aus der vorherigen Version:
- Sie überprüft das Vorhandensein bestimmter Konfigurationsparameter
- sie instanziiert mithilfe von Spring einen Dienst für den Zugriff auf die Anwendungsdomäne
- sie legt eine Fehlerliste an, um eventuelle Initialisierungsfehler anzuzeigen
- Bevor die Arbeit beginnt, ruft die Methode [init] die Methode [init] der übergeordneten Klasse [ActionServlet] auf. Diese ist dafür zuständig, die Struts-Konfigurationsdatei [struts-config.xml] auszuwerten.
- Es sind eine Reihe von privaten Feldern mit ihren Zugriffsmethoden definiert:
- Fehler: die Liste der von [init] erkannten Fehler
- [hActionListe, hActionPanier, hActionValidationPanier]: Wörterbücher. Jedes davon enthält die notwendigen Informationen zur Anzeige einer Option des Hauptmenüs, das von der Ansicht [entete.jsp] angezeigt wird
- acticlesDomain: der Dienst für den Zugriff auf das Anwendungsmodell
- Der Controller einer Struts-Anwendung ist für die Klassen [Action] zugänglich, die für die Verarbeitung der verschiedenen möglichen Aktionen zuständig sind. Diese Klassen haben Zugriff auf die zuvor genannten privaten Felder, da diese über öffentliche Accessoren verfügen.
Die Instanziierung dieses Controllers erfolgt über die Datei [web.xml]:
| <web-app>
<servlet>
<servlet-name>strutswebarticles</servlet-name>
<servlet-class>istia.st.articles.web.struts.MainServlet</servlet-class>
<init-param>
<param-name>config</param-name>
<param-value>/WEB-INF/struts-config.xml</param-value>
</init-param>
<init-param>
<param-name>springConfigFileName</param-name>
<param-value>spring-config-sqlmap-firebird.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>strutswebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
|
Jede URL, die auf .do endet, wird von einer Instanz der Klasse [istia.st.articles.web.struts.MainServlet] verarbeitet
3.6.6. Die Aktionen der Struts-Anwendung
3.6.6.1. Einführung
Jede Struts-Aktion wird zum Gegenstand einer Klasse. In der vorherigen Version war jede Aktion Gegenstand einer Methode im Controller der Anwendung. Das Schreiben der Klasse [Action] besteht meist darin:
- dem Kopieren und Einfügen der Methode, die in der vorherigen Version verwendet wurde
- den Code an die Struts-Konventionen anzupassen
3.6.6.2. main.do, liste.do
Diese beiden Aktionen sind identisch und werden in [struts-config.xml] wie folgt definiert:
| <action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action path="/liste" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
|
Wenn eine dieser Aktionen ([main.do, liste.do]) in einem Browser ausgeführt wird, erhält man folgendes Ergebnis:

Der Code der Klasse [ListeArticlesAction] lautet wie folgt:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST - ISTIA
*
*/
public class ListeArticlesAction extends Action {
/**
* affichage de la liste des articles - s'appuie sur la couche [domain]
*
* @param mapping :
* configuration de l'action dans struts-config.xml
* @param form :
* le formulaire passé à l'action - ici aucun
* @param request :
* la requête HTTP du client
* @param response :
* la réponse HTTP au client
*/
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// das Kontroll-Servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// Initialisierungsfehler?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// das Objekt für den Zugriff auf die Domäne
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// die Fehlerliste
ArrayList erreurs = new ArrayList();
// Die Artikelliste wird angefordert
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// Der Fehler wird gespeichert
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// Gibt es Fehler?
if (erreurs.size() != 0) {
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// Die Artikelliste wird angezeigt
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("afficherListeArticles");
}
}
|
Anmerkungen:
- Das Schreiben des Codes für eine Klasse [Action] besteht im Wesentlichen darin, den Code ihrer Methode [execute] zu schreiben
- In der Instanz des Controllers wurden bestimmte Informationen gespeichert. Eine Referenz darauf erhält man wie folgt:
// das Kontroll-Servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
- Man ruft die Liste der vom Controller gespeicherten Initialisierungsfehler ab. Ist diese Liste nicht leer, wird die Ansicht [ERREURS] an den Client gesendet:
| // Initialisierungsfehler?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
|
Die Ansicht, die tatsächlich an den Client gesendet wird, wird uns von [struts-config.xml] bereitgestellt:
<action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Es handelt sich um die Ansicht [/vues/erreurs.jsp]. Der Leser wird gebeten, zu überprüfen, was von dieser Ansicht erwartet wird. Diese Informationen werden hier von der Aktion im Objekt [request] als Attribute bereitgestellt.
- Ebenfalls mithilfe des Controllers kann die Aktion das Objekt für den Zugriff auf das Anwendungsmodell (Domain-Schicht) abrufen:
// das Objekt für den Zugriff auf die Domäne
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
- Anschließend kann die Liste der Artikel abgefragt werden:
| // die Fehlerliste
ArrayList erreurs = new ArrayList();
// Die Artikelliste wird angefordert
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// Der Fehler wird gespeichert
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
|
- Sollten Fehler auftreten, wird die Ansicht [ERREURS] gesendet:
| // Gibt es Fehler?
if (erreurs.size() != 0) {
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- Andernfalls wird die Ansicht [LISTE] gesendet:
| // Die Artikelliste wird angezeigt
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("afficherListeArticles");
|
Die Ansicht, die tatsächlich an den Kunden gesendet wird, wird uns von [struts-config.xml] bereitgestellt:
<action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Es handelt sich um die Ansicht [/vues/liste.jsp]. Der Leser wird gebeten, zu überprüfen, was von dieser Ansicht erwartet wird. Diese Informationen werden hier von der Aktion im Objekt [request] als Attribute bereitgestellt.
3.6.6.3. infos.do
Diese Aktion dient dazu, Informationen zu einem der in der Ansicht [LISTE] angezeigten Artikel bereitzustellen:
Diese Aktion ist in [struts-config.xml] wie folgt konfiguriert:
<action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Der Code der Klasse [InfosArticleAction] lautet wie folgt:
| package istia.st.articles.web.struts;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class InfosArticleAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// das Kontroll-Servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// Initialisierungsfehler?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// das Objekt für den Zugriff auf die Domäne
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// die Fehlerliste
ArrayList erreurs = new ArrayList();
// Die angeforderte ID wird abgerufen
String strId = request.getParameter("id");
// irgendetwas?
if (strId == null) {
// nicht normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// strId wird in eine Ganzzahl umgewandelt
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// nicht normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// Der Schlüssel-ID-Eintrag wird angefordert
Article article = null;
try {
article = articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// nicht normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
if (article == null) {
// nicht normal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// Der Artikel wird in die Sitzung aufgenommen
request.getSession().setAttribute("article", article);
// Die Informationsseite wird angezeigt
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
}
|
Anmerkungen:
- Der Anfang der Methode [execute] ist identisch mit dem zuvor untersuchten. Dies gilt auch für die anderen Aktionen.
- Die Methode ruft den Parameter [id] ab, der sich normalerweise in der URL befinden sollte. Diese muss tatsächlich die Form [/infos.do?id=X] haben. Es werden verschiedene Prüfungen durchgeführt, um das Vorhandensein und die Gültigkeit des Parameters [id] zu überprüfen. Im Falle eines Problems wird die Ansicht [ERREURS] gesendet.
- Ist [id] gültig, wird der entsprechende Artikel von der Ebene [domain] angefordert. Löst diese eine Ausnahme aus oder wird der Artikel nicht gefunden, wird wiederum die Ansicht [ERREURS] gesendet.
- Wenn alles gut geht, wird der abgerufene Artikel in die Sitzung gespeichert. Dies ist ein strittiger Punkt. Hier geht man davon aus, dass der Kunde diesen Artikel möglicherweise kaufen wird. Tut er dies, wird der Artikel aus der Sitzung abgerufen, anstatt ihn erneut bei der Ebene [domain] anzufordern.
- Schließlich wird die Ansicht [INFOS] angezeigt. Die Ansicht, die tatsächlich an den Kunden gesendet wird, wird uns von [struts-config.xml] bereitgestellt:
<action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Es handelt sich um die Ansicht [/vues/infos.jsp]. Der Leser wird gebeten, zu überprüfen, was von dieser Ansicht erwartet wird. Diese Informationen werden hier von der Aktion im Objekt [request] als Attribute bereitgestellt.
3.6.6.4. achat.do
Diese Aktion dient dazu, den Artikel zu kaufen, der in der vorherigen Ansicht [INFOS] angezeigt wird:
Sobald der Artikel gekauft wurde, wird die Ansicht [LISTE] erneut angezeigt (rechte Ansicht). Betrachtet man den Code HTML der obigen linken Ansicht, so stellt man fest, dass das Tag <form> wie folgt definiert ist:
| <form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
|
Man sieht also, dass das Formular mit der Aktion [achat.do] an den Controller gesendet wird.
Diese Aktion ist in [struts-config.xml] wie folgt konfiguriert:
<action
path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherListeArticles" path="/main.do"/>
</action>
Der Code der Klasse [AchatArticleAction] lautet wie folgt:
| package istia.st.articles.web.struts;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST
*
*/
public class AchatArticleAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// das Kontroll-Servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// Initialisierungsfehler?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// die Liste der Fehler bei dieser Aktion
ArrayList erreurs = new ArrayList();
// die gekaufte Menge wird abgerufen
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// Falsche Menge
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
// Die Kundensitzung wird abgerufen
HttpSession session = request.getSession();
// Abruf des in der Sitzung gespeicherten Artikels
Article article = (Article) session.getAttribute("article");
// Ist die Sitzung abgelaufen?
if(article==null){
// Fehlerseite anzeigen
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// Neuer Kauf wird angelegt
Achat achat = new Achat(article, qté);
// Der Einkauf wird in den Warenkorb des Kunden gelegt
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// Zurück zur Artikelliste
return mapping.findForward("afficherListeArticles");
}
}
|
Anmerkungen:
- Der Anfang der Methode [execute] ist identisch mit den zuvor untersuchten Methoden.
- Zur Erinnerung: Das an den Controller gesendete Formular hat folgende Form:
| <form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
|
- Die Anfrage enthält zwei Parameter: [id]: Nummer des gekauften Artikels, [qte]: gekaufte Menge.
- Das Vorhandensein und die Gültigkeit des Parameters [qte] werden geprüft. Wird dieser Parameter als fehlerhaft erkannt, wird die Ansicht [INFOS] zusammen mit einer Fehlermeldung an den Benutzer zurückgegeben:
| // die Fehlerliste zu dieser Aktion
ArrayList erreurs = new ArrayList();
// Die gekaufte Menge wird abgerufen
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// falsche Menge
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
|
- Der gekaufte Artikel wird aus der Sitzung abgerufen. Diese ist möglicherweise abgelaufen. In diesem Fall wird die Ansicht [ERREURS] gesendet:
| // Die Sitzung des Kunden wird abgerufen
HttpSession session = request.getSession();
// Abruf des in die Sitzung aufgenommenen Artikels
Article article = (Article) session.getAttribute("article");
// Ist die Sitzung abgelaufen?
if(article==null){
// Die Fehlerseite wird angezeigt
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- Wenn die Sitzung nicht abgelaufen ist, wird der Artikel in den Warenkorb gelegt, der ebenfalls aus der Sitzung abgerufen wird:
| // Die Sitzung des Kunden wird abgerufen
HttpSession session = request.getSession();
// Der in der Sitzung gespeicherte Artikel wird abgerufen
Article article = (Article) session.getAttribute("article");
// Sitzung abgelaufen?
if(article==null){
// Die Fehlerseite wird angezeigt
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// Der neue Kauf wird angelegt
Achat achat = new Achat(article, qté);
// Der Kauf wird in den Warenkorb des Kunden gelegt
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
|
- Schließlich wird die Ansicht [LISTE] gesendet:
// Rückkehr zur Artikelliste
return mapping.findForward("afficherListeArticles");
- Die Ansicht, die tatsächlich an den Kunden gesendet wird, wird uns von [struts-config.xml] bereitgestellt:
<action
path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherListeArticles" path="/main.do"/>
</action>
Es handelt sich um die Ansicht [/main.do]. Diese Ansicht ist keine Ansicht, sondern eine Aktion. Die zuvor beschriebene Aktion [/main.do] wird daher ausgeführt und zeigt die Liste der Artikel an.
3.6.6.5. panier.do
Diese Aktion dient dazu, alle Einkäufe des Kunden anzuzeigen. Sie ist über die Menüoption [Voir le panier] verfügbar:
Der Code HTML, der mit dem Link [Voir le panier] verknüpft ist, lautet wie folgt:
<a href="panier.do">Voir le panier</a>
Die Aktion [panier.do] ist in [struts-config.xml] wie folgt konfiguriert:
<action
path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
<forward name="afficherPanier" path="/vues/panier.jsp"/>
<forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
</action>
Der Code der Klasse [VoirPanierAction] lautet wie folgt:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class VoirPanierAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// das Kontroll-Servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// Initialisierungsfehler?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// Der Warenkorb wird angezeigt
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// Warenkorb leer
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
} else {
// Der Warenkorb enthält etwas
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(), mainServlet.getHActionValidationPanier() });
return mapping.findForward("afficherPanier");
}
}
}
|
Anmerkungen:
- Der Anfang der Methode [execute] ist identisch mit den zuvor untersuchten Methoden.
- Der Warenkorb wird aus der Sitzung abgerufen, in der er sich normalerweise befindet. Die Sitzung könnte abgelaufen sein, sodass kein Warenkorb vorhanden ist. Dieser Fall wird nicht als Fehler behandelt, sondern es wird einfach davon ausgegangen, dass der Warenkorb leer ist.
- Ist der Warenkorb leer, wird die Ansicht [PANIERVIDE] angezeigt
- Andernfalls wird die Ansicht [PANIER] angezeigt
Die tatsächlich an den Client gesendeten Ansichten werden durch die Aktion definiert:
<action
path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
<forward name="afficherPanier" path="/vues/panier.jsp"/>
<forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
</action>
3.6.6.6. retirerachat.do
Diese Aktion dient dazu, einen Artikel aus dem Warenkorb zu entfernen:
Nach der Aktion [retirerachat.do] wird der Warenkorb erneut angezeigt (Ansicht rechts oben). Betrachtet man den Code HTML des Links [Valider le panier] aus der Ansicht links oben, ergibt sich Folgendes:
<a href="retirerachat.do?id=3">Retirer</a>
Die Aktion [retirerachat.do] erhält also als Parameter die ID des Artikels, der aus dem Warenkorb entfernt werden soll. Diese Aktion ist in [struts-config.xml] wie folgt konfiguriert:
<action
path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
<forward name="afficherPanier" path="/panier.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Der Code der Klasse [RetirerAchatAction] lautet wie folgt:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class RetirerAchatAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// das Kontroll-Servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// Initialisierungsfehler?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// die Liste der Fehler bei dieser Aktion
ArrayList erreurs = new ArrayList();
// Warenkorb abrufen
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// Sitzung abgelaufen
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
}
// Die ID des zu entfernenden Artikels wird abgerufen
String strId = request.getParameter("id");
// Etwas?
if (strId == null) {
// nicht normal
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// strId wird in eine Ganzzahl umgewandelt
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// nicht normal
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// Den Kauf stornieren
panier.enlever(id);
// Der Warenkorb wird erneut angezeigt
return mapping.findForward("afficherPanier");
}
}
|
Anmerkungen:
- Der Anfang der Methode [execute] ist identisch mit den zuvor untersuchten Methoden.
- Der nachfolgende Code dient dazu, das Vorhandensein und die Gültigkeit des Parameters [id] zu überprüfen. Sollte dieser fehlerhaft sein, wird die Ansicht [ERREURS] gesendet.
- Andernfalls wird der Artikel aus dem Warenkorb entfernt:
// Kauf wird entfernt
panier.enlever(id);
- Anschließend wird der Warenkorb erneut angezeigt:
// Warenkorb wird erneut angezeigt
return mapping.findForward("afficherPanier");
Die Ansicht, die tatsächlich an den Client gesendet wird, wird durch die Aktion definiert:
<action
path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
<forward name="afficherPanier" path="/panier.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Man sieht, dass in Bezug auf die Ansicht die Aktion [/panier.do] ausgelöst wird. Diese wurde bereits beschrieben. Sie zeigt je nach Status des Warenkorbs die Ansicht [PANIER] oder [PANIERVIDE] an.
3.6.6.7. validerpanier.do
Diese Aktion dient dazu, die vom Kunden getätigten Einkäufe zu bestätigen. Konkret bedeutet dies eine einzige Aktion: Die Bestände der gekauften Artikel werden in der Datenbank um die gekauften Mengen verringert. Diese Aktion stammt aus dem folgenden Menü:
Der Code HTML des Links [Valider le panier] lautet wie folgt:
<a href="validerpanier.do">Valider le panier</a>
Wenn dieser Link aktiviert wird, werden die Bestände verringert und die Artikelliste erneut angezeigt.
Diese Aktion ist in [struts-config.xml] wie folgt konfiguriert:
<action
path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
<forward name="afficherListeArticles" path="/main.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Der Code der Klasse [ValiderPanierAction] lautet wie folgt:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class ValiderPanierAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// das Kontroll-Servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// Initialisierungsfehler?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// das Domänenzugriffsobjekt
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// die Liste der Fehler bei dieser Aktion
ArrayList erreurs = new ArrayList();
// Der Käufer hat seinen Warenkorb bestätigt
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// Sitzung abgelaufen
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// Der Warenkorb wird bestätigt
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// nicht normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// Eventuelle Fehler werden abgerufen
erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(),mainServlet.getHActionPanier() });
return mapping.findForward("afficherErreurs");
}
// Alles scheint in Ordnung zu sein OK – Die Artikelliste wird angezeigt
return mapping.findForward("afficherListeArticles");
}
}
|
Anmerkungen:
- Der Anfang der Methode [execute] ist identisch mit den zuvor untersuchten Methoden.
- Der Warenkorb wird aus der Sitzung abgerufen. Ist diese abgelaufen, wird die Ansicht [ERREURS] gesendet:
| // Fehlerliste zu dieser Aktion
ArrayList erreurs = new ArrayList();
// Der Käufer hat seinen Warenkorb bestätigt
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// Sitzung abgelaufen
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- Die Artikel im Warenkorb werden bestätigt. Es können Fehler auftreten, wenn die Lagerbestände nicht ausreichen, um die Bestellungen zu erfüllen. In diesem Fall wird die Ansicht [ERREURS] gesendet:
| // Der Warenkorb wird bestätigt
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// nicht normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// Eventuelle Fehler werden abgerufen
erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(),mainServlet.getHActionPanier() });
return mapping.findForward("afficherErreurs");
}
|
- Wenn alles erfolgreich verlaufen ist, wird die Artikelliste erneut angezeigt:
// Alles scheint in Ordnung zu sein OK – Die Artikelliste wird angezeigt
return mapping.findForward("afficherListeArticles");
Die tatsächlich an den Kunden gesendete Ansicht wird durch die Aktion definiert:
<action
path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
<forward name="afficherListeArticles" path="/main.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Man sieht, dass in Bezug auf die Ansicht die Aktion [/main.do] ausgelöst wird. Diese wurde bereits beschrieben. Sie zeigt die Ansicht [LISTE] an.
3.7. Architektur von MVC mit Spring
3.7.1. Allgemeine Architektur der Anwendung
Kommen wir noch einmal auf die Architektur MVC der Anwendung zurück:
In der ersten Version:
- wurde die Steuerung durch ein Servlet übernommen
- wurden die Ansichten durch JSP-Seiten bereitgestellt
- Das Modell wurde durch eine Gruppe von drei .jar-Archiven bereitgestellt
In der Struts-Version:
- wurde die Steuerung durch ein Servlet übernommen, das vom generischen Struts-Servlet [ActionServlet] abgeleitet war
- Die Ansichten wurden durch dieselben Seiten JSP bereitgestellt wie in der Version [Struts]
- Das Modell wurde durch dieselben drei Archive bereitgestellt
In der Spring-Version:
- wird der Controller von einem von Spring bereitgestellten Servlet [DispatcherServlet] übernommen
- Die Ansichten werden durch dieselben Seiten JSP bereitgestellt wie zuvor, bis auf einige Details
- wird das Modell durch dieselben drei Archive bereitgestellt
Wir werden feststellen, dass die Umstellung unserer Anwendung von Struts auf Spring einfach ist, wenn man bereit ist, nicht alle Elemente zu verwenden, die für eine orthodoxe Spring-Architektur MVC empfohlen werden. Die wichtigsten Änderungen sind folgende:
- Die Aktionen, die in Struts in speziellen Methoden des Servlets/Controllers oder durch Instanzen von Klassen behandelt wurden, die von der Klasse [Action] abgeleitet waren, werden nun durch Instanzen von Klassen behandelt, die die Spring-Schnittstelle [Controller] implementieren
- Die erforderlichen Konfigurationsdateien lauten nun wie folgt:
- [web.xml], da es sich um eine Webanwendung handelt. Diese Datei enthält einen Listener, der bei der Initialisierung der Anwendung die Datei [applicationContext.xml]
- [applicationContext.xml] auswertet, wodurch die von der Anwendung benötigten Beans erstellt werden, insbesondere die des Modellzugriffsdienstes
- Die Ansichten JSP werden mit denen von Struts identisch sein. Es wird erforderlich sein, eine neue zu erstellen.
Zur Erinnerung: Die in der vorherigen Version verwendete Architektur MVC STRUTS:
| die Fachklassen, die Datenzugriffsklassen und die Datenbank |
| die Seiten JSP |
| das Servlet zur Verarbeitung von Client-Anfragen, die Objekte [Action] |
Mit Spring verwenden wir eine identische Architektur:
| die Fachklassen, die Datenzugriffsklassen und die Datenbank |
| die Seiten JSP |
| das Servlet zur Verarbeitung von Client-Anfragen, die Objekte, die die Schnittstelle implementieren [Controller] |
- Der Controller ist das Herzstück der Anwendung. Alle Client-Anfragen laufen über ihn. Es handelt sich um ein generisches Servlet, das von SPRING bereitgestellt wird. Es ist vom Typ [DispatcherServlet]. Wir werden diesen Controller fortan als Controller [Spring] bezeichnen.
- Der Controller [Spring] leitet die Anfrage des Clients an eine der Instanzen vom Typ [Controller] weiter. Pro zu bearbeitender Aktion gibt es eine solche Instanz. Diese wird in der angeforderten URL wie bei Struts definiert. So weiß man, dass die angeforderte Aktion die Aktion [liste] ist, da die angeforderte URL [liste.do] lautet
- Wenn C der Anwendungskontext ist, nutzt der Controller [Spring] eine Datei namens [C-servlet.xml], die die Rolle der Konfigurationsdatei struts-config.xml in der Struts-Version übernimmt. Jeder Aktion, die von der Anwendung verarbeitet werden soll, wird der Name der Controller-Klasse zugeordnet, die für die Bearbeitung der Anfrage zuständig ist.
- Der Controller übergibt die Kontrolle an das der Aktion zugeordnete Controller-Objekt. Dazu fordert er die Ausführung der Methode handleRequest dieses Objekts an und übergibt dabei die Anfrage des Clients. An dieser Stelle führt der Entwickler die erforderlichen Schritte aus: Gegebenenfalls muss er auf Fachklassen oder Datenzugriffsklassen zurückgreifen. Am Ende der Verarbeitung übergibt das Controller-Objekt dem Controller den Namen der Ansicht, die er als Antwort an den Client senden soll.
- In seiner Konfigurationsdatei findet der Controller die Methode URL, die dem Namen der Ansicht zugeordnet ist, deren Anzeige angefordert wurde. Er sendet diese dann. Der Austausch mit dem Client ist damit abgeschlossen.
3.7.2. Das Modell
Es ist dasselbe wie in den beiden vorherigen Versionen. Es besteht aus den Java-Archiven [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.7.3. Konfiguration der Anwendung
3.7.3.1. Allgemeine Architektur
Die allgemeine Architektur des Eclipse-Projekts sieht wie folgt aus:

3.7.3.2. Konfiguration des Datenzugriffs
Da sich die Schnittstelle für den Datenzugriff nicht ändert, sind die zugehörigen Konfigurationsdateien dieselben wie in der vorherigen Version. Sie sind in [WEB-INF/src] definiert:

Auf dem obigen Screenshot stammen die Dateien [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] aus früheren Versionen.
3.7.3.3. Das Archivverzeichnis
In [WEB-INF/lib] befinden sich dieselben Archive wie in der vorherigen Version, mit Ausnahme derjenigen von Struts, das nicht mehr benötigt wird:

3.7.3.4. Konfiguration der Anwendung
Die Anwendung wird mithilfe von drei Dateien konfiguriert: [web.xml, applicationContext.xml, springwebarticles-servlet.xml] im Ordner [WEB-INF]:

Die Datei [web.xml] lautet wie folgt:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<!-- Der Spring-Kontext-Lader der Anwendung -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- das Servlet -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- das URL-Mapping -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- das Eingabedokument -->
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
Was steht in dieser Datei?
- Die Startseite der Anwendung ist [/vues/index.jsp] (welcome-file)
- URL-Anfragen der Form *.do werden an das Servlet [springwebarticles] weitergeleitet (Servlet-Mapping)
- Das Servlet [springwebarticles] ist eine Instanz der Klasse [org.springframework.web.servlet.DispatcherServlet] (servlet-name, servlet-class), die von Spring bereitgestellt wird.
- Der Listener [org.springframework.web.context.ContextLoaderListener] wird beim Start der Anwendung gestartet. Seine Hauptaufgabe besteht darin, die in der Datei [applicationContext.xml] definierten Spring-Beans zu instanziieren
Die Datei [applicationContext.xml] lautet wie folgt:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- die Klasse für den Datenzugriff -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- die Geschäftsklasse -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- die Konfiguration der Webanwendung-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
Hier finden sich sowohl bekannte als auch weniger bekannte Elemente. Bei der Initialisierung der Anwendung werden drei Beans instanziiert:
- articlesDao: Dienst für den Zugriff auf die Schicht [dao]
- articlesDomain: Dienst für den Zugriff auf das Modell
- config: Ein Bean, in dem wir die Informationen zusammenfassen, die alle Clients gemeinsam nutzen müssen. Dieser Bean übernimmt die Rolle, die üblicherweise der Anwendungskontext spielt, jedoch mit typisierten statt untypisierten Informationen.
Die letzte Datei [springwebarticles.xml] definiert die von der Anwendung akzeptierten Aktionen auf eine Weise, die der in der Struts-Datei [struts-config.xml] verwendeten sehr ähnlich ist. Ihr Inhalt lautet wie folgt:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- die Aktionshandler = Controller -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<!-- das Mapping der Anwendung-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
<!-- View-Handler -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- die Meldungsdatei -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>messages</value>
</property>
</bean>
</beans>
|
Was besagt diese Konfigurationsdatei?
- Dass unser Controller die folgenden URLs verarbeitet:
| um die Liste der Artikel anzuzeigen |
| um die Liste der Artikel anzuzeigen |
| um Informationen zu einem bestimmten Artikel anzuzeigen |
| um einen bestimmten Artikel zu kaufen |
| um den Warenkorb anzuzeigen |
| um einen Artikel aus dem Warenkorb zu entfernen |
| zum Bestätigen eines Warenkorbs |
- Die oben genannten Aktionen entsprechen eins zu eins den Aktionen, die vom Controller in früheren Versionen verarbeitet wurden. Für jede davon ist der Name der Klasse angegeben, die für die Verarbeitung zuständig ist. Nehmen wir das Beispiel der Aktion [/panier.do]:
- Sie muss von der Bean [VoirPanierController] verarbeitet werden. Dieser Name ist frei wählbar. Es handelt sich um einen einfachen Schlüssel.
<prop key="/panier.do">VoirPanierController</prop>
- Der Schlüssel [VoirPanierController] ist der Name einer Bean, die in derselben Konfigurationsdatei definiert ist:
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
- Die Bean [VoirPanierController] definiert:
- die zu instanziierende Klasse [ istia.st.articles.web.spring.VoirPanierController] zur Verarbeitung der Aktion
- wie sie instanziiert werden soll. Hier wird die Bean [config], die durch [applicationContext.xml] definiert und beim Start der Anwendung instanziiert wird, als Parameter übergeben. Dies geschieht für alle Aktionen [Controller]. Somit verfügt jede dieser Aktionen in einem privaten Feld über das Objekt [config], in dem sie alle Informationen findet, die von allen Clients gemeinsam genutzt werden.
- So müssen die Namen der Ansichten aufgelöst werden:
<!-- View-Manager -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
Wie bei Struts gibt die Instanz [Controller], die eine Aktion verarbeitet, nach der Verarbeitung einen Schlüssel an den Spring-Controller zurück, um ihm mitzuteilen, welche Ansicht er anzeigen soll. Anhand dieses Schlüssels gibt es verschiedene Strategien, um die dem Schlüssel zugeordnete Ansicht zu generieren. Die gewählte Strategie ist diejenige, die durch den Bean [viewResolver] definiert wird. Hier ist dieser Bean der Klasse [org.springframework.web.servlet.view.InternalResourceViewResolver] mit verschiedenen Initialisierungsparametern zugeordnet. Ohne ins Detail zu gehen, legt die Bean [viewResolver] hier fest, dass, wenn der Schlüssel der Ansicht „XX“ lautet, die generierte Ansicht [/vues/XX.jsp] sein wird. Die Art der an den Client gesendeten Ansichten kann auf verschiedene Weise geändert werden:
- durch Ändern der Implementierungsklasse für die Bean [viewResolver]
- durch Ändern der Initialisierungsparameter der Implementierungsklasse
So kann man von einer Ansicht „HTML“ zu einer Ansicht „XML“ wechseln, indem man einfach den Wert der Bean „[viewResolver]“ ändert
- dem Namen einer Meldungsdatei für die Anwendung (messageSource). In diesem Fall existiert die Datei zwar, ist aber leer. Sie wird nicht verwendet. Sie muss im Verzeichnis [ClassPath] der Anwendung abgelegt werden. Hier wird sie im Verzeichnis [WEB-INF/classes] abgelegt. Unter Eclipse erhält man dieses Ergebnis, indem man sie im Verzeichnis [WEB-INF/src] ablegt:

3.7.4. Die Ansichten JSP
Die verwendeten Ansichten JSP sind diejenigen, die von Struts verwendet werden. Keine davon wird geändert:

Es wird eine einzige neue Ansicht erstellt: redirpanier.jsp. Sie dient ausschließlich dazu, den Client zur Aktion [/panier.do] weiterzuleiten. Ihr Code lautet wie folgt:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/panier.do"/>
Der Leser wird gebeten, die Definition der verschiedenen Ansichten in der Struts-Version noch einmal durchzulesen.
3.7.5. Die Verarbeitung der Aktionen
Die für die Verarbeitung der verschiedenen Aktionen erforderlichen Klassen wurden im Paket [istia.st.articles.web.spring] zusammengefasst:

Lassen Sie uns die Funktionsweise der Spring-Anwendung anhand eines Beispiels veranschaulichen:
- Der Benutzer ruft die URL [http://localhost:8080/springwebarticles/main.do] auf

Was ist passiert?
- Die Datei [web.xml] der Anwendung [springwebarticles] wurde aufgerufen:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<!-- Lader für den Spring-Kontext der Anwendung -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- das Servlet -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- das URL-Mapping -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- das Eingabedokument -->
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
- Wenn dies die erste Anfrage an die Anwendung war, wurden verschiedene Vorgänge ausgelöst:
- Der Listener [ org.springframework.web.context.ContextLoaderListener] wurde geladen
- er hat die Konfigurationsdatei [applicationContext.xml] ausgewertet:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- die Klasse für den Datenzugriff -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- die Geschäftsklasse -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- die Konfiguration der Webanwendung-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
- Die oben genannten Beans wurden im Anwendungskontext erstellt
- anschließend wurde die Datei [springwebarticles-servlet.xml] verarbeitet:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- die Aktionshandler = Controller -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<!-- das Mapping der Anwendung-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
<!-- View-Handler -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- die Meldungsdatei -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>messages</value>
</property>
</bean>
</beans>
|
- Die durch diese Datei definierten Beans [Controller] wurden ebenfalls erstellt
- Nun ist alles bereit, um die Anfrage des Clients zu bearbeiten. Diese lautete: [http://localhost:8080/springwebarticles]. Hier wird nicht ein URL aus dem Kontext angefordert, sondern der Kontext selbst. Daher wird der Abschnitt [welcome-file] der Datei [web.xml] verwendet.
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
- Die Ansicht [index.jsp] sieht wie folgt aus:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main.do"/>
- Der Client wird daher aufgefordert, zur URL [http://localhost:8080/springwebarticles/main.do] weiterzuleiten. Er tut dies.
- Der Spring-Controller erhält somit eine neue Anfrage. Er verarbeitet die Datei [springwebarticles-servlet.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- Aktionshandler = Controller -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
....
</bean>
<!-- das Mapping der Anwendung-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
</beans>
|
- Diese Datei teilt ihm mit, dass die Aktion [/main.do] von der Bean [ListController] verarbeitet werden muss.
- Die Anfrage des Clients wird an die Methode [handleRequest] der Bean [ListController] weitergeleitet. Diese führt ihre Aufgabe aus und gibt dem Controller den Schlüssel für die anzuzeigende Ansicht zurück. Wenn alles gut läuft, lautet dieser Schlüssel hier [liste].
- Der Spring-Controller verwendet die Bean [viewResolver] aus der Konfigurationsdatei [springwebarticles-servlet.xml], um die diesem Schlüssel zugeordnete Ansicht zu ermitteln. In diesem Fall ist dies die Ansicht [/vues/liste.jsp]
- Die Ansicht [/vues/liste.jsp] wird an den Client gesendet
3.7.6. Initialisierung der Spring-Anwendung
Wir haben bereits erwähnt, dass beim Start der Anwendung die Beans aus der Datei [applicationContext.xml] instanziiert werden:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- die Datenzugriffsklasse -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- die Geschäftsklasse -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- die Konfiguration der Webanwendung-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
Wir kennen die Beans [articlesDao, articlesDomain], nicht jedoch die Bean [config]. Diese wird durch die folgende Java-Klasse definiert:
| package istia.st.articles.web.spring;
import java.util.Hashtable;
import istia.st.articles.domain.IArticlesDomain;
/**
* @author ST - ISTIA
*/
public class Config {
// private Felder
private IArticlesDomain articlesDomain = null;
private final String ACTION_LISTE = "liste.do";
private final String ACTION_PANIER = "panier.do";
private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
private final String lienActionListe = "Liste des articles";
private final String lienActionPanier = "Voir le panier";
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
// Getter und Setter
public IArticlesDomain getArticlesDomain() {
return articlesDomain;
}
public void setArticlesDomain(IArticlesDomain articlesDomain) {
this.articlesDomain = articlesDomain;
}
public Hashtable getHActionListe() {
return hActionListe;
}
public Hashtable getHActionPanier() {
return hActionPanier;
}
public Hashtable getHActionValidationPanier() {
return hActionValidationPanier;
}
// Initialisierung der Webanwendung
public void init() {
// Wir speichern bestimmte URLs der Anwendung
hActionListe.put("href", ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// Fertig
return;
}
}
|
Diese Klasse führt das aus, was die Methode [init] eines Web-Anwendungs-Servlets tut. Sie initialisiert die Anwendung. Dies geschieht hier wie folgt:
- da die Bean [config] in [applicationContext.xml] wie folgt definiert ist:
<!-- Konfiguration der Webanwendung-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
Bei der Erstellung wurde sein privates Feld [articlesDomain] initialisiert
- Anschließend wird aufgrund des Attributs [init-method="init"] des oben genannten Beans die Methode [init] der mit dem Bean verknüpften Klasse ausgeführt. Dort initialisiert sie die drei Wörterbücher [hActionListe, hActionPanier, hActionValidationPanier], die zur Generierung der drei möglichen Links des dem Benutzer angebotenen Menüs verwendet werden.
- Es werden öffentliche Accessoren erstellt, um diese privaten Felder für Instanzen vom Typ [Controller] zugänglich zu machen, die die Aktionen verarbeiten werden.
3.7.7. Die Aktionen [Controller] der Spring-Anwendung
3.7.7.1. Einführung
Jede Spring-Aktion wird durch eine Klasse vom Typ [Controller] abgedeckt. In der Struts-Version wurde jede Aktion durch eine Klasse vom Typ [Action] abgedeckt. Das Erstellen der Klasse [Controller] besteht meist darin,
- aus dem Kopieren und Einfügen der Klasse [Action], die in der Struts-Version verwendet wurde,
- den Code an die Spring-Konventionen anzupassen
3.7.7.2. main.do, liste.do
Diese beiden Aktionen sind identisch und in [springwebarticles-servlet.xml] wie folgt definiert:
| <!-- Das Mapping der Anwendung-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
...
</props>
</property>
</bean>
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
Sie sind mit der Klasse [ istia.st.articles.web.spring.ListController] verknüpft, auf die wir im Folgenden näher eingehen werden. Wenn eine dieser Aktionen in einem Browser ausgeführt wird, erhält man das folgende Ergebnis:

Der Code der Klasse [istia.st.articles.web.spring.ListController] lautet wie folgt:
| package istia.st.articles.web.spring;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class ListController implements Controller {
// Konfiguration der Web-App
Config config;
public void setConfig(Config config) {
this.config = config;
}
// Bearbeitung der Anfrage
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// Die Artikelliste wird angefordert
List articles = null;
try {
articles = config.getArticlesDomain().getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// Der Fehler wird gespeichert
ArrayList erreurs = new ArrayList();
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
// die Fehleransicht senden
return new ModelAndView("erreurs");
}
// Die Artikelliste wird angezeigt
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { config
.getHActionPanier() });
// Ansicht senden
return new ModelAndView("liste");
}
}
|
Anmerkungen:
- Die Klasse verfügt über ein privates Feld [config]. Dieses Feld wurde von Spring initialisiert, als die Bean [ListController] instanziiert wurde:
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
Wie oben zu sehen ist, wird das Feld [config] von [ListController] mit dem Bean [config] initialisiert. Worum handelt es sich dabei? Es handelt sich um die Bean [config], die in [applicationContext.xml] und c.a.d definiert ist – eine Instanz von [istia.st.articles.web.spring.Config], die weiter oben beschrieben wurde.
- Das Schreiben des Codes für eine Klasse [Controller] besteht im Wesentlichen darin, den Code ihrer Methode [handleRequest] zu schreiben
- Man fordert die Liste der Artikel vom Modell an. Dieses ist über [config.getArticlesDomain()] zugänglich. Tritt eine Ausnahme auf, wird die Ansicht [ERREURS] gesendet. Das von [handleRequest] zurückgegebene Ergebnis muss vom Typ [ModelAndView] sein. Diese Klasse kann auf verschiedene Arten instanziiert werden. Hier – und das wird immer der Fall sein – wird eine Instanz von [ModelAndView] erstellt, indem ihr der Schlüssel der anzuzeigenden Ansicht übergeben wird. Zur Erinnerung: Aufgrund der Konfiguration des Beans [viewResolver] führt die Anforderung der Ansicht mit dem Schlüssel XX dazu, dass die Ansicht [/vues/XX.jsp] gesendet wird.
| // die Artikelliste wird angefordert
List articles = null;
try {
articles = config.getArticlesDomain().getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// Der Fehler wird gespeichert
ArrayList erreurs = new ArrayList();
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
// Die Fehlerseite wird angezeigt
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
// Fehlerseite senden
return new ModelAndView("erreurs");
}
|
- Wenn keine Fehler vorliegen, wird die Ansicht [LISTE] gesendet:
| // Die Artikelliste wird angezeigt
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { config
.getHActionPanier() });
// Ansicht senden
return new ModelAndView("liste");
|
3.7.7.3. infos.do
Diese Aktion dient dazu, Informationen zu einem der in der Ansicht [LISTE] angezeigten Artikel bereitzustellen:
Diese Aktion ist in [springwebarticles-servlet.xml] wie folgt definiert:
| <!-- das Mapping der Anwendung-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/infos.do">InfosController</prop>
...
</props>
</property>
</bean>
...
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
Der Code der Klasse [InfosController] lautet wie folgt:
| package istia.st.articles.web.spring;
import istia.st.articles.dao.Article;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class InfosController implements Controller {
// Konfiguration der Webanwendung
Config config;
public void setConfig(Config config) {
this.config = config;
}
// Bearbeitung der Anfrage
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// Fehlerliste
ArrayList erreurs = new ArrayList();
// Die angeforderte ID wird abgerufen
String strId = request.getParameter("id");
// Irgendetwas?
if (strId == null) {
// nicht normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// strId wird vollständig umgewandelt
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// nicht normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// Der Schlüssel-ID-Eintrag wird angefordert
Article article = null;
try {
article = config.getArticlesDomain().getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// nicht normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
if (article == null) {
// nicht normal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// Der Artikel wird in die Sitzung aufgenommen
request.getSession().setAttribute("article", article);
// Die Informationsseite wird angezeigt
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("infos");
}
}
|
Anmerkungen:
- Die Methode [handleRequest] ruft den Parameter [id] ab, der normalerweise in der URL enthalten sein muss. Diese muss tatsächlich die Form [/infos.do?id=X] haben. Es werden verschiedene Tests durchgeführt, um das Vorhandensein und die Gültigkeit des Parameters [id] zu prüfen. Im Falle eines Problems wird die Ansicht [ERREURS] gesendet.
- Ist [id] gültig, wird der entsprechende Artikel von der Ebene [domain] angefordert. Löst diese eine Ausnahme aus oder wird der Artikel nicht gefunden, wird wiederum die Ansicht [ERREURS] gesendet.
- Wenn alles gut geht, wird der abgerufene Artikel in die Sitzung gespeichert. Dies ist ein strittiger Punkt. Hier geht man davon aus, dass der Kunde diesen Artikel möglicherweise kaufen wird. Tut er dies, wird der Artikel aus der Sitzung abgerufen, anstatt ihn erneut bei der Ebene [domain] anzufordern.
- Schließlich wird die Ansicht [INFOS] angezeigt.
3.7.7.4. achat.do
Diese Aktion dient dazu, den Artikel zu kaufen, der in der vorherigen Ansicht [INFOS] angezeigt wurde:

Betrachtet man den Code HTML dieser Ansicht, so stellt man fest, dass das Tag <form> wie folgt definiert ist:
<form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
Man sieht also, dass das Formular mit der Aktion [achat.do] an den Controller gesendet wird.
Diese Aktion ist in [springwebarticles-servlet.xml] wie folgt konfiguriert:
<!-- das Mapping der Anwendung-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/achat.do">AchatController</prop>
...
</props>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
Der Code der Klasse [AchatController] lautet wie folgt:
| package istia.st.articles.web.spring;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class AchatController implements Controller {
// Konfiguration der Webanwendung
Config config;
public void setConfig(Config config) {
this.config = config;
}
// Bearbeitung der Anfrage
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// die Liste der Fehler bei dieser Aktion
ArrayList erreurs = new ArrayList();
// Die gekaufte Menge wird abgerufen
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// Falsche Menge
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("infos");
}
// Abruf der Kundensitzung
HttpSession session = request.getSession();
// Abruf des in der Sitzung gespeicherten Artikels
Article article = (Article) session.getAttribute("article");
// Ist die Sitzung abgelaufen?
if (article == null) {
// Die Fehlerseite wird angezeigt
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// Neuer Kauf wird angelegt
Achat achat = new Achat(article, qté);
// Der Einkauf wird in den Warenkorb des Kunden gelegt
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// Zurück zur Artikelliste
return new ModelAndView("index");
}
}
|
Anmerkungen:
- Zur Erinnerung: Das an den Controller gesendete Formular sieht wie folgt aus:
<form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
- Die Anfrage enthält zwei Parameter: [id]: Nummer des gekauften Artikels, [qte]: gekaufte Menge.
- Das Vorhandensein und die Gültigkeit des Parameters [qte] werden geprüft. Wird dieser Parameter als fehlerhaft erkannt, wird dem Benutzer die Ansicht [INFOS] zusammen mit einer Fehlermeldung zurückgegeben:
| // die Fehlerliste zu dieser Aktion
ArrayList erreurs = new ArrayList();
// Die gekaufte Menge wird abgerufen
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// Falsche Menge
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("infos");
}
|
- Der gekaufte Artikel wird aus der Sitzung abgerufen. Diese ist möglicherweise abgelaufen. In diesem Fall wird die Ansicht [ERREURS] gesendet:
| // Die Sitzung des Kunden wird abgerufen
HttpSession session = request.getSession();
// Abruf des in der Sitzung gespeicherten Artikels
Article article = (Article) session.getAttribute("article");
// Ist die Sitzung abgelaufen?
if (article == null) {
// Die Fehlerseite wird angezeigt
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
|
- Wenn die Sitzung nicht abgelaufen ist, wird der Artikel in den Warenkorb gelegt, der ebenfalls aus der Sitzung abgerufen wird:
| // Neuer Kauf wird angelegt
Achat achat = new Achat(article, qté);
// Der Einkauf wird in den Warenkorb des Kunden gelegt
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
|
- Schließlich wird die Ansicht [LISTE] gesendet:
// Zurück zur Artikelliste
return new ModelAndView("index");
- Oben wird die Ansicht [/vues/index.jsp] gesendet. Wir wissen, dass diese den Client-Browser anweist, zur URL [/main.do] umzuleiten. Durch diese Weiterleitung wird die Liste der Artikel angezeigt.
3.7.7.5. panier.do
Diese Aktion dient dazu, alle Einkäufe des Kunden anzuzeigen. Sie ist über das Menü verfügbar:

Der dem obigen Link zugeordnete Code HTML lautet wie folgt:
<a href="panier.do">Voir le panier</a>
Die über diesen Link aufgerufene Seite lautet wie folgt:

Diese Aktion ist in [springwebarticles-servlet.xml] wie folgt konfiguriert:
| <!-- das Mapping der Anwendung-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/panier.do">VoirPanierController</prop>
...
</props>
</property>
</bean>
...
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
Der Code der Klasse [VoirPanierController] lautet wie folgt:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class VoirPanierController implements Controller {
// Konfiguration der Web-App
Config config;
public void setConfig(Config config) {
this.config = config;
}
// Bearbeitung der Anfrage
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// Der Warenkorb wird angezeigt
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// Warenkorb leer
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("paniervide");
} else {
// Der Warenkorb enthält etwas
request.setAttribute("actions", new Hashtable[] {
config.getHActionListe(), config.getHActionValidationPanier() });
return new ModelAndView("panier");
}
}
}
|
Anmerkungen:
- Der Warenkorb wird aus der Sitzung übernommen, in der er sich normalerweise befindet. Die Sitzung ist möglicherweise abgelaufen, sodass kein Warenkorb vorhanden ist. Dieser Fall wird nicht als Fehler behandelt, sondern es wird einfach davon ausgegangen, dass der Warenkorb leer ist.
- Ist der Warenkorb leer, wird die Ansicht [PANIERVIDE] angezeigt
- Andernfalls wird die Ansicht [PANIER] angezeigt
3.7.7.6. retirerachat.do
Diese Aktion dient dazu, einen Artikel aus dem Warenkorb zu entfernen:

Betrachtet man den Code HTML aus dem obigen Link, so ergibt sich Folgendes:
<a href="retirerachat.do?id=3">Retirer</a>
Die Aktion [retirerachat.do] erhält also als Parameter die ID des Artikels, der aus dem Warenkorb entfernt werden soll. Diese Aktion ist in [springwebarticles-servlet.xml] wie folgt konfiguriert:
| <!-- Anwendungs-Mapping-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/retirerachat.do">RetirerAchatController</prop>
</props>
</property>
</bean>
...
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
Der Code der Klasse [RetirerAchatController] lautet wie folgt:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class RetirerAchatController implements Controller {
// Konfiguration der Webanwendung
Config config;
public void setConfig(Config config) {
this.config = config;
}
// Bearbeitung der Anfrage
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// die Liste der Fehler bei dieser Aktion
ArrayList erreurs = new ArrayList();
// Der Warenkorb wird abgerufen
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// Sitzung abgelaufen
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// Die ID des zu entfernenden Artikels wird abgerufen
String strId = request.getParameter("id");
// Etwas?
if (strId == null) {
// nicht normal
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// strId wird in eine Ganzzahl umgewandelt
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// nicht normal
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// Den Kauf stornieren
panier.enlever(id);
// Der Warenkorb wird erneut angezeigt
request.setAttribute("actions",
new Hashtable[] { config.getHActionListe() });
return new ModelAndView("redirpanier");
}
}
|
Anmerkungen:
- Der Code überprüft das Vorhandensein und die Gültigkeit des Parameters [id]. Sollte dieser fehlerhaft sein, wird die Ansicht [ERREURS] gesendet.
- Andernfalls wird der Artikel aus dem Warenkorb entfernt:
// Kauf wird entfernt
panier.enlever(id);
- Anschließend wird der Warenkorb erneut angezeigt:
// Warenkorb wird erneut angezeigt
request.setAttribute("actions",
new Hashtable[] { config.getHActionListe() });
return new ModelAndView("redirpanier");
Hier noch einmal der Code der Ansicht [/vues/redirpanier.jsp]:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/panier.do"/>
Man sieht, dass der Kunde zur Aktion [/panier.do] weitergeleitet wird. Diese wurde bereits beschrieben. Je nach Status des Warenkorbs wird dabei die Ansicht [PANIER] oder [PANIERVIDE] angezeigt.
3.7.7.7. validerpanier.do
Diese Aktion dient dazu, die vom Kunden getätigten Einkäufe zu bestätigen. Konkret bedeutet dies einen einzigen Vorgang: Die Bestände der gekauften Artikel werden in der Datenbank um die gekauften Mengen verringert. Diese Aktion stammt aus dem folgenden Menü:

Der Code HTML des Links [Valider le panier] lautet wie folgt:
<a href="validerpanier.do">Valider le panier</a>
Wenn dieser Link aktiviert wird, werden die Bestände verringert und die Artikelliste erneut angezeigt.
Diese Aktion ist in [springwebarticles-servlet.xml] wie folgt konfiguriert:
| <!-- das Mapping der Anwendung-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
...
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
Der Code der Klasse [ValiderPanierController] lautet wie folgt:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class ValiderPanierController implements Controller {
// Konfiguration der Web-App
Config config;
public void setConfig(Config config) {
this.config = config;
}
// Bearbeitung der Anfrage
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// die Liste der Fehler bei dieser Aktion
ArrayList erreurs = new ArrayList();
// Der Käufer hat seinen Warenkorb bestätigt
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// Sitzung abgelaufen
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// Der Warenkorb wird bestätigt
try {
config.getArticlesDomain().acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// nicht normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// Eventuelle Fehler werden abgerufen
erreurs = config.getArticlesDomain().getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe(), config.getHActionPanier() });
return new ModelAndView("erreurs");
}
// Alles scheint in Ordnung zu sein OK – Die Artikelliste wird angezeigt
return new ModelAndView("index");
}
}
|
Anmerkungen:
- Der Warenkorb wird aus der Sitzung abgerufen. Ist diese abgelaufen, wird die Ansicht [ERREURS] gesendet:
| // die Liste der Fehler bei dieser Aktion
ArrayList erreurs = new ArrayList();
// Der Käufer hat seinen Warenkorb bestätigt
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// Sitzung abgelaufen
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
|
- Die Artikel im Warenkorb werden bestätigt. Es können Fehler auftreten, wenn die Lagerbestände nicht ausreichen, um die Bestellungen zu erfüllen. In diesem Fall wird die Ansicht [ERREURS] gesendet:
| // Der Warenkorb wird bestätigt
try {
config.getArticlesDomain().acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// nicht normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// Eventuelle Fehler werden abgerufen
erreurs = config.getArticlesDomain().getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe(), config.getHActionPanier() });
return new ModelAndView("erreurs");
}
|
- Wenn alles erfolgreich verlaufen ist, wird die Artikelliste erneut angezeigt:
// Alles scheint in Ordnung zu sein OK – Die Artikelliste wird angezeigt
return new ModelAndView("index");
Es ist bekannt, dass die Ansicht [/vues/index.jsp] den Kunden zur Aktion [/main.do] weiterleitet. Diese Aktion zeigt die Ansicht [LISTE] an.