Application WEB : les bonnes pratiques

Structure des applications WEB

Une application de modification d'un annuaire :

Description fonctionnelle : Un utilisateur se connecte au système (page login.jsp) en donnant un nom et un mot de passe. La soumission se connecte sur la page auth.jsp qui vérifie l'identité. En cas d'erreur, un forward est effectué sur la page login.jsp. En cas de réussite, le Bean est chargé depuis la base et un forward est effectué vers la page form.jsp. Cette dernière se charge d'afficher un formulaire de modification. La validation se connecte sur la page save.jsp. cette dernière enregistre les données du formulaire dans le bean est valide ce dernier en vérifiant la conformité des données entrées par l'utilisateur. Si les données sont valides, la page save.jsp sauvegarde le bean et affiche un message indiquant la réussite. Si les données ne sont pas valides, la page save.jsp effectue un forward vers la page form.jsp afin que l'utilisateur corrige les erreurs.

img/img-ex-annuaire.png

Dans cet exemple, les symboles « A » indiquent des actions (du code Java qui agit sur les données) et « SQL » des accès à la couche de stockage. Les flèches qui arrivent sur le JabaBean indiquent une modification et celles qui partent une utilisation. A ce stade nous pouvons faire les remarques suivantes :

Il est très difficile de développer des applications complexes et/ou importantes en adoptant cette structure éclatée. Nous allons donc proposer une organisation plus rationnelle pour les applications WEB :

Réorganisation des pages JSP

Il faut distinguer les pages qui agissent et celles qui produisent les réponses :

img/reorg-jsp.png

Pour simplifier et améliorer la qualité de l'application, il est nécessaire d'effectuer cette séparation. Ce ne sont pas les mêmes personnes qui s'occupent de ces pages (par exemple un graphiste pour les vues).

Il faut une entrée unique :

img/reorg-jsp2.png

Pour améliorer la qualité et la sécurité, il est nécessaire de mettre en place une porte d'entrée unique qui traite toutes les demandes.

Architecture MVC (Modèle, Vue, Contrôleur)

Trois parties :

img/img-mvc.png

C'est une architecture concue pour les I.H.M. (Interfaces Homme/Machines).

MVC et couche métier indépendante

Plusieurs applications peuvent interagir avec la même couche métier :

img/img-mvc2.png

Les vues

♦  Les vues sont basées sur des technologies de production de contenu (JSP, XML/XSLT, XSL-FO, PDF, XQuery).

♦  Les vues ne modifient pas les données (problème avec JSP).

♦  Une vue peut être interprétée comme un processus de fabrication à partir de données fournies en entrée (pas de logique applicative).

Le modèle

♦  Le modèle à la charge de représenter les données du domaine et de fournir les méthodes permettants l'accès et la modification.

♦  Le modèle est indépendant de la logique applicative.

♦  Si une couche métier existe, elle représente le modèle.

♦  Dans une architecture orientée service, le modèle est l'ensemble des services du système d'information.

Le contrôleur

♦  Le contrôleur est basé sur des technologies de traitement de requêtes (Servlet, C#, C++, Ruby, Python).

♦  Le contrôleur peut être unique ou multiple.

♦  Le contrôleur est responsable de la logique applicative.

♦  Il transforme les requêtes utilisateur en requêtes métier (vérification des données entrantes).

♦  Il choisit la vue et lui fournit les données.

img/img-cont.png

Enchaînement

img/chain.png

♦  L'étapebeforeAll traite toutes les requêtes (contrôle de la machine cliente, de l'authentification, ouverture de session JPA, etc...)

♦  L'action traite une demande spécifique et choisie la vue.

♦  L'étape beforeView traite la demande de vue (vérification, aiguillage en fonction du contexte).

♦  Les Vues produisent les résultats.

♦  L'étape afterAll assure la fermeture des ressources allouées (session, temps d'exécution).

Démarche M.V.C.

Quatre étapes :

  1. Dresser la liste des vues (maquette HTML),
    • nom de la vue,
    • type de données généré (XHTML/XML/PDF),
    • paramètres (nom, type, obligatoire/facultatif),
    • requêtes engendrées par la vue,
  2. Implantation et test des vues.
  3. Dresser la liste des requêtes
    • nom de la requête (URL),
    • paramètres (nom, type, obligatoire/facultatif),
    • contexte d'utilisation,
    • vues utlisées.
  4. Implantation et test des requêtes.

La démarche M.V.C. pour l'annuaire

Architecture :

img/img-mvc-annuaire.png

Représentation d'un utilisateur

Chaque utilisateur de l'application va être représenté par une instance d'un javaBean de porté session :

package fr.exemple.values;

public class User {

    // properties
    private String id = "";
    private String name = "";
    private String password = "";
    private Boolean auth = false; // important !

    public User() {
    }

    // getters and setters
    ...

}

Configurer les pages JSP

Nous ajoutons dans le fichier web.xml :

