Skip to content

3. Artikel 2 – Beispiele für dreistufige Webarchitekturen

Ziele dieses Artikels:

  • 3-Schichten-Architekturen
  • Grundlegende MVC-Webarchitektur
  • Struts-MVC-Architektur
  • Spring-MVC-Architektur

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/: DBMS, JDBC-Treiber. Tatsächlich ist jede JDBC-Quelle geeignet.
  • IBExpert, Personal Edition: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (März 2005). Mit IBExpert können Sie das Firebird-DBMS 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/

Das Verständnis dieses Dokuments setzt verschiedene Voraussetzungen voraus. Einige davon finden sich in Dokumenten, die ich verfasst habe. In solchen Fällen verweise ich darauf. Dies ist natürlich nur ein Vorschlag, und es steht den Lesern frei, ihre bevorzugten Ressourcen zu nutzen.

  • Java-Sprache: [https://tahe.developpez.com/java/cours]
  • Webprogrammierung in Java: [https://tahe.developpez.com/java/web/]
  • Webprogrammierung mit Java, Eclipse und Tomcat: [https://tahe.developpez.com/java/eclipse/]
  • Webprogrammierung mit Struts: [https://tahe.developpez.com/java/struts/]
  • Verwendung des IoC-Aspekts von Spring: [https://tahe.developpez.com/java/springioc]
  • JSTL-Tag-Bibliothek: [https://tahe.developpez.com/java/eclipse/] (auszugsweise)
  • Ibatis SqlMap-Dokumentation: [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 bei Wrox.


3.1. Die Webarticles-Anwendung

Hier möchten wir einige Komponenten einer E-Commerce-Webanwendung vorstellen. Diese Anwendung ermöglicht es Web-Clients

  • eine Liste von Artikeln aus einer Datenbank anzuzeigen
  • einige davon in einen elektronischen Warenkorb zu legen
  • den Warenkorb zu bestätigen. Diese Bestätigung aktualisiert lediglich die Lagerbestände der gekauften Artikel in der Datenbank.

Dem Benutzer werden folgende Ansichten angezeigt:

  • die Ansicht [LIST], die eine Liste der zum Verkauf stehenden Artikel anzeigt

Image

  • die Ansicht [INFO], die zusätzliche Informationen zu einem Produkt bereitstellt:

Image

  1. die Ansicht [WARENKORB], die den Inhalt des Warenkorbs des Kunden anzeigt

Image

  1. die Ansicht [WARENKORB LEER], falls der Warenkorb des Kunden leer ist

Image

  1. die Ansicht [ERRORS], die etwaige Anwendungsfehler meldet

Image

3.2. Allgemeine Anwendungsarchitektur

Wir möchten eine Anwendung mit der folgenden dreischichtigen Architektur erstellen:

  • Die drei Schichten werden durch die Verwendung von Java-Schnittstellen voneinander unabhängig gemacht
  • Die Integration der verschiedenen Schichten wird von Spring übernommen
  • Jede Schicht ist in einem eigenen Paket untergebracht: Web (Benutzeroberfläche), Domain (Geschäftsschicht) und DAO (Datenzugriffsschicht).

Wir gehen hier davon aus, dass die [Domain]- und [DAO]-Schichten bereits vorhanden sind. Wir konzentrieren uns ausschließlich auf die [Web]-Schicht, die wir auf verschiedene Arten aufbauen wollen:

  • unter Verwendung klassischer Servlet-Controller-Technologie – JSP-Seiten
  • unter Verwendung der Struts-MVC-Technologie
  • unter Verwendung der Spring-MVC-Technologie

In allen Fällen folgt die Anwendung einer MVC-Architektur (Model-View-Controller). Wenn wir uns das obige Schichtdiagramm noch einmal ansehen, fügt sich die MVC-Architektur wie folgt ein:

Die Bearbeitung einer Client-Anfrage erfolgt in folgenden Schritten:

  1. Der Client sendet eine Anfrage an den Controller. Dieser Controller ist ein Servlet, das alle Client-Anfragen verarbeitet. Er ist der Einstiegspunkt der Anwendung. Er ist das C in MVC.
  2. Der Controller verarbeitet diese Anfrage. Dazu benötigt er möglicherweise Unterstützung durch die Geschäftslogik, die in der MVC-Struktur als M-Modell bezeichnet wird.
  3. Der Controller erhält eine Antwort von der Geschäftsschicht. Die Anfrage des Clients wurde verarbeitet. Dies kann verschiedene mögliche Antworten auslösen. Ein klassisches Beispiel ist
    • eine Fehlerseite, wenn die Anfrage nicht korrekt verarbeitet werden konnte
    • ansonsten eine Bestätigungsseite
  4. Der Controller wählt die Antwort (= View) aus, die an den Client gesendet werden soll. Meistens handelt es sich dabei um eine Seite mit dynamischen Elementen. Der Controller stellt diese der View zur Verfügung.
  5. Die Ansicht wird an den Client gesendet. Dies ist das V in MVC.

3.3. Das Modell

Hier betrachten wir das M in MVC. Das Modell besteht aus den folgenden Elementen:

  1. Geschäftsklassen
  2. Datenzugriffsklassen
  3. die Datenbank

3.3.1. Die Datenbank

Die Datenbank enthält nur eine Tabelle namens ARTICLES. Diese Tabelle wurde mit den folgenden SQL-Befehlen 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
);


/* constraints */
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<>'');

/* primary key */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
Primärschlüssel, der einen Artikel eindeutig identifiziert
Name
Elementname
Preis
sein Preis
Aktueller Lagerbestand
aktueller Lagerbestand
Mindestbestand
der Lagerbestand, unterhalb dessen eine Nachbestellung aufgegeben werden muss

In den folgenden Tests wurde eine [Firebird]-Datenbank verwendet. [Firebird] ist ein „Open-Source“-DBMS. Der JDBC-Treiber [firebirdsql-full.jar] befindet sich im Ordner [WEB-INF/lib] der Webanwendung.

3.3.2. Die Modellpakete

Das M-Modell wird hier in Form von drei Archiven bereitgestellt:

  • istia.st.articles.dao: enthält die Datenzugriffsklassen für die [DAO]-Schicht
  • istia.st.articles.exception: enthält eine Ausnahmeklasse für diese Artikelverwaltung
  • istia.st.articles.domain: enthält die Business-Klassen der [Domain]-Schicht
Archiv
Inhalt
Rolle
istia.st.articles.dao
- enthält das Paket [istia.st.articles.dao], das seinerseits die folgenden Elemente enthält:
- [IArticlesDao]: die Schnittstelle für den Zugriff auf die Dao-Schicht. Dies ist die einzige Schnittstelle, die für die [domain]-Schicht sichtbar ist. Sie sieht keine anderen.
- [Article]: Klasse, die einen Artikel definiert
- [ArticlesDaoSqlMap]: Implementierungsklasse für die [IArticlesDao]-Schnittstelle unter Verwendung des SqlMap-Tools
Datenzugriffsebene – befindet sich vollständig innerhalb der [dao]-Schicht der 3-Schichten-Architektur der Webanwendung
istia.st.articles.domain
- enthält das Paket [istia.st.articles.domain], das seinerseits die folgenden Elemente enthält:
- [IArticlesDomain]: die Schnittstelle für den Zugriff auf die [domain]-Schicht. Dies ist die einzige Schnittstelle, die für die Webschicht sichtbar ist. Sie sieht keine anderen.
- [ArticlePurchases]: eine Klasse, die [IArticlesDomain] implementiert
- [Purchase]: eine Klasse, die den Kauf eines Kunden repräsentiert
- [ShoppingCart]: eine Klasse, die die Gesamteinkäufe eines Kunden repräsentiert
stellt das Web-Kaufmodell dar – befindet sich vollständig innerhalb der [Domain]-Schicht der 3-Schichten-Architektur der Webanwendung
istia.st.articles.exception
- enthält das Paket [istia.st.articles.exception], das seinerseits die folgenden Elemente enthält:
- [UncheckedAccessArticlesException]: Klasse, die eine [RuntimeException]-Ausnahme definiert. Diese Art von Ausnahme wird von der [dao]-Schicht ausgelöst, sobald ein Problem beim Datenzugriff auftritt.
 

3.3.3. Das Paket [istia.st.articles.dao]

Die Klasse, die einen Artikel definiert, sieht wie folgt aus:

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) {
     // init instance attributes
    setId(id);
    setNom(nom);
    setPrix(prix);
    setStockActuel(stockActuel);
    setStockMinimum(stockMinimum);
  }

     // getters - setters
  public int getId() {
    return id;
  }

  public void setId(int id) {
     // valid id?
    if (id < 0)
      throw new UncheckedAccessArticlesException("id[" + id + "] invalide");
    this.id = id;
  }

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
     // valid 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) {
     // valid price?
    if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
    this.prix = prix;
  }

  public int getStockActuel() {
    return stockActuel;
  }

  public void setStockActuel(int stockActuel) {
     // valid stock?
    if (stockActuel < 0)
      throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
    this.stockActuel = stockActuel;
  }

  public int getStockMinimum() {
    return stockMinimum;
  }

  public void setStockMinimum(int stockMinimum) {
     // valid stock?
    if (stockMinimum < 0)
      throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
    this.stockMinimum = stockMinimum;
  }

  public String toString() {
    return "[" + id + "," + nom + "," + prix + "," + stockActuel + ","
        + stockMinimum + "]";
  }
}

Diese Klasse bietet:

  1. einen Konstruktor zum Festlegen der 5 Informationen für ein Element
  2. Zugriffsmethoden, oft als Getter/Setter bezeichnet, zum Lesen und Schreiben der 5 Informationen. Die Namen dieser Methoden entsprechen dem JavaBean-Standard. Die Verwendung von JavaBean-Objekten in der DAO-Schicht zur Anbindung an DBMS-Daten ist gängige Praxis.
  3. Validierung der für das Element eingegebenen Daten. Sind die Daten ungültig, wird eine Ausnahme ausgelöst.
  4. Eine toString-Methode, die den Wert eines Elements als Zeichenkette zurückgibt. Dies ist oft nützlich für die Fehlersuche in einer Anwendung.

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:

getAllArticles
gibt alle Einträge aus der Tabelle ARTICLES in einer Liste von [Article]-Objekten zurück
clearAllArticles
löscht die Tabelle ARTICLES
getArticleById
gibt das [Article]-Objekt zurück, das durch seinen Primärschlüssel identifiziert wird
addArticle
ermöglicht es Ihnen, einen Artikel zur Tabelle ARTICLES hinzuzufügen
modifyArticle
ermöglicht es Ihnen, einen Eintrag in der Tabelle [ARTICLES] zu ändern
deleteItem
ermöglicht es Ihnen, einen Eintrag aus der Tabelle [ARTICLES] zu löschen
updateItemStock
ermöglicht es Ihnen, den Lagerbestand eines Artikels in der Tabelle [ARTICLES] zu ändern

Die Schnittstelle stellt Client-Programmen eine Reihe von Methoden zur Verfügung, die ausschließlich durch ihre Signaturen definiert sind. Sie befasst sich nicht damit, wie diese Methoden tatsächlich implementiert werden. Dies sorgt für Flexibilität in einer Anwendung. Das Client-Programm ruft eine Schnittstelle auf, anstatt eine bestimmte Implementierung dieser Schnittstelle anzusprechen.

Die Auswahl einer bestimmten Implementierung erfolgt über eine Spring-Konfigurationsdatei. Wir schlagen vor, die Schnittstelle IArticlesDao mithilfe eines Open-Source-Produkts namens SqlMap zu implementieren. Auf diese Weise können wir alle SQL-Anweisungen aus dem Java-Code entfernen.

Die Implementierungsklasse [ArticlesDaoSqlMap] ist wie folgt definiert:

package istia.st.articles.dao;

// Imports
import com.ibatis.sqlmap.client.SqlMapClient;
import istia.st.articles.domain.Article;
import java.util.List;

public class ArticlesDaoSqlMap implements IArticlesDao {

  // Fields
  private SqlMapClient sqlMap;

   // Constructors
  public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }

   // Methods
  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 Datenzugriffsmethoden wurden synchronisiert, um Probleme durch gleichzeitigen Zugriff auf die Datenquelle zu vermeiden. Zu jedem Zeitpunkt hat nur ein Thread Zugriff auf eine bestimmte Methode.

Die Klasse [ArticlesDaoSqlMap] verwendet das Tool [Ibatis SqlMap]. Der Vorteil dieses Tools besteht darin, dass es die Trennung des SQL-Codes für den Datenzugriff vom Java-Code ermöglicht. Dieser wird dann in einer Konfigurationsdatei abgelegt. Wir werden später noch einmal darauf zurückkommen. Um instanziiert zu werden, benötigt die Klasse [ArticlesDaoSqlMap] eine Konfigurationsdatei, deren Name als Parameter an den Klassenkonstruktor übergeben wird. Diese Konfigurationsdatei definiert die Informationen, die erforderlich sind, um:

  • Zugriff auf das DBMS, das die Artikel enthält
  • Verwaltung eines Verbindungspools
  • Transaktionen zu verwalten

In unserem Beispiel wird sie den Namen [sqlmap-config-firebird.xml] tragen und den Zugriff auf eine Firebird-Datenbank definieren:

<?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 erwähnte Konfigurationsdatei [articles.xml] definiert, wie eine Instanz der Klasse [istia.st.articles.dao.Article] aus einer Zeile in der Tabelle [ARTICLES] des DBMS erstellt wird. Sie definiert außerdem die SQL-Abfragen, mit denen die [dao]-Schicht 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">

     <!-- an alias to the istia.st.articles.dao.Article class -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- mapping ORM :  row table ARTICLES - instance class Article -->
  <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>

     <!-- query SQL to obtain all items -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- query SQL to delete all items -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- the SQL query to insert an article -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#id#,#nom#,#prix#,#stockactuel#,#stockminimum#)
</statement>

     <!-- the SQL query to delete a given item -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- query SQL to modify a given item -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#nom#,
    prix=#prix#,stockactuel=#stockactuel#,stockminimum=#stockminimum# where
    id=#id#
</statement>

     <!-- query SQL to obtain a given item -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- query SQL to modify the stock of a given item -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#mouvement#
    where id=#id# and stockActuel+#mouvement#&gt;=0
</statement>
</sqlMap>

Der Code für das [dao]-Paket ist im Anhang zu finden.

3.3.4. Das [istia.st.articles.domain]-Paket

Die Schnittstelle [IArticlesDomain] entkoppelt die [business]-Schicht von der [web]-Schicht. Letztere greift über diese Schnittstelle auf die [business/domain]-Schicht 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 Business-Schicht:

 package istia.st.articles.domain;

// Imports
import java.util.ArrayList;
import java.util.List;

public abstract interface IArticlesDomain {

   // Methods
  void acheter(Panier panier);
  List getAllArticles();
  Article getArticleById(int idArticle);
  ArrayList getErreurs();
}
List getAllArticles()
gibt die Liste der [Article]-Objekte zurück, die dem Client angezeigt werden sollen
Artikel getArticleById(int idArticle)
gibt das durch [idArticle] identifizierte [Article]-Objekt zurück
void buy(Cart cart)
bearbeitet den Warenkorb des Kunden, indem der Lagerbestand der gekauften Artikel um die gekaufte Menge verringert wird – kann fehlschlagen, wenn der Lagerbestand nicht ausreicht
ArrayList getErrors()
gibt die Liste der aufgetretenen Fehler zurück – leer, wenn keine Fehler vorliegen

Hier wird die Schnittstelle [IArticlesDomain] durch die folgende Klasse [PurchaseItems] implementiert:

package istia.st.articles.domain;

// Imports
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 {

   // Fields
  private IArticlesDao articlesDao;
  private ArrayList erreurs;

   // Manufacturers
  public AchatsArticles(IArticlesDao articlesDao) { }

   // Methods
  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:

IArticlesDao articlesDao
das von der Datenzugriffsschicht bereitgestellte Datenzugriffsobjekt
ArrayList errors
die Liste aller Fehler

Um eine Instanz der Klasse zu erstellen, müssen Sie das Objekt angeben, das den Zugriff auf die DBMS-Daten ermöglicht:

public PurchasesItems(IArticlesDao articlesDao)
Konstruktor

Die Klasse [Purchase] repräsentiert einen Kundenkauf:

package istia.st.articles.domain;

public class Achat {

   // Fields
  private Article article;
  private int qte;

  // Manufacturers
  public Achat(Article article, int qte) { }

  // Methods
  public double getTotal() {}
  public Article getArticle() {}
  public void setArticle(Article article) { }
  public int getQte() {}
  public void setQte() { }
  public String toString() {}
}

Die Klasse [Purchase] ist ein JavaBean mit den folgenden Feldern und Methoden:

item
der gekaufte Artikel
Anzahl
die gekaufte Menge
doppelt getTotal()
gibt den Kaufbetrag zurück
String toString()
Zeichenkette zur Darstellung des Objekts

Die Klasse [Cart] repräsentiert die Gesamteinkäufe des Kunden:

package istia.st.articles.domain;

// Imports
import java.util.ArrayList;

public class Panier {

  // Fields
  private ArrayList achats;

   // Manufacturers
  public Panier() { }

   // Methods
  public ArrayList getAchats() {}
  public void ajouter(Achat unAchat) { }
  public void enlever(int idAchat) { }
  public double getTotal() {}
  public String toString() { }
}

Die Klasse [Cart] ist ein JavaBean mit den folgenden Feldern und Methoden:

Einkäufe
die Liste der Einkäufe des Kunden – eine Liste von Objekten vom Typ [Purchase]
void add(Purchase purchase)
fügt einen Kauf zur Liste der Käufe hinzu
void remove(int itemId)
entfernt den Kauf mit der Artikel-ID idArticle
double getTotal()
gibt den Gesamtbetrag der Einkäufe zurück
String toString()
gibt die Zeichenfolgendarstellung des Warenkorbs zurück
ArrayList getPurchases()
gibt die Liste der Einkäufe zurück

Der Code für das [domain]-Paket 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 [dao]-Schicht ausgelöst wird, wenn beim Zugriff auf die Datenquelle ein Problem 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. Modellprüfung

Modell M wurde in Eclipse mit der folgenden Konfiguration getestet:

Image

Anmerkungen:

  • In [WEB-INF/lib] finden Sie:
    • die Archive, die vom [ibatis SqlMap]-Tool benötigt werden, das für den Zugriff auf das Firebird-DBMS zuständig ist: ibatis-*.jar
    • die für das [Spring]-Tool benötigte Datei: spring.jar
    • den JDBC-Treiber für das [Firebird]-DBMS: firebirdsql-full.jar
    • die für die Protokollierung erforderlichen Archive: log4-*.jar, commons-logging.jar
    • die drei Archive für das getestete Modell: istia.st.articles.*.jar
    • das für das [junit]-Testtool benötigte Archiv
  • In [WEB-INF/src] befinden sich die Konfigurationsdateien, die von Eclipse automatisch nach [WEB-INF/classes] kopiert werden:
    • die Konfigurationsdateien für das [sqlmap]-Tool: sqlmap-config-firebird.xml, articles.xml
    • die Konfigurationsdateien des [spring]-Tools: spring-config-test-dao.xml, spring-config-test-domain.xml
    • die Konfigurationsdatei für das [log4j]-Tool: log4j.properties
  • Im Paket [istia.st.articles.tests] finden Sie die Modelltestklassen

3.3.6.1. Tests für die [dao]-Schicht

Die JUnit-Testklasse für die [dao]-Schicht sieht wie folgt aus. Wenn du sie liest, verstehst du besser, wie die Methoden der [IArticlesDao]-Schnittstelle 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 the ArticlesDaoSqlMap class
public class JunitModeleDaoArticles extends TestCase {

     // an instance of the class under test
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // retrieves a data access instance
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }

    public void testGetAllArticles() {
         // displays articles
        listArticles();
    }

    public void testClearAllArticles() {
         // empties item table
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
    }

    public void testAjouteArticle() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         //the poster
        listArticles();
    }

    public void testSupprimeArticle() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // delete
        articlesDao.supprimeArticle(4);
         // reads the ARTICLES table
        articles = articlesDao.getAllArticles();
        assertEquals(1, articles.size());
         // displays the table
        listArticles();
    }

    public void testModifieArticle() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        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");
         // modification
        articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
         // getById
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getPrix(), 44, 1e-6);
         // displays the table
        listArticles();
    }

    public void testGetArticleById() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        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() {
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
         // display read articles
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

    public void testChangerStockArticle() throws InterruptedException {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // insertion
        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);
         // creation of 100 threads to update the stock of item 3
        Thread[] taches = new Thread[100];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
            taches[i].start();
        }
         // we wait for the end of threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // retrieve item 3 and check stock
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        assertEquals(1, unArticle.getStockActuel());
         // modification stock article 4
        boolean erreur = false;
        int nbLignes = articlesDao.changerStockArticle(4, -100);
        assertEquals(0, nbLignes);
         // displays the table
        listArticles();
    }
}

Kommentare:

  • Die Testklasse speichert mithilfe ihrer setUp-Methode eine Instanz der zu testenden Klasse:
1
2
3
4
5
6
7
8
    // une instance de la classe testée
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
        // récupère une instance d'accès aux données
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }
  • Das zu testende Objekt wird von [Spring] bereitgestellt. Oben fordern wir das Spring-Bean mit dem Namen [articlesDao] an. Dieses 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>
     <!-- data access class -->
    <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 gezeigt, ist die Bean [articlesDao] eine Instanz der Klasse [istia.st.articles.dao.ArticlesDaoSqlMap]. Diese Klasse verfügt über einen Konstruktor, der den Namen der Konfigurationsdatei des [SqlMap]-Tools als Parameter entgegennimmt. Dieser Name wird hier angegeben. Es handelt sich um [sqlmap-config-firebird.xml]. Diese Datei wurde bereits beschrieben. Sie enthält alle notwendigen Informationen für den Zugriff auf die DBMS-Daten.

Die Methode [testChangerStockArticle] erstellt 100 Threads, die für die Verringerung des Lagerbestands eines bestimmten Artikels zuständig sind. Der Zweck hierbei ist es, den gleichzeitigen Zugriff auf das DBMS zu testen. Da die Methode [changerStockArticle] der Klasse [istia.st.articles.dao.ArticlesDaoSqlMap] synchronisiert wurde, besteht dieser Test. Wenn wir die Synchronisation entfernen, besteht der Test nicht mehr. Die Klasse, die für die Aktualisierung des Lagerbestands zuständig ist, sieht wie folgt aus:

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() {
         // follow-up
        System.out.println(name + " lancé");
         // modification stock article 3
        articlesDao.changerStockArticle(3, -1);
         // follow-up
        System.out.println(name + " terminé");
    }
}
  • Die obige Klasse verringert den Bestand von Artikel Nr. 3 um 1

3.3.6.2. [Domain] Layer-Tests

Die JUnit-Testklasse für die [Domain]-Schicht sieht wie folgt aus:

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 the ArticlesDaoSqlMap class
public class JunitModeleDomainArticles extends TestCase {

     // an instance of the domain access class
    private IArticlesDomain articlesDomain;

     // an instance of the data access class
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // retrieves a domain access instance
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
         // retrieves a data access instance
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }

     // retrieve a specific item
    public void testGetArticleById() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        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");
    }

     // screen display
    private void listArticles() {
         // reads the ARTICLES table
        List articles = articlesDomain.getAllArticles();
         // display read articles
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

     // article purchases
    public void testAchatPanier(){
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // reads the ARTICLES table
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // create a basket with two purchases
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // checks
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // shopping cart validation
        articlesDomain.acheter(panier);
         // checks
        assertEquals(0,articlesDomain.getErreurs().size());
        assertEquals(0,panier.getAchats().size());
         // search article n° 3
        article3=articlesDomain.getArticleById(3);
        assertEquals(20,article3.getStockActuel());
         // search article n° 4
        article4=articlesDomain.getArticleById(4);
        assertEquals(30,article4.getStockActuel());
         // new basket
        panier.ajouter(new Achat(article3,100));
         // shopping cart validation
        articlesDomain.acheter(panier);
         // checks - we bought too much
         // we must have an error
        assertEquals(1,articlesDomain.getErreurs().size());
         // search article n° 3
        article3=articlesDomain.getArticleById(3);
         // its stock must not have changed
        assertEquals(20,article3.getStockActuel());    
    }

     // withdraw purchases
    public void testRetirerAchats(){
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // reads the ARTICLES table
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // create a basket with two purchases
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // checks
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // add a previously purchased item
        panier.ajouter(new Achat(article3,10));
         // checks
         // the total must be increased to 1000
        assertEquals(1000.0,panier.getTotal(),1e-6);
         // always 2 items in the basket
        assertEquals(2,panier.getAchats().size());
         // qty item 3 increased to 20
        Achat achat=(Achat)panier.getAchats().get(0);
        assertEquals(20,achat.getQte());
         // article 3 is removed from the basket
        panier.enlever(3);
         // checks
         // the total must be increased to 400
        assertEquals(400.0,panier.getTotal(),1e-6);
         // 1 item only in basket
        assertEquals(1,panier.getAchats().size());
         // this must be article no. 4
        assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
    }
}

Kommentare:

  • Die Testklasse verwendet ihre setUp-Methode, um eine Instanz der zu testenden Klasse sowie eine Instanz der Datenzugriffsklasse zu speichern. Dieser letzte Punkt ist umstritten. Theoretisch sollte die Testklasse keinen Zugriff auf die [DAO]-Schicht benötigen, von der sie eigentlich gar nichts wissen sollte. Hier haben wir diese „Ethik“ außer Acht gelassen, die uns, wenn wir sie befolgt hätten, dazu gezwungen hätte, neue Methoden in unserer [IArticlesDomain]-Schnittstelle zu erstellen.
    // une instance de la classe d'accès au domaine
    private IArticlesDomain articlesDomain;

    // une instance de la classe d'accès aux données
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
        // récupère une instance d'accès au domaine
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
        // récupère une instance d'accès aux données
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }
  • Das zu testende Objekt wird von [Spring] bereitgestellt. Oben fordern wir das Spring-Bean mit dem Namen [articlesDomain] an. Dieses 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>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

Wie oben gezeigt, ist die Bean [articlesDomain] eine Instanz der Klasse [istia.st.articles.domain.AchatsArticles]. Diese Klasse verfügt über einen Konstruktor, der als Parameter ein Objekt erwartet, das Zugriff auf die [dao]-Schicht vom Typ [IArticlesDao] bietet. Hier legt die Konfigurationsdatei fest, dass dieses Objekt die Bean mit dem Namen [articlesDao] ist. Dies zwingt Spring dazu, 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:

1
2
3
4
        // récupère une instance d'accès au domaine
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");

Das [articlesDomain]-Bean wird dann abgerufen. Beim zweiten Aufruf von Spring:

1
2
3
        // récupère une instance d'accès aux données
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");