<jsp-config>
    <jsp-property-group>
        <description>Toutes les pages</description>
        <url-pattern>*.jsp</url-pattern>
        <page-encoding>UTF-8</page-encoding>
        <include-prelude>/prelude.jsp</include-prelude>
        <include-prelude>/copyright.jsp</include-prelude>
        <include-coda>/coda.jsp</include-coda>
    </jsp-property-group>
</jsp-config>

Cette configuration va inclure les pages /prelude.jsp et /copyright.jsp au début et /coda.jsp à la fin de chaque page JSP.

Le code de la page /prelude.jsp :

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:url var="charte" value="/charte.css" />

<head>
    <title>Annuaire en ligne</title>
    <link rel="stylesheet" type="text/css" href="${charte}" />
</head>
<body>

Le code de la page /copyright.jsp :

<!-- (c) JL Massat 2007 -->

Le code de la page /coda.jsp :

</body>
</html>

La page « /index.jsp »

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:redirect url="/login.do" />

Cette page JSP donne la réponse HTTP suivant :

HTTP/1.1 302 Déplacé Temporairement
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=D96D1EA6FA9E99A50E383C7E9E0B0995; Path=/cours-JSP
Location: http://monserveur.fr:8080/cours-JSP/login.do;jsessionid=D96D1EA6FA9E..
Content-Type: text/html;charset=UTF-8
Content-Length: 0
Date: Thu, 17 Dec 2009 11:18:59 GMT
Connection: close

Dans cet exemple, le contexte de l'application WEB est /cours-JSP et la balise <c:redirect.../> se charge de transformer l'URL locale /login.do en l'URL globale équivalente.

La vue « /loginForm.jsp »

Paramètres :

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:url var="login" value="/login.do" />

<h1>Authentification</h1>
<p><c:out value="${message}" default="Identifiez-vous :"/></p>

<form method="POST" action="${login}">
    <label>Login :</label>
    <input name="id" type="text"
        value="<c:out value="${user.id}"/>"/>
    <label>Password : </label>
    <input name="password" type="password"
        value="<c:out value="${user.password}"/>"/>
    <br/>
    <label>Validation :</label>
    <input name="ok" type="submit" value="Ok"/>
</form>

La vue « /home.jsp »

Paramètres :

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:url var="logout" value="/logout.do" />
<c:url var="editForm" value="/editForm.do" />

<h1>Bienvenue <c:out value="${user.name}"/></h1>

<p>Vous pouvez <a href="${logout}">nous quitter</a> ou
<a href="${editForm}">modifier vos informations</a>.</p>

<p>Utilisateurs :</p>
<ul><c:forEach var="u" items="${users}">
    <li>${u.name}</li>
</c:forEach></ul>

Requêtes engendrées :

La vue « /message.jsp »

Paramètre :

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:url var="index" value="/index.jsp" />

<h1><c:out value="${message}"/></h1>
<p><a href="${index}">Retour</a>.</p>

Requête engendrée :

Interface vers les vues

package fr.exemple.webapp;

import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import fr.exemple.values.User;

public class Views {

    String loginForm(HttpServletRequest request, User user, String message) {
        request.setAttribute("user", user);
        request.setAttribute("message", message);
        return ("/loginForm.jsp");
    }

    String home(HttpServletRequest request, User user, Set<User> users) {
        request.setAttribute("user", user);
        request.setAttribute("users", users);
        return ("/home.jsp");
    }

    String message(HttpServletRequest request, String message) {
        request.setAttribute("message", message);
        return ("/message.jsp");
    }
}

La couche métier

Un exemple de couche métier :

package fr.exemple.business;
import fr.exemple.values.User;

public class Business {

    void authenticate(User p) throws BusinessException;

    void updatePerson(User p) throws BusinessException;

}

Déclarer la servlet contrôleur

Avant de décrire le contrôleur, nous devons le déclarer dans le fichier web.xml :

<!-- Déclaration de la servlet contrôleur -->
<servlet>
   <servlet-name>Cont</servlet-name>
   <servlet-class>fr.exemple.webapp.MyControler</servlet-class>
   <init-param>
      <description>Répertoire de stockage</description>
      <param-name>dataDir</param-name>
      <param-value>/tmp/donnees</param-value>
   </init-param>
   <load-on-startup>2</load-on-startup>
</servlet>

<servlet-mapping>
   <servlet-name>Cont</servlet-name>
   <url-pattern>*.do</url-pattern>
</servlet-mapping>

Ce contrôleur gère les URL qui se terminent par .do et possède un paramètre d'initialisation (dataDir).

Le contrôleur de notre application

package fr.exemple.webapp;

import ...

public class MyControler extends HttpServlet {

    private Business business = null;
    final private Set<User> users = new HashSet<User>();
    final private Views views = new Views();

    final private String loginUrl = "/login.do";
    final private String logoutUrl = "/logout.do";

    // démarrage et arret du contrôleur
    ...
    
    // traitement des sessions
    ...
    
    // traitement des erreurs et des actions
    ...

    }

Démarrage et arret du contrôleur :

// Démarrage du contrôleur : création de la couche métier
@Override
final public void init(ServletConfig c) throws ServletException {
    Business b = new Business();
    b.setDataDir(c.getInitParameter("dataDir"));
    b.init();
    business = b;
}