[Spring] gibt einfach eine Referenz auf die [articlesDao]-Bean zurück, die bereits beim vorherigen Aufruf erstellt wurde. Dies ist das Singleton-Prinzip. Wenn Sie eine Bean von Spring anfordern, instanziiert Spring diese, falls sie noch nicht existiert; andernfalls gibt Spring eine Referenz auf die vorhandene Bean zurück.

3.4. Dreischichtige MVC-Webanwendung

Als Nächstes möchten wir die folgende dreischichtige Webanwendung erstellen:

Die Anwendung wird eine MVC-Architektur haben. Das M-Modell wurde bereits geschrieben und getestet. Es handelt sich um das zuvor beschriebene Modell. Es wird uns in drei Archiven zur Verfügung gestellt [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception]. Wir müssen den C-Controller und die V-Ansichten schreiben.

Betrachten wir zunächst einen klassischen Ansatz, bei dem:

  • der Controller C von einem einzigen Servlet verwaltet wird
  • die Views V von JSP-Seiten verwaltet werden

3.5. MVC-Architektur auf Basis eines Controller-Servlets und JSP-Seiten

Die MVC-Architektur der Anwendung sieht wie folgt aus:

M = Modell
Geschäftsklassen, Datenzugriffsklassen und die Datenbank
V = Ansichten
JSP-Seiten
C = Controller
das Servlet, das Client-Anfragen verarbeitet

3.5.1. Das Modell

Es wurde bereits 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 am Anfang dieses Dokuments vorgestellt wurden:

LIST
list.jsp
Die Ansichten befinden sich im Ordner [vues] der Anwendung
INFO
info.jsp
WARENKORB
Warenkorb.jsp
Warenkorb leer
empty-cart.jsp
FEHLER
Fehler.jsp

3.5.3. Der Controller

Der Controller besteht aus einem einzigen Servlet namens [WebArticles]. Er verarbeitet verschiedene Client-Anfragen. Diese Anfragen werden durch das Vorhandensein eines [action]-Parameters in der HTTP-Anfrage des Clients identifiziert:

Anfrage
Bedeutung
Controller-Aktion
Mögliche Antworten
action=list
Der Client möchte die Liste der
Elemente
- fordert die Liste der Artikel vom
Unternehmen
- [LIST]
- [ERRORS]
action=info
Der Client fordert
Informationen zu einem der in der Ansicht angezeigten Elemente an
[LIST]
- fordert das Element aus der Geschäftsschicht an
- [INFO]
- [ERRORS]
action=purchase
Der Kunde kauft einen Artikel
- fordert den Artikel von der Geschäftsschicht an und
legt ihn in den Warenkorb des Kunden
- [INFO] bei Mengenfehler
- [LIST] wenn kein Fehler vorliegt
action=Kauf entfernen
Der Kunde möchte einen
Kauf aus seinem Warenkorb entfernen
- den Warenkorb aus der Sitzung abrufen und ändern
- [WARENKORB]
- [WARENKORB LEEREN]
- [FEHLER]
action=cart
Der Kunde möchte seinen
Warenkorb
- ruft den Warenkorb aus der Sitzung ab
- [WARENKORB]
- [WARENKORB LEER]
- [FEHLER]
action=validate-cart
Der Kunde hat seinen Einkauf abgeschlossen
und geht zur Kasse
– aktualisiert die Datenbank mit den Lagerbeständen der
gekauften Artikeln
- entfernt die Artikel, für die
bereits bestätigt wurden
- [LISTE]
- [FEHLER]

3.5.4. Anwendungskonfiguration

Wir werden die Anwendung so konfigurieren, dass sie hinsichtlich Änderungen wie den folgenden so flexibel wie möglich ist:

  1. Änderungen an den URLs der verschiedenen Ansichten
  2. Änderungen an den Klassen, die die Schnittstellen [IArticlesDao] und [IArticlesDomain] implementieren
  3. Änderungen am DBMS, an der Datenbank oder an der Artikeltabelle

3.5.5. Änderungen an den URLs

Die Namen der Ansichts-URLs werden zusammen mit einigen anderen Parametern in die Konfigurationsdatei [web.xml] der Anwendung aufgenommen:

<?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 [web.xml]

  • die URLs der verschiedenen Ansichten der Anwendung
  • den Namen [springConfigFileName] der Spring-Konfigurationsdatei, die die Erstellung von Singleton-Objekten für den Zugriff auf die Geschäfts- und DAO-Schichten ermöglicht
  • die Ansicht [/vues/index.jsp], die angezeigt wird, wenn die vom Client angeforderte URL /<context> lautet, wobei <context> der Anwendungskontext ist

3.5.6. Änderung der Klassen, die die Schnittstellen implementieren

Im Sinne der dreischichtigen Architektur müssen die Schichten voneinander isoliert sein. Diese Isolierung wird wie folgt erreicht:

  • Schichten kommunizieren über Schnittstellen miteinander, nicht über konkrete Klassen
  • Der Code einer Schicht instanziiert niemals selbst die Klasse einer anderen Schicht, um sie zu verwenden. Er fordert lediglich eine Instanz der Schnittstellenimplementierung von einem externen Tool – in diesem Fall [Spring] – für die Schicht an, die er verwenden möchte. Dazu muss er nicht den Namen der Implementierungsklasse kennen, sondern lediglich den Namen des Spring-Beans, für das er eine Referenz wünscht.

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>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

Um auf die [Business]-Schicht zuzugreifen, kann eine Klasse in der [Benutzeroberfläche, UI]-Schicht die [articlesDomain]-Bean anfordern. Spring instanziiert dann ein Objekt vom Typ [istia.st.articles.domain.AchatsArticles]. Für diese Instanziierung benötigt es eine Bean vom Typ [articlesDao], d. h. ein Objekt vom Typ [istia.st.articles.dao.ArticlesDaoSqlMap]. Spring instanziiert dann ein solches Objekt. Diese Instanziierung basiert auf den Informationen in der Datei [sqlmap-config-firebird.xml], der Konfigurationsdatei für den Datenzugriff über SqlMap. Am Ende des Vorgangs verfügt die [UI]-Klasse, die die [articlesDomain]-Bean angefordert hat, über die gesamte Kette, die sie mit den DBMS-Daten verbindet:

3.5.7. Änderungen in Bezug auf das DBMS oder die Datenbank

Die Unabhängigkeit der Webanwendung von Änderungen am DBMS oder an der Datenbank wird hier durch die Konfigurationsdateien von SqlMap gewährleistet. Es gibt zwei davon:

  1. 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 bezieht sich auf eine Firebird-Datenbank. Ändern Sie einfach den Namen des JDBC-Treibers, um mit einem anderen DBMS zu arbeiten.

  1. Die Datei [articles.xml], die die verschiedenen von der Anwendung benötigten SQL-Anweisungen 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">

     <!-- an alias to the istia.st.articles.dao.Article class -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- mapping ORM :  row table ARTICLES - instance class Article -->
  <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>

     <!-- query SQL to obtain all items -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- query SQL to delete all items -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- the SQL query to insert an article -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#id#,#nom#,#prix#,#stockactuel#,#stockminimum#)
</statement>

     <!-- the SQL query to delete a given item -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- query SQL to modify a given item -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#nom#,
    prix=#prix#,stockactuel=#stockactuel#,stockminimum=#stockminimum# where
    id=#id#
</statement>

     <!-- the SQL query to obtain a given item -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- query SQL to modify the stock of a given item -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#mouvement#
    where id=#id# and stockActuel+#mouvement#&gt;=0
</statement>
</sqlMap>

Sollten sich die Namen der Produkttabelle oder der Spalten ändern, müssten wir die Abfragen in dieser Konfigurationsdatei umschreiben, ohne den Java-Code ändern zu müssen. 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 [webarticles]-Anwendung

Eine Java-Webanwendung ist ein Puzzle mit vielen Teilen. Durch die Verwendung einer MVC-Architektur erhöht sich die Anzahl dieser Teile in der Regel. Die Struktur der [webarticles]-Anwendung unter [Eclipse] sieht wie folgt aus:

Allgemeine Struktur – unten sind die
 Java-Archive, die von
 Eclipse verwendet werden.
spring: für Spring
ibatis: für SqlMap
log4j, commons-logging: für
 von Spring und SqlMap
firebird: für das Firebird-DBMS
mysql: für das MySQL-DBMS
jstl, standard: für das
 JSTL-Tag-Bibliothek
der Java-Quellordner: enthält den Java-Code
 sowie die Konfigurationsdateien
 Eclipse kopiert automatisch
 diese Dateien
nach [WEB-INF/classes].
Dort findet die Anwendung sie.
Der [WEB-INF]-Ordner der Anwendung: enthält den
 [web.xml]-Deskriptor der Anwendung sowie die
 JSTL-Bibliotheksdefinitionsdateien
die Ansichten

3.5.9. JSP-Ansichten

JSP-Ansichten verwenden die JSTL-Tag-Bibliothek.

3.5.9.1. header.jsp

Um die Konsistenz zwischen den verschiedenen Ansichten zu gewährleisten, verwenden diese denselben Header, der den Anwendungsnamen zusammen mit dem Menü anzeigt:

Das Menü ist dynamisch und wird vom Controller festgelegt. Der Controller fügt der an die JSP-Seite gesendeten Anfrage ein Schlüsselattribut „actions“ hinzu, dessen Wert ein Hastable[]-Array ist. Jedes Element dieses Arrays ist ein Dictionary, das dazu dient, eine Menüoption in der Kopfzeile zu generieren. Jedes Dictionary hat zwei Schlüssel:

  • href: die mit der Menüoption verknüpfte URL
  • link: der Menütext

Die anderen Ansichten der Anwendung verwenden die in [entete.jsp] definierte Kopfzeile unter Verwendung des folgenden JSP-Tags:

<jsp:include page="entete.jsp"/>

Zur Laufzeit bindet dieses Tag den Code der Seite [entete.jsp] in die JSP-Seite ein, die es enthält. Da es sich bei der Seiten-URL um eine relative URL handelt (ohne abschließendes /), wird die Seite [entete.jsp] im selben Verzeichnis gesucht wie die Seite, die das <jsp:include>-Tag 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. list.jsp

Diese Ansicht zeigt die Liste der zum Verkauf stehenden Artikel an:

Sie wird nach einer Anfrage an /main?action=list oder /main?action=cartvalidation angezeigt. Die Parameter der Controller-Anfrage lauten wie folgt:

Aktionen
Hashtable[]-Objekt – das Array der Menüoptionen
listarticles
ArrayList mit Objekten vom Typ [Item]
message
String-Objekt – Meldung, die am unteren Rand der Seite angezeigt werden soll

Jeder [Info]-Link in der HTML-Tabelle der Artikel hat eine URL in 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 dessen Kauf:

Image

Sie wird nach einer Anfrage /main?action=infos&id=ID oder einer Anfrage /main?action=achat&id=ID angezeigt, wenn die gekaufte Menge falsch ist. Die Controller-Anfrageparameter lauten wie folgt:

actions
Hashtable[]-Objekt – das Array der Menüoptionen
item
Objekt vom Typ [Article] – anzuzeigender Eintrag
msg
String-Objekt – Meldung, die im Falle eines Fehlers bei der Menge angezeigt werden soll
qte
String-Objekt – Wert, der im Eingabefeld [Qty] angezeigt werden soll

Die Felder [msg] und [qte] werden bei einem Eingabefehler bezüglich der Menge verwendet:

Image

Diese Seite enthält ein Formular, das über die Schaltfläche [Kaufen] übermittelt wird. Die POST-Ziel-URL lautet [?action=purchase&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. cart.jsp

Diese Ansicht zeigt den Inhalt des Warenkorbs an:

Image

Sie wird nach einer Anfrage an /main?action=cart oder /main?action=remove&id=ID angezeigt. Die Controller-Anfrageparameter lauten wie folgt:

Aktionen
Hashtable[]-Objekt – das Array der Menüoptionen
cart
Objekt vom Typ [Cart] – der anzuzeigende Warenkorb

Jeder [Entfernen]-Link in der HTML-Warenkorb-Tabelle hat eine URL in der Form [?action=removeitem&id=ID], wobei ID das Feld [id] des Artikels ist, der aus dem Warenkorb entfernt werden soll.

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. emptycart.jsp

Diese Ansicht zeigt Informationen an, die darauf hinweisen, dass der Warenkorb leer ist:

Image

Sie wird nach einer Anfrage an /main?action=cart oder /main?action=remove&id=ID angezeigt. Die Controller-Anfrageparameter lauten wie folgt:

actions
Hashtable[]-Objekt – das Array der Menüoptionen

Code:

1
2
3
4
5
6
7
8
9
<%@ 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. errors.jsp

Diese Ansicht wird im Falle von Fehlern angezeigt:

Image

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] behandelt wird. Die Elemente der Controller-Anfrage lauten wie folgt:

actions
Hashtable[]-Objekt – das Array der Menüoptionen
Fehler
ArrayList mit 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 Datei [web.xml] der Anwendung 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 einfach zum Einstiegspunkt der Anwendung weiter:

1
2
3
4
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main?action=liste"/>

3.5.10. Der Controller

Wir müssen noch den Kern unserer Webanwendung schreiben, den Controller. Seine Aufgabe ist es:

  • die Anfrage des Clients abzurufen,
  • die vom Client angeforderte Aktion mithilfe von Business-Klassen zu verarbeiten,
  • die entsprechende Ansicht als Antwort zu senden.

3.5.10.1. Initialisierung des Controllers