// Arret du contrôleur
@Override
final public void destroy() {
    business.close();
}

Obtenir l'utilisateur associé à la session :

public User getUser(HttpServletRequest request) {
    HttpSession session = request.getSession();
    synchronized (session) {
        Object o = session.getAttribute("user");
        if (o instanceof User) {
            return (User) o;
        }
        User newUser = new User();
        session.setAttribute("user", newUser);
        // espionner la session
        session.setAttribute("spy", new Spy(newUser));
        return (newUser);
    }
}

Surveiller la session :

private class Spy implements HttpSessionBindingListener {
    final User user;

    public Spy(User user) {
        this.user = user;
    }

    // traitement d'une nouvelle session
    public void valueBound(HttpSessionBindingEvent ev) {
        users.add(user);
    }

    // traitement d'une fin de session
    public void valueUnbound(HttpSessionBindingEvent ev) {
        users.remove(user);
    }

}

Intercepteur :

// Interception d'une requête
public String beforeAll(HttpServletRequest request) {
    // pour obtenir le temps de calcul
    request.setAttribute("timer", System.currentTimeMillis());
    // vérifier l'authentification
    if (!request.getServletPath().equals(loginUrl)) {
        User user = getUser(request);
        if (user.getAuth() == false) {
            String msg = "Vous devez vous authentifier";
            return views.loginForm(request, user, msg);
        }
    }
    return null;
}

// Interception après vue
public void afterAll(HttpServletRequest request) {
    Long start = (Long) request.getAttribute("timer");
    Long duration = System.currentTimeMillis() - start;
    debug("time : ", duration.toString(), "ms");
}

Après l'action et avant la vue :

// Interception avant vue
public String beforeView(String view, HttpServletRequest request) {
    return (view);
}

Traitement des erreurs :

// Interception d'une erreur
public String error(HttpServletRequest request,
        HttpServletResponse response, Throwable e) {
    String msg = "erreur interne : " + e.getMessage();
    return views.message(request, msg);
}

Action spécifique de login :

public String doLogin(HttpServletRequest request) {
    User user = getUser(request);
    // si l'utilisateur est connu
    if (user.getAuth())
        return views.home(request, user, users);
        
    // récupérer les données du formulaire
    user.setId(request.getParameter("id"));
    user.setPassword(request.getParameter("password"));
    // si aucune données retour au formulaire
    if (user.getId() == null || user.getPassword() == null)
        return views.loginForm(request, user, null);

    // traiter les données
    if (user.getPassword().length() == 0)
        return views.loginForm(request, user, "Mot de passe vide");
    try {
        business.authenticate(user);
        if (user.getAuth())
            return views.home(request, user, users);
        return views.loginForm(request, user, "mot de passe incorrecte");
    } catch (BusinessException e) {}
    return views.loginForm(request, user, "Utilisateur inconnu");
}

Action spécifique de logout :

public String doLogout(HttpServletRequest request) {
    User user = getUser(request);
    user.setAuth(false);
    user.setId("");
    user.setName("");
    user.setPassword("");
    return views.message(request, "A bientôt");
}

Le traitement des requêtes HTTP :

@Override
final protected void service(HttpServletRequest request,
        HttpServletResponse response) throws ServletException, IOException {
    try {
        String view = beforeAll(request);
        if (view == null)
            view = processAction(request, response);
        if (view != null)
            view = beforeView(view, request);
        if (view != null)
            processView(view, request, response);
    } catch (Throwable t) {
        String view = error(request, response, t);
        if (view != null)
            try {
                processView(view, request, response);
            } catch (Throwable e) {
                throw new ServletException(e);
            }
    } finally {
        afterAll(request);
    }
}

Le mécanisme d'aiguillage :

private String processAction(HttpServletRequest request,
        HttpServletResponse response) throws Throwable {
    request.setCharacterEncoding("UTF-8");
    String action = request.getServletPath();
    if (action.equals(loginUrl))
        return doLogin(request);
    if (action.equals(logoutUrl))
        return doLogout(request);
    // autres actions
    throw new ServletException("action " + action + " inconnue.");
}

On peut envisager également utiliser le mécanisme d'introspection pour déterminer la bonne méthode.

@Action(name = "/backoffice/liste.do")
String doBackOfficeList(HttpServletRequest request,
        HttpServletResponse response) throws ...

Le traitement des vues :

private void processView(String view, HttpServletRequest request,
        HttpServletResponse response) throws Throwable {
    if (view.endsWith(".jsp") || view.endsWith(".jspx")) {
        // technologie JSP/JSPX
        request.getRequestDispatcher(view).forward(request, response);
    } else if (view.endsWith(".xsl")) {
        // technologie XSLT / JAXP
        processXsltView(view, request, response);
    } else {
        // autres technologies
        throw new IllegalStateException("no view " + view);
    }
}

Quelques derniers conseils

validation des données :

Traitement des formulaires :

Liens : Utilisez soit

Charte graphique :