Wenn die Controller-Klasse vom Servlet-Server geladen wird, wird ihre [init]-Methode ausgeführt. Dies geschieht nur einmal. Sobald der Controller in den Speicher geladen wurde, verbleibt er dort und verarbeitet Anfragen von verschiedenen Clients. Jeder Client wird von einem separaten Ausführungsthread bedient, sodass die Methoden des Controllers gleichzeitig von verschiedenen Threads ausgeführt werden. Beachten Sie, dass der Controller aus diesem Grund keine Felder haben darf, die von seinen Methoden geändert werden könnten. Seine Felder müssen schreibgeschützt sein. Sie werden durch die [init]-Methode initialisiert, was deren Hauptaufgabe ist. Diese Methode hat die einzigartige Eigenschaft, dass sie nur einmal von einem einzigen Thread ausgeführt wird. Daher gibt es keine Probleme mit dem gleichzeitigen Zugriff auf die Felder des Controllers innerhalb dieser Methode. Der Zweck der [init]-Methode besteht darin, die von der Webanwendung benötigten 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:

  • den privaten Feldern des Controllers
  • im Ausführungskontext der Anwendung (ServletContext)

Die [init]-Methode der Anwendung [webarticles] führt die folgenden Aktionen aus:

  • die Datei [web.xml] auf die für den ordnungsgemäßen Betrieb der Anwendung erforderlichen Parameter überprüfen. Diese wurden in Abschnitt 3.5.5 beschrieben.
  • Initialisierung eines privaten Feldes [ArrayList errors], das eine Liste aller Fehler enthält. Diese Liste ist leer, wenn keine Fehler vorliegen, existiert aber dennoch.
  • Wenn Fehler aufgetreten sind, bricht die [init]-Methode 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 die benötigte Bean vom Spring-Framework an. Dieser Instanziierungsvorgang kann zu verschiedenen Fehlern führen. In diesem Fall werden diese wiederum im Feld [errors] des Controllers gespeichert.

3.5.10.2. doGet-, doPost-Methoden

Diese beiden Methoden bearbeiten HTTP-GET- und -POST-Anfragen von Clients. Sie werden austauschbar behandelt. Die [doPost]-Methode kann somit zur [doGet]-Methode umleiten oder umgekehrt. Die Client-Anfrage wird wie folgt verarbeitet:

  • Das Feld [errors] wird überprüft. Ist es nicht leer, bedeutet dies, dass bei der Initialisierung der Anwendung Fehler aufgetreten sind und die Anwendung nicht ausgeführt werden kann. In diesem Fall wird die Ansicht [ERRORS] als Antwort gesendet.
  • Der Parameter [action] der Anfrage wird abgerufen und überprüft. Wenn er keiner bekannten Aktion entspricht, wird die Ansicht [ERRORS] mit einer entsprechenden Fehlermeldung gesendet.
  • Ist der Parameter [action] gültig, wird die Anfrage des Clients zur Verarbeitung an eine für die Aktion spezifische Prozedur weitergeleitet. Die Prozedur, die die Aktion [uneAction] verarbeitet, hat die folgende Signatur:
1
2
3
4
5
6
7
8
/**
   * @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. Behandlung verschiedener Aktionen

Die Methoden, die die verschiedenen möglichen Aktionen der Anwendung verarbeiten, lauten wie folgt:

Methode
request
Verarbeitung
Mögliche Antworten
doList
GET /main?action=list
- Liste der Elemente anfordern
aus der Business-Klasse
- anzeigen
[LIST] oder [ERRORS]
doInfo
GET /main?action=info&id=ID
- das Element mit der ID=ID aus
der Business-Klasse
- es anzeigen
[INFO] oder [ERRORS]
doPurchase
POST /main?action=purchase&id=ID
- Die gekaufte Menge ist in den übermittelten Parametern enthalten
- Fordere den Artikel mit id=ID von
der Business-Klasse
- Füge ihn im
der Kundensitzung
[LIST] oder [INFO]
oder [ERRORS]
doRemovePurchase
GET /main?action=removePurchase&id=ID
- Entferne den Artikel mit der ID=ID aus der
Einkaufsliste in der
der Kundensitzung
[CART]
doCart
GET /main?action=cart
- Zeige die
Client-Sitzung
[CART] oder [EMPTY_CART]
doCartValidation
GET /main?action=cartvalidation
- Verringere die
Lagerbestände aller Artikel
im
[LIST] oder [ERRORS]
[LIST] oder [ERRORS]

3.5.10.4. Der Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
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 fields
    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() {
         // retrieve servlet initialization parameters
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // we memorize the error
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // mistakes?
        if (erreurs.size() != 0) {
            return;
        }
         // create a IArticlesDomain business layer access object
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // we memorize the error
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // memorize certain application urls
        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);

         // it's over
        return;
    }


    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {

         // check how the initialization of the servelet went
        if (erreurs.size() != 0) {
             // do we have the url of the error page?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // action is processed
        String action = request.getParameter("action");
        if (action == null) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // article info
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // purchase an item
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // basket display
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // remove an item from the basket
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // shopping cart validation
            doValidationPanier(request, response);
            return;
        }
         // unknown share
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // the error page is displayed
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // end
        return;
    }


    private void doValidationPanier(
        HttpServletRequest request,
        HttpServletResponse response)
        throws ServletException, IOException {

         // the buyer has confirmed his basket
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // validate this basket
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // not normal
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // error recovery
        ArrayList erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
            request.setAttribute(
                "actions",
                new Hashtable[] { hActionListe, hActionPanier });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // displays the list of items
        request.setAttribute("message", "Votre panier a été validé");
        doListe(request, response);
         // end
        return;
    }


    private void doRetirerAchat(
        HttpServletRequest request,
        HttpServletResponse response)
        throws ServletException, IOException {

         // remove a purchase from the basket
        try {
            Panier panier =
                (Panier) request.getSession().getAttribute("panier");
            String strIdAchat = request.getParameter("id");
            panier.enlever(Integer.parseInt(strIdAchat));
        } catch (NumberFormatException ignored) {
        } catch (NullPointerException ignored) {
        }
         // the basket is displayed
        doPanier(request, response);
    }


    private void doPanier(
        HttpServletRequest request,
        HttpServletResponse response)
        throws ServletException, IOException {

         // the basket is displayed
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // empty basket?
        if (panier == null || panier.getAchats().size() == 0) {
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
                .forward(request, response);
             // end
            return;
        }
         // there's something in the basket
        request.setAttribute("panier", panier);
        request.setAttribute(
            "actions",
            new Hashtable[] { hActionListe, hActionValidationPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_PANIER))
            .forward(request, response);
         // end
        return;
    }


    private void doAchat(
        HttpServletRequest request,
        HttpServletResponse response)
        throws ServletException, IOException {

         // purchase an item
         // we recover the quantity
        int qté = 0;
        try {
            qté = Integer.parseInt(request.getParameter("qte"));
            if (qté <= 0)
                throw new NumberFormatException();
        } catch (NumberFormatException ex) {
             // wrong qty
            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);
             // end
            return;
        }
         // retrieve the client session
        HttpSession session = request.getSession();
         // we create the purchase
        Article article = (Article) session.getAttribute("article");
        Achat achat = new Achat(article, qté);
         // the purchase is added to the customer's basket
        Panier panier = (Panier) session.getAttribute("panier");
        if (panier == null) {
            panier = new Panier();
            session.setAttribute("panier", panier);
        }
        panier.ajouter(achat);
         // we return to the list of items
        String url = config.getInitParameter(URL_MAIN) + "?action=liste";
        getServletContext().getRequestDispatcher(url).forward(
            request,
            response);
         // end
        return;
    }


    private void afficheDebugInfos(
        HttpServletRequest request,
        HttpServletResponse response,
        ArrayList infos)
        throws ServletException, IOException {

         // displays the list of items
        request.setAttribute("infos", infos);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_DEBUG))
            .forward(request, response);
         // end
        return;
    }


    public void doPost(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {

         // idem get
        doGet(request, response);
    }


    private void doInfos(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {

         // error list
        ArrayList erreurs = new ArrayList();
         // retrieve the requested id
        String strId = request.getParameter("id");
         // anything?
        if (strId == null) {
             // not normal
            erreurs.add("action incorrecte([infos,id=null]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // transform strId into an integer
        int id = 0;
        try {
            id = Integer.parseInt(strId);
        } catch (Exception ex) {
             // not normal
            erreurs.add("action incorrecte([infos,id=" + strId + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // the key item id is requested
        Article article = null;
        try {
            article=articlesDomain.getArticleById(id);
        } catch (UncheckedAccessArticlesException ex) {
             // not 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) {
             // not normal
            erreurs.add("Article de clé [" + id + "] inexistant");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // put the article in the session
        request.getSession().setAttribute("article", article);
         // the info page is displayed
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        //    request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_INFOS))
            .forward(request, response);
         // end
        return;
    }

    private void afficheErreurs(
        HttpServletRequest request,
        HttpServletResponse response,
        ArrayList erreurs)
        throws ServletException, IOException {

         // the error page is displayed
        request.setAttribute("erreurs", erreurs);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
            .forward(request, response);
         // end
        return;
    }


    private void doListe(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {

         // list of errors
        ArrayList erreurs = new ArrayList();
         // the list of items is requested
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // we memorize the error
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // mistakes?
        if (erreurs.size() != 0) {
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // displays the list of items
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // end
        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 zu lesen und zu verstehen. Wir hoffen, dass die Kommentare dabei helfen.

3.5.10.5. Anwendungstests

Sehen wir uns ein paar Screenshots der Tests an. Zunächst die Startseite der Anwendung:

Image

Die angeforderte URL lautete tatsächlich [http://localhost:8080/webarticles]. Der Leser wird sehen, 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 daher eine Weiterleitung zur URL [http://localhost:8080/webarticles/main?action=liste], wie die Browser-URL im Screenshot zeigt. Die URL [/main?action=liste] wurde somit angefordert. Ebenfalls in [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] wird das Servlet [webarticles] dem Servlet [istia.st.articles.web.WebArticles] zugeordnet:

        <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 [init]-Methode wird ausgeführt:

    public void init() {
         // retrieve servlet initialization parameters
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // we memorize the error
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // mistakes?
        if (erreurs.size() != 0) {
            return;
        }
         // create a IArticlesDomain business layer access object
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // we memorize the error
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // memorize certain application urls
        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);

         // it's over
        return;
    }

Kommentare: Die [init]-Methode

  • prüft auf das Vorhandensein bestimmter Konfigurationsparameter
  • instanziiert einen Service, um über Spring auf die Anwendungsdomäne zuzugreifen
  • legt eine Fehlerliste an, um etwaige Initialisierungsfehler anzuzeigen
  • eine Reihe privater Felder:
    • errors: die Liste der von [init] erkannten Fehler
    • [hActionListe, hActionPanier, hActionValidationPanier]: Wörterbücher. Jedes enthält die Informationen, die zur Anzeige einer Option im Hauptmenü der Ansicht [entete.jsp] erforderlich sind
    • acticlesDomain: der Dienst für den Zugriff auf das Anwendungsmodell

Die Methode [init] wird nur einmal ausgeführt, beim ersten Laden des Servlets. Danach wird je nach [GET, POST]-Typ 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 {
         // check how the initialization of the servelet went
        if (erreurs.size() != 0) {
             // do we have the url of the error page?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // action is processed
        String action = request.getParameter("action");
        if (action == null) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // article info
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // purchase an item
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // basket display
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // remove an item from the basket
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // shopping cart validation
            doValidationPanier(request, response);
            return;
        }
         // unknown share
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // the error page is displayed
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // end
        return;
    }
  • Die Methode [doGet] prüft zunächst, ob nach der Methode [init] Initialisierungsfehler aufgetreten sind. Ist dies der Fall, wird die Ansicht [ERRORS] angezeigt und der Prozess wird beendet.
  • Andernfalls ruft sie den Parameter [action] aus der Anfrage des Clients ab. Beachten Sie, dass die Anwendung so konzipiert wurde, dass sie auf Anfragen reagiert, die einen [action]-Parameter enthalten müssen.
  • Sie führt die mit der Aktion verknüpfte Methode aus. In diesem Fall ist das die [doListe]-Methode.

Die [doListe]-Methode lautet wie folgt:

    private void doListe(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {

         // error list
        ArrayList erreurs = new ArrayList();
         // the list of items is requested
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // we memorize the error
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // mistakes?
        if (erreurs.size() != 0) {
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // displays the list of items
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // end
        return;
    }
  • Erinnern Sie sich daran, dass die [init]-Methode den Dienst für den Zugriff auf das Anwendungsmodell (Domänenebene) in einem privaten Feld des Servlets gespeichert hat:
    // champs privés
    private IArticlesDomain articlesDomain = null;
  • Mit diesem Zugriffsdienst können wir die Liste der Artikel abrufen:
        // la liste des erreurs
        ArrayList erreurs = new ArrayList();
        // on demande la liste des articles
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
            // on mémorise l'erreur
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
  • Wenn Fehler auftreten, wird die Ansicht [ERRORS] gesendet:
         // mistakes?
        if (erreurs.size() != 0) {
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
  • ansonsten wird die Ansicht [LIST] gesendet:
1
2
3
4
5
6
7
         // displays the list of items
        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 verlief alles reibungslos und wir haben die Ansicht [LIST] erfolgreich abgerufen. Der Leser ist eingeladen, den Code für die Ansicht [LIST] zu überprüfen, um sicherzustellen, dass die von dieser Ansicht erwarteten dynamischen Parameter tatsächlich oben vom Controller bereitgestellt werden. Die gleiche Art der Überprüfung sollte für jede Ansicht durchgeführt werden:

  • Suchen Sie die dynamischen Parameter der Ansicht
  • stellen Sie sicher, dass der Controller diese in die an die Ansicht übergebenen Request-Attribute einfügt

Wir werden nun kurz den Ablauf der Bildschirme skizzieren, die einem Anwendungsbenutzer angezeigt werden. Der Leser ist eingeladen, jedes Mal einer ähnlichen Argumentationskette wie zuvor zu folgen:

Aus der Liste der Elemente kann der Benutzer ein Element auswählen:

Der Käufer kann hier Artikel Nr. 3 kaufen. Geben wir mal eine falsche Menge ein:

Der Fehler wurde markiert. Nun wollen wir ein paar Artikel kaufen:

Der Kauf wurde erfasst und die Liste der Artikel wurde erneut angezeigt. Schauen wir uns den Warenkorb an:

Der Artikel befindet sich tatsächlich im Warenkorb. Nehmen wir ihn heraus:

Der Artikel wurde aus dem Warenkorb entfernt und der Warenkorb wurde neu geladen. Er ist nun leer.

Kaufen wir 100 Stück von Artikel Nr. 3 und 2 Stück von Artikel Nr. 4:

Der Kauf von Artikel Nr. 3 war nicht möglich, da wir 100 Stück kaufen wollten, aber nur 30 auf Lager waren. Dieser Artikel blieb im Warenkorb:

Artikel Nr. 4 wurde jedoch gekauft, wie der neue Lagerbestand von 39 (40-1) zeigt:

3.6. MVC-Architektur mit Struts

3.6.1. Allgemeine Anwendungsarchitektur

Werfen wir noch einmal einen Blick auf die MVC-Architektur der Anwendung:

In der vorherigen Version:

  • wurde der Controller von einem Servlet verwaltet
  • wurden die Ansichten von JSP-Seiten verwaltet
  • wurde das Modell von drei .jar-Dateien verwaltet

In der Struts-Version:

  • wird der Controller von einem Servlet übernommen, das von Struts' generischem [ActionServlet] abgeleitet ist
  • Die Ansichten werden von denselben JSP-Seiten wie zuvor verwaltet, mit einigen geringfügigen Unterschieden
  • Das Modell wird von denselben drei Archiven verwaltet

Wir werden sehen, dass die Migration der bisherigen Anwendung zu Struts die folgenden Aufgaben umfasst:

  • Aktionen, die zuvor in bestimmten Methoden des Servlets/Controllers abgewickelt wurden, werden nun von Instanzen von Klassen abgewickelt, die von der Struts-Klasse [Action] abgeleitet sind
  • Erstellen der Konfigurationsdateien [web.xml] und [struts-config.xml]
  • Einige Änderungen an den JSP-Seiten vornehmen

Sehen wir uns die von Struts verwendete generische MVC-Architektur an:

M = Modell
Geschäftsklassen, Datenzugriffsklassen und die Datenbank
V = Ansichten
JSP-Seiten
C = Controller
das Servlet, das Client-Anfragen verarbeitet, die [Action]-Objekte und die mit Formularen verknüpften [ActionForm]-Beans.
  • 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 manchen Fällen müssen Sie es möglicherweise erweitern. In einfachen Fällen ist dies nicht erforderlich. Dieses generische Servlet bezieht die benötigten Informationen aus einer Datei, die meist struts-config.xml heißt.
  • Wenn die Client-Anfrage Formularparameter enthält, legt der Controller diese in einem Bean-Objekt ab. Die im Laufe der Zeit erstellten Bean-Objekte werden in der Sitzung oder der Client-Anfrage gespeichert. Dieses Verhalten ist konfigurierbar. Sie müssen nicht neu erstellt werden, wenn sie bereits vorhanden sind.
  • In der Konfigurationsdatei „struts-config.xml“ ist jede URL, die programmgesteuert verarbeitet werden soll (und somit keiner JSP-Ansicht entspricht, die direkt angefordert werden könnte), mit bestimmten Informationen verknüpft:
    • dem Namen der Action-Klasse, die für die Verarbeitung 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 (wie beim Absenden eines Formulars an den Controller), wird der Name des Beans angegeben, das für die Speicherung der Formulardaten zuständig ist.
  • Anhand dieser Informationen aus der Konfigurationsdatei kann der Controller beim Empfang einer URL-Anfrage von einem 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. Der Entwickler platziert daher den Code, der die Gültigkeit der Formulardaten überprüft, innerhalb der validate-Methode. Wenn die Daten als ungültig befunden werden, fährt der Controller nicht fort. Er übergibt die Kontrolle an eine Ansicht, deren Namen er in seiner Konfigurationsdatei findet. Die Interaktion ist dann abgeschlossen. Beachten Sie, dass der Entwickler festlegen kann, die Gültigkeit des Formulars nicht prüfen zu lassen. Dies erfolgt ebenfalls in der Datei struts-config.xml. In diesem Fall ruft der Controller die validate-Methode des Beans nicht auf.
  • Sind die Daten des Beans korrekt, findet keine Validierung statt oder ist kein Bean vorhanden, übergibt der Controller die Kontrolle an das mit der URL verknüpfte Action-Objekt. Dies geschieht durch Aufruf der execute-Methode dieses Objekts, wobei ihm die Referenz auf das möglicherweise erstellte Bean übergeben wird. Hier führt der Entwickler die erforderlichen Schritte aus: Er muss möglicherweise Business-Klassen oder Datenzugriffsklassen aufrufen. Am Ende der Verarbeitung gibt das Action-Objekt dem Controller den Namen der Ansicht zurück, die er als Antwort an den Client senden muss.
  • In seiner Konfigurationsdatei findet der Controller die URL, die mit dem Namen der Ansicht verknüpft ist, die er anzeigen soll. Anschließend sendet er die Ansicht. Die Interaktion mit dem Client ist abgeschlossen.

In unserer Anwendung werden wir keine [Bean]-Objekte als Pufferobjekte zwischen dem Client und den [Action]-Klassen verwenden. Das [Action]-Objekt wird die Parameter der Client-Anfrage direkt aus dem empfangenen [HttpServletRequest]-Objekt abrufen. Dies erleichtert die Portierung unserer ursprünglichen Anwendung. Die endgültige Architektur unserer Anwendung wird daher wie folgt aussehen:

M = Modell
Geschäftsklassen, Datenzugriffsklassen und die Datenbank
V = Views
die JSP-Seiten
C = Controller
das Servlet zur Verarbeitung von Client-Anfragen, [Action]-Objekte

3.6.2. Das Modell

Es wurde bereits vorgestellt. Es besteht aus den Java-Archiven [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.6.3. Anwendungskonfiguration

3.6.3.1. Allgemeine Architektur

Die allgemeine Architektur des Eclipse-Projekts sieht wie folgt aus:

Image

3.6.3.2. Konfiguration des Datenzugriffs

Da die Datenzugriffsschnittstelle unverändert bleibt, sind die zugehörigen Konfigurationsdateien dieselben wie in der vorherigen Version. Sie sind in [WEB-INF/src] definiert:

Image

Im 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 Verzeichnis „archives“

In [WEB-INF/lib] finden Sie dieselben Bibliotheken wie in der vorherigen Version sowie die von Struts benötigte:

Image

3.6.3.4. Anwendungskonfiguration

Die Anwendung wird über zwei Dateien konfiguriert: [web.xml, struts-config.xml] im Ordner [WEB-INF]:

Image

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 [views/index.jsp] (welcome-file)
  • URL-Anfragen der Form *.do werden an das [strutswebarticles]-Servlet weitergeleitet (servlet-mapping)
  • Das [strutswebarticles]-Servlet ist eine Instanz der Klasse [istia.st.articles.web.struts.MainServlet] (servlet-name, servlet-class)
  • 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:
main.do
um die Liste der Artikel anzuzeigen
liste.do
um die Liste der Artikel anzuzeigen
info.do
um Informationen zu einem bestimmten Artikel anzuzeigen
purchase.do
um einen bestimmten Artikel zu kaufen
Warenkorb.do
um den Warenkorb anzuzeigen
remove-purchase.do
um einen Artikel aus dem Warenkorb zu entfernen
confirmcart.do
um den Warenkorb zu bestätigen
  • Die oben aufgeführten Aktionen entsprechen eins zu eins den Aktionen, die vom Servlet in der vorherigen Version verarbeitet wurden. Für jede davon werden die folgenden Informationen bereitgestellt:
  • den Namen der Klasse, die für die Verarbeitung dieser Aktion zuständig ist
  • die möglichen Antworten (= Ansichten) nach der Verarbeitung der Aktion. Nur eine davon wird vom Controller ausgewählt.
  • den Namen einer Nachrichtendatei für die Anwendung (message-resources). Hier existiert die Datei zwar, ist aber leer. Sie wird nicht verwendet. Sie muss im [ClassPath] der Anwendung abgelegt werden. Hier wird sie in [WEB-INF/classes] abgelegt. In Eclipse geschieht dies durch Ablage in [WEB-INF/src]:

Image

3.6.4. Die JSP-Ansichten

Die hier verwendeten JSP-Ansichten stammen ebenfalls aus der vorherigen Version. Es wurden nur sehr wenige Änderungen vorgenommen: URLs der Form [?action=XX?id=YY& ...] wurden in [/XX.do?id=YY&....] geändert. Wir wiederholen hier bereits gegebene Erklärungen, um dem Benutzer das Zurückblättern zu ersparen. Es ist wichtig zu verstehen, dass die vom Controller an die Ansicht übergebenen Informationen in beiden Versionen exakt gleich sind. In dieser Hinsicht hat sich nichts geändert.

3.6.4.1. entete.jsp

Um die Konsistenz zwischen den verschiedenen Ansichten zu gewährleisten, verwenden diese denselben Header, der den Anwendungsnamen zusammen mit dem Menü anzeigt:

Das Menü ist dynamisch und wird vom Controller definiert. Der Controller fügt der an die JSP-Seite gesendeten Anfrage ein Schlüsselattribut „actions“ mit dem zugehörigen Wert eines Hastable[]-Arrays hinzu. Jedes Element dieses Arrays ist ein Dictionary, das dazu dient, eine Menüoption in der Kopfzeile zu generieren. Jedes Dictionary hat zwei Schlüssel:

  • href: die mit der Menüoption verknüpfte URL
  • link: der Menütext

Die anderen Ansichten der Anwendung verwenden die in [entete.jsp] definierte Kopfzeile unter Verwendung des folgenden JSP-Tags:

<jsp:include page="entete.jsp"/>

Bei der Ausführung bindet dieses Tag den Code der Seite [entete.jsp] in die JSP-Seite ein, die es enthält. Da es sich bei der Seiten-URL um eine relative URL handelt (ohne abschließendes /), wird die Seite [entete.jsp] im selben Verzeichnis gesucht wie die Seite, die das <jsp:include>-Tag 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>

Kommentare: keine Änderungen gegenüber der vorherigen Version

3.6.4.2. list.jsp

Diese Ansicht zeigt die Liste der zum Verkauf stehenden Artikel an:

Sie wird nach einer Anfrage an /main.do oder /validerpanier.do angezeigt. Die Controller-Anfrageparameter lauten wie folgt:

Aktionen
Hashtable[]-Objekt – das Array der Menüoptionen
listarticles
ArrayList mit Objekten vom Typ [Item]
message
String-Objekt – Meldung, die am unteren Rand der Seite angezeigt werden soll

Jeder [Info]-Link in der HTML-Tabelle der Artikel hat eine URL in 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>

Kommentare: eine Änderung (oben hervorgehoben)

3.6.4.3. infos.jsp

Diese Ansicht zeigt Informationen zu einem Artikel an und ermöglicht dessen Kauf:

Image

Sie wird nach einer Anfrage an /infos.do?id=ID oder einer Anfrage an /achat.do?id=ID angezeigt, wenn die gekaufte Menge falsch ist. Die Elemente der Controller-Anfrage lauten wie folgt:

Aktionen
object Hashtable[] – das Array der Menüoptionen
item
Objekt vom Typ [Artikel] – anzuzeigender Eintrag
msg
String-Objekt – Meldung, die im Falle eines Fehlers bei der Menge angezeigt werden soll
qte
String-Objekt – Wert, der im Eingabefeld [Qty] angezeigt werden soll

Die Felder [msg] und [qte] werden bei einem Eingabefehler bezüglich der Menge verwendet:

Image

Diese Seite enthält ein Formular, das über die Schaltfläche [Kaufen] übermittelt wird. Die POST-Ziel-URL 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>

Kommentare: eine Änderung (oben hervorgehoben)

3.6.4.4. cart.jsp

Diese Ansicht zeigt den Inhalt des Warenkorbs an:

Image

Sie wird nach einer Anfrage an /panier.do oder /retirerachat.do?id=ID angezeigt. Die Controller-Anfrageparameter lauten wie folgt:

Aktionen
Hashtable[]-Objekt – das Array der Menüoptionen
Warenkorb
Objekt vom Typ [ShoppingCart] – der anzuzeigende Warenkorb

Jeder [Remove]-Link im HTML-Warenkorb-Array hat eine URL in der Form [removeitem.do?id=ID], wobei ID das [id]-Feld des Artikels ist, der aus dem Warenkorb entfernt werden soll.

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. emptycart.jsp

Diese Ansicht zeigt Informationen an, die darauf hinweisen, dass der Warenkorb leer ist:

Image

Sie wird nach einer Anfrage an /panier.do oder /retirerachat.do?id=ID angezeigt. Die Controller-Anfrageparameter lauten wie folgt:

Aktionen
Hashtable[]-Objekt – das Array der Menüoptionen

Code:

1
2
3
4
5
6
7
8
9
<%@ 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. errors.jsp

Diese Ansicht wird bei Fehlern angezeigt:

Image

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] behandelt wird. Die Elemente der Controller-Anfrage lauten wie folgt:

Aktionen
Hashtable[]-Objekt – das Array der Menüoptionen
Fehler
ArrayList mit 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>

Kommentare: keine Änderungen.

3.6.4.7. index.jsp

Diese Seite ist in der Datei [web.xml] der Anwendung 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 einfach zum Einstiegspunkt der Anwendung weiter:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main.do"/>

Kommentare: eine Änderung (oben hervorgehoben)

3.6.5. Der Struts-Controller

Struts verfügt über einen generischen Controller namens [ActionServlet]. Wir wissen, dass ein Servlet über eine [init]-Methode verfügt, die es ermöglicht, die Anwendung beim Start zu initialisieren. Wenn wir den generischen Controller [ActionServlet] von Struts verwenden, haben wir keinen Zugriff auf dessen [init]-Methode. Hier müssen wir beim Start der Anwendung bestimmte Aufgaben ausführen, vor allem die Instanziierung eines Modellzugriffsobjekts. Daher benötigen wir eine [init]-Methode. Wir erweitern daher die Klasse [ActionServlet] in der folgenden Klasse [MainServlet]:

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 fields
  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);

     // getters - setters
  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{

     // init parent class
    super.init();
     // retrieve servlet initialization parameters
    config = getServletConfig();
    String param = null;
    for (int i = 0; i < parameters.length; i++) {
      param = config.getInitParameter(parameters[i]);
      if (param == null) {
         // we memorize the error
        erreurs.add("Paramètre [" + parameters[i]
            + "] absent dans le fichier [web.xml]");
      }
    }
     // mistakes?
    if (erreurs.size() != 0) {
      return;
    }
     // create a IArticlesDomain business layer access object
    try {
      articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
          new ClassPathResource((String) config
              .getInitParameter(SPRING_CONFIG_FILENAME))))
          .getBean("articlesDomain");
    } catch (Exception ex) {
       // we memorize the error
      erreurs.add("Erreur de configuration de l'accès aux données : "
          + ex.toString());
      return;
    }
     // memorize certain application urls
    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);

     // it's over
    return;
  }
}

Kommentare:

  • Der Wert der Klasse liegt in ihrer [init]-Methode und ihren privaten Feldern
  • Die [init]-Methode führt dasselbe aus wie die [init]-Methode im Controller der vorherigen Version:
    • Sie prüft auf das Vorhandensein bestimmter Konfigurationsparameter
    • Sie instanziiert einen Dienst, um über Spring auf die Anwendungsdomäne zuzugreifen
    • sie richtet eine Fehlerliste ein, um eventuelle Initialisierungsfehler anzuzeigen
  • Vor Beginn der Arbeit ruft die [init]-Methode die [init]-Methode der übergeordneten Klasse [ActionServlet] auf. Diese Methode verarbeitet die Struts-Konfigurationsdatei [struts-config.xml].
  • Es werden mehrere private Felder mit ihren Zugriffsmethoden definiert:
    • errors: die Liste der von [init] erkannten Fehler
    • [hActionListe, hActionPanier, hActionValidationPanier]: Wörterbücher. Jedes enthält die Informationen, die zur Anzeige einer Option im Hauptmenü der Ansicht [entete.jsp] benötigt werden
    • acticlesDomain: der Dienst, der den Zugriff auf das Anwendungsmodell ermöglicht
  • Der Controller einer Struts-Anwendung ist für die [Action]-Klassen zugänglich, die für die Abwicklung der verschiedenen möglichen Aktionen zuständig sind. Diese Klassen haben Zugriff auf die vorgenannten privaten Felder, da diese über öffentliche Zugriffsmethoden verfügen.

Dieser Controller wird durch die Datei [web.xml] instanziiert:

<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. Struts-Anwendungsaktionen

3.6.6.1. Einführung

Jede Struts-Aktion wird als Klasse implementiert. In der vorherigen Version wurde jede Aktion als Methode im Anwendungscontroller implementiert. Das Schreiben der [Action]-Klasse umfasst in der Regel:

  • Kopieren und Einfügen der in der vorherigen Version verwendeten Methode
  • Anpassen des Codes an die Struts-Konventionen

3.6.6.2. main.do, list.do

Diese beiden Aktionen sind identisch und in [struts-config.xml] wie folgt definiert:

1
2
3
4
5
6
7
8
        <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, ergibt sich folgendes Ergebnis:

Image

Der Code für die 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 {

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();

     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

     // domain access object
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();

     // list of errors
    ArrayList erreurs = new ArrayList();
     // the list of items is requested
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // we memorize the error
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
     // mistakes?
    if (erreurs.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // displays the list of items
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");
  }
}

Kommentare:

  • Das Schreiben des Codes für eine [Action]-Klasse besteht im Wesentlichen aus dem Schreiben des Codes für ihre [execute]-Methode
  • In der Controller-Instanz wurde eine bestimmte Menge an Informationen gespeichert. Wir rufen eine Referenz darauf ab mit:
     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
  • Wir rufen die Liste der vom Controller gespeicherten Initialisierungsfehler ab. Ist diese Liste nicht leer, wird die Ansicht [ERRORS] an den Client gesendet:
1
2
3
4
5
6
7
8
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

Die Ansicht, die tatsächlich an den Client gesendet wird, wird 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>

Dies ist die Ansicht [/vues/erreurs.jsp]. Der Leser ist aufgefordert, zu überprüfen, was von dieser Ansicht erwartet wird. Diese Informationen werden hier von der Aktion im [request]-Objekt als Attribute bereitgestellt.

  • Dank des Controllers kann die Aktion wiederum das Objekt abrufen, das den Zugriff auf das Anwendungsmodell (Domänenebene) ermöglicht:
     // domain access object
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
  • Sobald dies erledigt ist, können wir die Liste der Artikel anfordern:
    // la liste des erreurs
    ArrayList erreurs = new ArrayList();
    // on demande la liste des articles
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
      // on mémorise l'erreur
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
  • Wenn Fehler auftreten, wird die Ansicht [ERRORS] gesendet:
1
2
3
4
5
6
7
8
     // mistakes?
    if (erreurs.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • ansonsten wird die Ansicht [LIST] gesendet:
1
2
3
4
5
6
     // displays the list of items
    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 Client gesendet wird, wird 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>

Dies ist die Ansicht [/vues/liste.jsp]. Der Leser ist eingeladen, zu überprüfen, was von dieser Ansicht erwartet wird. Diese Informationen werden hier von der Aktion im [request]-Objekt als Attribute bereitgestellt.

3.6.6.3. infos.do

Diese Aktion dient dazu, Informationen zu einem der in der Ansicht [LIST] angezeigten Elemente 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 für die 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 {

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // domain access object
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // list of errors
    ArrayList erreurs = new ArrayList();
     // retrieve the requested id
    String strId = request.getParameter("id");
     // anything?
    if (strId == null) {
       // not normal
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // transform strId into an integer
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // not normal
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // the key item id is requested
    Article article = null;
    try {
      article = articlesDomain.getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // not 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) {
       // not normal
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // put the article in the session
    request.getSession().setAttribute("article", article);
     // the info page is displayed
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionListe() });
    return mapping.findForward("afficherInfosArticle");
  }
}

Kommentare:

  • Der Anfang der [execute]-Methode ist identisch mit dem zuvor besprochenen. Dies gilt auch für die anderen Aktionen.
  • Die Methode ruft den Parameter [id] ab, der normalerweise in der URL enthalten sein sollte. Die URL sollte 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. Wenn ein Problem auftritt, wird die Ansicht [ERRORS] gerendert.
  • Ist [id] gültig, wird das entsprechende Element von der [domain]-Schicht angefordert. Wenn dabei eine Ausnahme ausgelöst wird oder das Element nicht gefunden wird, wird erneut die Ansicht [ERRORS] gesendet.
  • Wenn alles gut läuft, wird das abgerufene Element in der Sitzung gespeichert. Dies ist ein strittiger Punkt. Hier gehen wir davon aus, dass der Client dieses Element kaufen könnte. Wenn dies der Fall ist, rufen wir es aus der Sitzung ab, anstatt es erneut von der [domain]-Schicht anzufordern.
  • Schließlich wird die Ansicht [INFOS] angezeigt. Die Ansicht, die tatsächlich an den Client gesendet wird, wird 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>

Dies ist die Ansicht [/vues/infos.jsp]. Der Leser ist aufgefordert, 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. purchase.do

Diese Aktion dient zum Kauf des Artikels, der in der vorherigen Ansicht [INFOS] angezeigt wurde:

Sobald der Artikel gekauft wurde, wird die Ansicht [LIST] erneut angezeigt (rechte Ansicht). Wenn wir uns den HTML-Code für die obige linke Ansicht ansehen, erkennen wir, dass das Tag <form> wie folgt definiert ist:

1
2
3
4
5
6
7
8
9
        <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>

Wir sehen, dass das Formular mit der Aktion [achat.do] an den Controller übermittelt 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 für die 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 {

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // the quantity purchased is recovered
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // wrong qty
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the article placed in session
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if(article==null){
       // the error page is displayed
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // create the new purchase
    Achat achat = new Achat(article, qté);
     // the purchase is added to the customer's basket
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // we return to the list of items
    return mapping.findForward("afficherListeArticles");
  }
}

Kommentare:

  • Der Anfang der [execute]-Methode ist identisch mit den zuvor behandelten.
  • Erinnern wir uns an das Format des an den Controller gesendeten Formulars:
1
2
3
4
5
6
7
8
9
        <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]: Artikelnummer, [qte]: gekaufte Menge.
  • Das Vorhandensein und die Gültigkeit des Parameters [qte] werden überprüft. Wenn dieser Parameter als fehlerhaft erkannt wird, wird dem Benutzer die Ansicht [INFOS] zusammen mit einer Fehlermeldung zurückgegeben:
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // the quantity purchased is recovered
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // wrong qty
      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. Die Sitzung ist möglicherweise abgelaufen. In diesem Fall wird die Ansicht [ERRORS] angezeigt:
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the article placed in session
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if(article==null){
       // the error page is displayed
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
  • Wenn die Sitzung noch nicht abgelaufen ist, wird der Artikel in den Warenkorb gelegt, der ebenfalls aus der Sitzung abgerufen wird:
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the article placed in session
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if(article==null){
       // the error page is displayed
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // create the new purchase
    Achat achat = new Achat(article, qté);
     // the purchase is added to the customer's basket
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • Schließlich senden wir die [LIST]-Ansicht:
    // on revient à la liste des articles
    return mapping.findForward("afficherListeArticles");
  • Die Ansicht, die tatsächlich an den Client gesendet wird, wird 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>

Dies ist die Ansicht [/main.do]. Diese Ansicht ist keine Ansicht, sondern eine Aktion. Die oben beschriebene Aktion [/main.do] wird daher ausgeführt und zeigt die Liste der Artikel an.

3.6.6.5. cart.do

Diese Aktion dient dazu, alle Einkäufe des Kunden anzuzeigen. Sie ist über die Menüoption [Warenkorb anzeigen] verfügbar:

Der HTML-Code für den Link [Warenkorb anzeigen] 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 für die 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 {

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // the basket is displayed
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // empty basket
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    } else {
       // there's something in the basket
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(), mainServlet.getHActionValidationPanier() });
      return mapping.findForward("afficherPanier");
    }
  }
}

Kommentare:

  • Der Anfang der [execute]-Methode ist identisch mit den zuvor besprochenen.
  • Der Warenkorb wird aus der Sitzung abgerufen, in der er sich normalerweise befindet. Die Sitzung ist möglicherweise abgelaufen; in diesem Fall gibt es keinen Warenkorb. Wir behandeln dies nicht als Fehler, sondern gehen einfach davon aus, dass der Warenkorb leer ist.
  • Ist der Warenkorb leer, wird die Ansicht [EMPTY CART] 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. checkout.do

Diese Aktion entfernt einen Artikel aus dem Warenkorb:

Nach der Aktion [retirerachat.do] wird der Warenkorb erneut angezeigt (Ansicht oben rechts). Wenn wir uns den HTML-Code für den Link [Warenkorb bestätigen] in der Ansicht oben links ansehen, sehen wir Folgendes:

<a href="retirerachat.do?id=3">Retirer</a>

Die Aktion [retirerachat.do] erhält daher 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 für die 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 {

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // we pick up the basket
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    }
     // retrieve the id of the item to be removed
    String strId = request.getParameter("id");
     // anything?
    if (strId == null) {
       // not normal
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // transform strId into an integer
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // not normal
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // we remove the purchase
    panier.enlever(id);
     // the basket is displayed again
    return mapping.findForward("afficherPanier");
  }
}

Kommentare:

  • Der Anfang der [execute]-Methode ist identisch mit den zuvor behandelten.
  • Der folgende Code prüft das Vorhandensein und die Gültigkeit des Parameters [id]. Ist dieser fehlerhaft, wird die Ansicht [ERRORS] gesendet.
  • Andernfalls wird der Artikel aus dem Warenkorb entfernt:
    // on enlève l'achat
    panier.enlever(id);
  • dann wird der Warenkorb erneut angezeigt:
    // on affiche de nouveau le panier
    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>

Wir sehen, dass in Bezug auf die Ansichten die Aktion [/cart.do] ausgelöst wird. Dies wurde bereits beschrieben. Je nach Status des Warenkorbs wird die Ansicht [CART] oder [EMPTY CART] angezeigt.

3.6.6.7. confirmCart.do

Diese Aktion dient zur Bestätigung der Einkäufe des Kunden. In der Praxis umfasst dies einen einzigen Vorgang: Die Lagerbestände der gekauften Artikel werden in der Datenbank um die gekauften Mengen reduziert. Diese Aktion ist im folgenden Menü zu finden:

 

Der HTML-Code für den Link [Warenkorb bestätigen] lautet wie folgt:

<a href="validerpanier.do">Valider le panier</a>

Wenn dieser Link angeklickt wird, werden die Lagerbestände aktualisiert 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 für die 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 {

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // domain access object
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
         // the buyer has confirmed his basket
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
         // validate basket
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // not 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");
        }
         // recover any errors
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
         // everything looks OK - the item list is displayed
    return mapping.findForward("afficherListeArticles");
  }
}

Kommentare:

  • Der Anfang der [execute]-Methode ist identisch mit den zuvor behandelten.
  • Wir rufen den Warenkorb aus der Sitzung ab. Wenn die Sitzung abgelaufen ist, zeigen wir die Ansicht [ERRORS] an:
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
         // the buyer has confirmed his basket
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • Wir bearbeiten die Einkäufe im Warenkorb. Es können Fehler auftreten, wenn die Lagerbestände nicht ausreichen, um die Einkäufe zu erfüllen. In diesem Fall zeigen wir die Ansicht [ERRORS] an:
         // validate basket
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // not 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");
        }
         // recover any errors
         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 geklappt hat, zeigen wir die Liste der Elemente erneut an:
        // tout semble OK - on affiche la liste des articles
    return mapping.findForward("afficherListeArticles");

Die Ansicht, die tatsächlich an den Client gesendet wird, 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>

Wir sehen, dass in Bezug auf die Ansicht die Aktion [/main.do] ausgelöst wird. Dies wurde bereits beschrieben. Sie zeigt die Ansicht [LIST] an.

3.7. MVC-Architektur mit Spring

3.7.1. Allgemeine Anwendungsarchitektur

Werfen wir noch einmal einen Blick auf die MVC-Architektur der Anwendung:

In der ersten Version:

  • wurde der Controller von einem Servlet gesteuert
  • wurden die Ansichten von JSP-Seiten verwaltet
  • wurde das Modell von drei .jar-Dateien verwaltet

In der Struts-Version:

  • wurde der Controller von einem Servlet übernommen, das von Struts' generischem [ActionServlet] abgeleitet war
  • Die Ansichten wurden von denselben JSP-Seiten wie in der [Struts]-Version verwaltet
  • Das Modell wurde von denselben drei Archiven verwaltet

In der Spring-Version:

  • wird der Controller von einem von Spring bereitgestellten Servlet [DispatcherServlet] verwaltet
  • Die Ansichten werden von denselben JSP-Seiten wie zuvor verwaltet, mit einigen geringfügigen Unterschieden
  • Das Modell wird von denselben drei Dateien verwaltet

Wir werden feststellen, dass die Migration unserer Anwendung von Struts zu Spring unkompliziert ist, wenn wir bereit sind, auf die Verwendung aller für eine Standard-Spring-MVC-Architektur empfohlenen Elemente zu verzichten. Die wichtigsten Änderungen sind wie folgt:

  • Aktionen, die zuvor in bestimmten Methoden des Servlets/Controllers oder durch Instanzen von Klassen, die von der [Action]-Klasse in Struts abgeleitet waren, verarbeitet wurden, werden nun von Klasseninstanzen verarbeitet, die die Spring [Controller]-Schnittstelle implementieren
  • Die erforderlichen Konfigurationsdateien sind 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] verwendet, um die von der Anwendung benötigten Beans zu erstellen, insbesondere die Model-Access-Service-Bean
  • Die JSP-Ansichten sind identisch mit denen in Struts. Wir müssen eine neue erstellen.

Erinnern wir uns an die STRUTS-MVC-Architektur, die in der vorherigen Version verwendet wurde:

M = Modell
Geschäftsklassen, Datenzugriffsklassen und die Datenbank
V = Ansichten
die JSP-Seiten
C = Controller
das Servlet zur Verarbeitung von Client-Anfragen, [Action]-Objekte

Mit Spring verwenden wir dieselbe Architektur:

M = Modell
Geschäftsklassen, Datenzugriffsklassen und die Datenbank
V = Ansichten
die JSP-Seiten
C = Controller
das Servlet, das Client-Anfragen verarbeitet, Objekte, die die [Controller]-Schnittstelle implementieren
  • 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]. Im Folgenden bezeichnen wir diesen Controller als [Spring]-Controller.
  • Der [Spring]-Controller leitet die Anfrage des Clients an eine der [Controller]-Instanzen weiter. Pro zu verarbeitender Aktion gibt es eine solche Instanz. Dies wird in der angeforderten URL definiert, genau wie bei Struts. So wissen wir, dass es sich bei der angeforderten Aktion um die [list]-Aktion handelt, da die angeforderte URL [list.do] lautet
  • Wenn C der Anwendungskontext ist, verwendet der [Spring]-Controller eine [C-servlet.xml]-Datei, die dieselbe Rolle spielt wie die Konfigurationsdatei struts-config.xml in der Struts-Version. Für jede von der Anwendung zu verarbeitende Aktion ordnen wir den Namen der Klasse vom Typ Controller zu, die für die Bearbeitung der Anfrage zuständig ist.
  • Der Controller übergibt die Kontrolle an das mit der Aktion verknüpfte Objekt vom Typ Controller. Dies geschieht durch Aufruf der Methode handleRequest dieses Objekts und Übergabe der Client-Anfrage an dieses. Hier führt der Entwickler die erforderlichen Aufgaben aus: Möglicherweise muss er Business-Logik-Klassen oder Datenzugriffsklassen aufrufen. Am Ende der Verarbeitung gibt das Controller-Objekt den Namen der Ansicht an den Controller zurück, die dieser als Antwort an den Client senden muss.
  • In seiner Konfigurationsdatei findet der Controller die URL, die dem Namen der Ansicht zugeordnet ist, die er anzeigen soll. Anschließend sendet er die Ansicht. Die Interaktion mit dem Client ist 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. Anwendungskonfiguration

3.7.3.1. Allgemeine Architektur

Die allgemeine Architektur des Eclipse-Projekts sieht wie folgt aus:

Image

3.7.3.2. Konfiguration des Datenzugriffs

Da die Datenzugriffsschnittstelle unverändert bleibt, sind die zugehörigen Konfigurationsdateien dieselben wie in der vorherigen Version. Sie sind in [WEB-INF/src] definiert:

Image

Im 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 Verzeichnis „archives“

In [WEB-INF/lib] finden Sie dieselben Bibliotheken wie in der vorherigen Version, mit Ausnahme derjenigen von Struts, die nicht mehr benötigt werden:

Image

3.7.3.4. Anwendungskonfiguration

Die Anwendung wird über drei Dateien konfiguriert: [web.xml, applicationContext.xml, springwebarticles-servlet.xml] im Ordner [WEB-INF]:

Image

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>
     <!-- application spring context loader -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- the servlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- url mapping -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- entry document -->
    <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 [springwebarticles]-Servlet weitergeleitet (servlet-mapping)
  • Das [springwebarticles]-Servlet ist eine Instanz der von Spring bereitgestellten Klasse [org.springframework.web.servlet.DispatcherServlet] (servlet-name, servlet-class).
  • 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] sieht wie folgt aus:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- web application configuration-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

Einige Elemente sind bekannt, andere weniger. Bei der Initialisierung der Anwendung werden drei Beans instanziiert:

  • articlesDao: Dienst, der Zugriff auf die [dao]-Schicht bietet
  • articlesDomain: Dienst, der Zugriff auf das Modell bietet
  • config: ein Bean, in dem wir die Informationen sammeln, die alle Clients gemeinsam nutzen müssen. Dieser Bean übernimmt die Rolle, die traditionell vom Anwendungskontext gespielt wird, jedoch mit typisierten statt untypisierten Informationen.

Die letzte Datei [springwebarticles.xml] definiert die von der Anwendung akzeptierten Aktionen auf eine Weise, die der im Struts-Konfigurationsdatei [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>
     <!-- stock managers = controllers -->
    <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>

     <!-- application mapping-->
    <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 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>

     <!-- message file -->
    <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:
main.do
um die Liste der Artikel anzuzeigen
liste.do
um die Liste der Artikel anzuzeigen
info.do
um Informationen zu einem bestimmten Artikel anzuzeigen
purchase.do
um einen bestimmten Artikel zu kaufen
Warenkorb.do
um den Warenkorb anzuzeigen
remove-purchase.do
um einen Artikel aus dem Warenkorb zu entfernen
confirmcart.do
um den Warenkorb zu bestätigen
  • Die oben aufgeführten Aktionen entsprechen eins zu eins den Aktionen, die in früheren Versionen vom Controller 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 beliebig. Er dient lediglich als 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>
  • Der [VoirPanierController]-Bean definiert:
  • die zu instanziierende Klasse [istia.st.articles.web.spring.VoirPanierController] zur Verarbeitung der Aktion
  • wie sie instanziiert werden soll. Hier wird die [config]-Bean, die in [applicationContext.xml] definiert und beim Start der Anwendung instanziiert wurde, als Parameter bereitgestellt. Dies geschieht für alle [Controller]-Aktionen. Somit verfügt jede von ihnen in einem privaten Feld über das [config]-Objekt, in dem sie alle Informationen findet, die von allen Clients gemeinsam genutzt werden.
  • Wie View-Namen aufgelöst werden sollten:
    <!-- gestionnaire de vues -->
    <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 [Controller]-Instanz, die eine Aktion verarbeitet, nach der Verarbeitung einen Schlüssel an den Spring-Controller zurück, um anzugeben, welche Ansicht angezeigt werden soll. Basierend auf diesem Schlüssel gibt es verschiedene Strategien zur Generierung der mit dem Schlüssel verbundenen Ansicht. Die verwendete Strategie ist diejenige, die durch den [viewResolver]-Bean definiert wird. Hier ist diese Bean mit der Klasse [org.springframework.web.servlet.view.InternalResourceViewResolver] und verschiedenen Initialisierungsparametern verknüpft. Ohne ins Detail zu gehen, legt die [viewResolver]-Bean hier fest, dass, wenn der View-Schlüssel „XX“ lautet, die generierte Ansicht [/views/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 [viewResolver]-Bean
  • durch Ändern der Initialisierungsparameter der Implementierungsklasse

So können Sie einfach durch Ändern des Werts der [viewResolver]-Bean von einer HTML-Ansicht zu einer XML-Ansicht wechseln

  • den Namen einer Nachrichtendatei für die Anwendung (messageSource). Hier existiert die Datei zwar, ist aber leer. Sie wird nicht verwendet. Sie muss im [ClassPath] der Anwendung abgelegt werden. Hier wird sie in [WEB-INF/classes] abgelegt. In Eclipse geschieht dies durch Ablage in [WEB-INF/src]:

Image

3.7.4. Die JSP-Ansichten

Es werden die von Struts verwendeten JSP-Ansichten verwendet. Keine davon wird geändert:

Image

Es wird eine einzige neue Ansicht erstellt: redirpanier.jsp. Sie dient ausschließlich dazu, den Client zur Aktion [/panier.do] umzuleiten. Ihr Code lautet wie folgt:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/panier.do"/>

Lesern wird empfohlen, sich die Definitionen der verschiedenen Ansichten in der Struts-Version anzusehen.

3.7.5. Aktionsverarbeitung

Die für die Verarbeitung der verschiedenen Aktionen erforderlichen Klassen wurden im Paket [istia.st.articles.web.spring] zusammengefasst:

Image

Sehen wir uns anhand eines Beispiels an, wie die Spring-Anwendung funktioniert:

  • Der Benutzer ruft die URL [http://localhost:8080/springwebarticles/main.do] auf

Image

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>
     <!-- application spring context loader -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- the servlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- url mapping -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- entry document -->
    <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 mehrere Vorgänge ausgelöst:
    • Der Listener [org.springframework.web.context.ContextLoaderListener] wurde geladen
    • er hat die Konfigurationsdatei [applicationContext.xml] geparst:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- web application configuration-->
    <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] verwendet:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- stock managers = controllers -->
    <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>

     <!-- application mapping-->
    <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 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>

     <!-- message file -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>
  • Die in dieser Datei definierten [Controller]-Beans wurden ebenfalls erstellt
  • Nun ist alles bereit, um die Anfrage des Clients zu bearbeiten. Die Anfrage lautete: [http://localhost:8080/springwebarticles]. Hier fordern wir keine URL aus dem Kontext an, sondern den 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 daraufhin eine neue Anfrage. Er nutzt seine 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>
     <!-- stock managers = controllers -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
....
    </bean>

     <!-- application mapping-->
    <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 gibt an, dass die Aktion [/main.do] vom [ListController]-Bean verarbeitet werden muss.
  • Die Anfrage des Clients wird an die Methode [handleRequest] der [ListController]-Bean weitergeleitet. Diese Methode führt ihre Aufgabe aus und gibt den Schlüssel der anzuzeigenden Ansicht an den Controller zurück. Wenn alles gut läuft, lautet dieser Schlüssel hier [list].
  • Der Spring-Controller verwendet die [viewResolver]-Bean aus der Konfigurationsdatei [springwebarticles-servlet.xml], um die mit diesem Schlüssel verknüpfte 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 erwähnt, dass beim Start der Anwendung die Beans in 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>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- web application configuration-->
    <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 Bean 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 fields
    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);

     // getters-setters
    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;
    }

     // init web application
    public void init() {
         // memorize certain application urls
        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);

         // it's over
        return;
    }
}

Diese Klasse erfüllt dieselbe Funktion wie die [init]-Methode eines Webanwendungs-Servlets. Sie initialisiert die Anwendung. Dies geschieht hier wie folgt:

  • da die [config]-Bean in [applicationContext.xml] wie folgt definiert ist:
    <!-- la configuration de l'application web-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>

Bei der Erstellung wird das private Feld [articlesDomain] initialisiert

  • Anschließend wird aufgrund des Attributs [init-method="init"] der oben genannten Bean die Methode [init] der mit der Bean verknüpften Klasse ausgeführt. Hier werden die drei Wörterbücher [hActionListe, hActionPanier, hActionValidationPanier] initialisiert, die zur Generierung der drei möglichen Menü-Links verwendet werden, die dem Benutzer angeboten werden.
  • Es werden öffentliche Zugriffsmethoden erstellt, um diese privaten Felder für Instanzen vom Typ [Controller] zugänglich zu machen, die die Aktionen verarbeiten.

3.7.7. Die [Controller]-Aktionen der Spring-Anwendung

3.7.7.1. Einführung

Jede Spring-Aktion ist Gegenstand einer Klasse vom Typ [Controller]. In der Struts-Version war jede Aktion Gegenstand einer Klasse vom Typ [Action]. Das Schreiben der [Controller]-Klasse umfasst meist:

  • Kopieren und Einfügen der [Action]-Klasse, die in der Struts-Version verwendet wurde
  • Anpassung des Codes an die Spring-Konventionen

3.7.7.2. main.do, liste.do

Diese beiden Aktionen sind identisch und in [springwebarticles-servlet.xml] wie folgt definiert:

    <!-- le mapping de l'application-->
    <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 gleich noch näher eingehen werden. Wenn eine dieser Aktionen in einem Browser ausgeführt wird, ergibt sich folgendes Ergebnis:

Image

Der Code für die 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 {

     // web app configuration
    Config config;

    public void setConfig(Config config) {
        this.config = config;
    }

     // query processing
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

         // the list of items is requested
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // we memorize the error
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
             // send error view
            return new ModelAndView("erreurs");
        }
         // displays the list of items
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
         // send view
        return new ModelAndView("liste");
    }

}

Kommentare:

  • Die Klasse verfügt über ein privates Feld [config]. Dieses Feld wurde von Spring initialisiert, als die [ListController]-Bean instanziiert wurde:
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

Wie oben gezeigt, wird das Feld [config] von [ListController] mit dem Bean [config] initialisiert. Was ist das? Es handelt sich um den in [applicationContext.xml] definierten Bean [config], d. h. eine Instanz des oben beschriebenen [istia.st.articles.web.spring.Config].

  • Das Schreiben des Codes für eine [Controller]-Klasse umfasst im Wesentlichen das Schreiben des Codes für ihre [handleRequest]-Methode
  • Wir fordern die Liste der Artikel vom Modell an. Diese ist über [config.getArticlesDomain()] zugänglich. Tritt eine Ausnahme auf, wird die Ansicht [ERRORS] gerendert. 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 – erstellen wir eine Instanz von [ModelAndView], indem wir ihr den Schlüssel der anzuzeigenden Ansicht übergeben. Erinnern Sie sich daran, dass basierend auf der Konfiguration des [viewResolver]-Beans die Anforderung der Ansicht mit dem Schlüssel XX dazu führt, dass die Ansicht [/vues/XX.jsp] gesendet wird.
        // on demande la liste des articles
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
            // on mémorise l'erreur
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
            // on affiche la page des erreurs
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
            // envoyer la vue erreurs
            return new ModelAndView("erreurs");
        }
  • Wenn keine Fehler vorliegen, wird die Ansicht [LIST] gesendet:
1
2
3
4
5
6
7
        // on affiche la liste des articles
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
        // envoyer la vue
        return new ModelAndView("liste");

3.7.7.3. infos.do

Diese Aktion dient dazu, Informationen zu einem der in der Ansicht [LIST] angezeigten Elemente bereitzustellen:

Diese Aktion ist in [springwebarticles-servlet.xml] wie folgt definiert:

    <!-- le mapping de l'application-->
    <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 für die 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 {

     // web app configuration
    Config config;

    public void setConfig(Config config) {
        this.config = config;
    }

     // query processing
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

     // error list
    ArrayList erreurs = new ArrayList();
     // retrieve the requested id
    String strId = request.getParameter("id");
     // anything?
    if (strId == null) {
       // not normal
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // transform strId into an integer
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // not normal
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // the id key item is requested
    Article article = null;
    try {
      article = config.getArticlesDomain().getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // not 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) {
       // not normal
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // put the article in the session
    request.getSession().setAttribute("article", article);
     // the info page is displayed
    request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
        return new ModelAndView("infos");
    }
}

Kommentare:

  • Die Methode [handleRequest] ruft den Parameter [id] ab, der normalerweise in der URL enthalten sein sollte. Die URL sollte 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. Wenn ein Problem auftritt, wird die Ansicht [ERRORS] angezeigt.
  • Ist [id] gültig, wird das entsprechende Element von der [domain]-Schicht angefordert. Wenn dabei eine Ausnahme ausgelöst wird oder das Element nicht gefunden wird, wird erneut die Ansicht [ERROR] gesendet.
  • Wenn alles gut läuft, wird das abgerufene Element in der Sitzung gespeichert. Dies ist ein strittiger Punkt. Hier gehen wir davon aus, dass der Kunde dieses Element möglicherweise kaufen wird. Wenn dies der Fall ist, rufen wir es aus der Sitzung ab, anstatt es erneut von der [domain]-Schicht anzufordern.
  • Schließlich wird die [INFO]-Ansicht angezeigt.

3.7.7.4. purchase.do

Diese Aktion dient zum Kauf des Artikels, der in der vorherigen [INFOS]-Ansicht angezeigt wurde:

Image

Wenn wir uns den HTML-Code dieser Ansicht ansehen, stellen wir fest, dass das <form>-Tag 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>

Wir sehen, dass das Formular mit der Aktion [achat.do] an den Controller übermittelt wird.

Diese Aktion ist in [springwebarticles-servlet.xml] wie folgt konfiguriert:

    <!-- le mapping de l'application-->
    <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 für die 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 {

   // web app configuration
  Config config;

  public void setConfig(Config config) {
    this.config = config;
  }

   // query processing
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // the quantity purchased is recovered
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // wrong qty
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the session item
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if (article == null) {
       // the error page is displayed
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // create the new purchase
    Achat achat = new Achat(article, qté);
     // the purchase is added to the customer's basket
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // we return to the list of items
    return new ModelAndView("index");
  }
}

Kommentare:

  • Sehen wir uns das Format des an den Controller gesendeten Formulars an:
        <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]: Artikelnummer, [qte]: gekaufte Menge.
  • Das Vorhandensein und die Gültigkeit des Parameters [qte] werden überprüft. Wenn dieser Parameter als fehlerhaft erkannt wird, wird dem Benutzer die Ansicht [INFOS] zusammen mit einer Fehlermeldung zurückgegeben:
    // la liste des erreurs sur cette action
    ArrayList erreurs = new ArrayList();
    // on récupère la quantité achetée
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
      // qté erronée
      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. Die Sitzung ist möglicherweise abgelaufen. In diesem Fall wird die Ansicht [ERRORS] gesendet:
    // on récupère la session du client
    HttpSession session = request.getSession();
    // on récupère l'article mis en session
    Article article = (Article) session.getAttribute("article");
    // session expirée ?
    if (article == null) {
      // on affiche la page des erreurs
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • Wenn die Sitzung noch nicht abgelaufen ist, wird der Artikel in den Warenkorb gelegt, der ebenfalls aus der Sitzung abgerufen wird:
1
2
3
4
5
6
7
8
9
    // on crée le nouvel achat
    Achat achat = new Achat(article, qté);
    // on ajoute l'achat au panier du client
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • Schließlich senden wir die Ansicht [LIST]:
    // on revient à la liste des articles
    return new ModelAndView("index");
  • Oben senden wir die Ansicht [/views/index.jsp]. Wir wissen, dass diese Ansicht den Browser des Clients anweist, zur URL [/main.do] weiterzuleiten. Durch diese Weiterleitung wird die Liste der Artikel angezeigt.

3.7.7.5. cart.do

Diese Aktion dient dazu, alle Einkäufe des Kunden anzuzeigen. Sie ist über das Menü verfügbar:

Image

Der mit dem obigen Link verbundene HTML-Code lautet wie folgt:

<a href="panier.do">Voir le panier</a>

Die Seite, die über diesen Link aufgerufen wird, sieht wie folgt aus:

Image

Diese Aktion ist in [springwebarticles-servlet.xml] wie folgt konfiguriert:

    <!-- le mapping de l'application-->
    <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 für die 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 {

   // web app configuration
  Config config;

  public void setConfig(Config config) {
    this.config = config;
  }

   // query processing
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // the basket is displayed
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // empty basket
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("paniervide");
    } else {
       // there's something in the basket
      request.setAttribute("actions", new Hashtable[] {
          config.getHActionListe(), config.getHActionValidationPanier() });
      return new ModelAndView("panier");
    }
  }
}

Kommentare:

  • Der Warenkorb wird aus der Sitzung abgerufen, in der er normalerweise gespeichert ist. Die Sitzung ist möglicherweise abgelaufen; in diesem Fall gibt es keinen Warenkorb. Wir behandeln dies nicht als Fehler, sondern gehen einfach davon aus, dass der Warenkorb leer ist.
  • Ist der Warenkorb leer, wird die Ansicht [LEERER WARENKORB] angezeigt
  • , andernfalls wird die Ansicht [WARENKORB] angezeigt

3.7.7.6. removePurchase.do

Diese Aktion dient dazu, einen Artikel aus dem Warenkorb zu entfernen:

Image

Wenn wir uns den HTML-Code für den obigen Link ansehen, sehen wir Folgendes:

<a href="retirerachat.do?id=3">Retirer</a>

Die Aktion [retirerachat.do] erhält daher als Parameter die ID des Artikels, der aus dem Warenkorb entfernt werden soll. Diese Aktion ist in [springwebarticles-servlet.xml] wie folgt konfiguriert:

    <!-- le mapping de l'application-->
    <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 für die 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 {

   // web app configuration
  Config config;

  public void setConfig(Config config) {
    this.config = config;
  }

   // query processing
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // we pick up the basket
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // retrieve the id of the item to be removed
    String strId = request.getParameter("id");
     // anything?
    if (strId == null) {
       // not normal
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // transform strId into an integer
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // not normal
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // we remove the purchase
    panier.enlever(id);
     // the basket is displayed again
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");
  }
}

Kommentare:

  • Der Code prüft das Vorhandensein und die Gültigkeit des Parameters [id]. Ist dieser fehlerhaft, wird die Ansicht [ERRORS] gesendet.
  • Andernfalls wird der Artikel aus dem Warenkorb entfernt:
    // on enlève l'achat
    panier.enlever(id);
  • dann wird der Warenkorb neu geladen:
    // on affiche de nouveau le panier
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");

Sehen wir uns den View-Code [/vues/redirpanier.jsp] an:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/panier.do"/>

Wir sehen, dass der Client zur Aktion [/panier.do] weitergeleitet wird. Dies wurde bereits beschrieben. Je nach Status des Warenkorbs wird die Ansicht [PANIER] oder [PANIERVIDE] angezeigt.

3.7.7.7. confirmcart.do

Diese Aktion dient zur Bestätigung der Einkäufe des Kunden. In der Praxis umfasst dies einen einzigen Vorgang: Die Lagerbestände der gekauften Artikel werden in der Datenbank um die gekauften Mengen verringert. Diese Aktion ist über das folgende Menü erreichbar:

Image

Der HTML-Code für den Link [Warenkorb bestätigen] lautet wie folgt:

<a href="validerpanier.do">Valider le panier</a>

Wenn dieser Link angeklickt wird, werden die Lagerbestände aktualisiert und die Artikelliste erneut angezeigt.

Diese Aktion ist in [springwebarticles-servlet.xml] wie folgt konfiguriert:

    <!-- le mapping de l'application-->
    <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 für die 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 {

   // web app configuration
  Config config;

  public void setConfig(Config config) {
    this.config = config;
  }

   // query processing
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // the buyer has confirmed his basket
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // validate basket
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
       // not 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");
    }
     // recover any errors
    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");
    }
     // everything looks OK - the item list is displayed
    return new ModelAndView("index");
  }
}

Kommentare:

  • Wir rufen den Warenkorb aus der Sitzung ab. Wenn die Sitzung abgelaufen ist, zeigen wir die Ansicht [ERRORS] an:
    // la liste des erreurs sur cette action
    ArrayList erreurs = new ArrayList();
    // l'acheteur a confirmé son panier
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
      // session expirée
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • Wir bearbeiten die Einkäufe im Warenkorb. Es können Fehler auftreten, wenn die Lagerbestände nicht ausreichen, um die Einkäufe zu erfüllen. In diesem Fall zeigen wir die Ansicht [ERRORS] an:
    // on valide le panier
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
      // pas 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");
    }
    // on récupère les éventuelles erreurs
    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 geklappt hat, zeigen wir die Liste der Artikel erneut an:
    // tout semble OK - on affiche la liste des articles
    return new ModelAndView("index");

Wir wissen, dass die Ansicht [/vues/index.jsp] den Client zur Aktion [/main.do] weiterleitet. Diese Aktion zeigt die Ansicht [LISTE] an.