C'est un design pattern qui permet de modéliser des applications « interactives » :
Ces trois aspects sont représentés par trois composants :
Une application Web typique :
La séparation permet d'obtenir :
Dans une application JSP typique, le modèle MVC peut être implémenté de la manière suivante :
On se place dans le cadre d'une application permettant d'accéder à une base de données de films. Le schéma logique de la base est représenté par le diagramme UML suivant :
CREATE TABLE PEOPLE (pid INTEGER, firstname VARCHAR(30),
lastname VARCHAR(30),
PRIMARY KEY(pid));
CREATE TABLE MOVIE (mid INTEGER, title VARCHAR(90) NOT NULL,
year INTEGER NOT NULL,
runtime INTEGER NOT NULL, rank INTEGER NOT NULL,
PRIMARY KEY (mid));
CREATE TABLE ROLE (mid INTEGER, pid INTEGER, name VARCHAR(70),
PRIMARY KEY(mid, pid, name),
FOREIGN KEY (mid) REFERENCES MOVIE,
FOREIGN KEY (pid) REFERENCES PEOPLE);
CREATE TABLE DIRECTOR (mid INTEGER, pid INTEGER, PRIMARY KEY (mid, pid),
FOREIGN KEY (mid) REFERENCES MOVIE,
FOREIGN KEY (pid) REFERENCES
PEOPLE);
Ici on veut faire une mini-application simplifiée pour rechercher toutes les personnes qui contiennent une chaîne donnée dans leur nom, trié par ordre alphabétique.
On va créer deux classes Java pour le modèle :
public class Person {
private final String firstname;
private final String lastname;
public Person(String f, String l) {
this.firstname = f;
this.lastname = l;
}
public String getFirstname() { return firstname; }
public String getLastname() { return lastname; }
}
Le getter pour une propriété foo est une méthode publique getFoo()
public class PersonDB {
Connection cnx;
public PersonDB() {
Class.forName("org.postgresql.Driver");
cnx = DriverManager.getConnection("jdbc:postgresql://host:port/base",
"username", "password");
}
public Vector<Person> getPersons (String s) throws SQLException {
Vector<Person> res = new Vector<>();
Statement st = cnx.createStatement();
ResultSet r = st.executeQuery("SELECT * FROM PEOPLE "
+ " WHERE LASTNAME LIKE "
+ " '%" + s + "%'");
while (r.next()) {
res.add(new Person<>(r.getString("FIRSTNAME"),
r.getString("LASTNAME")));
}
return res;
}
}
Class.forName("org.postgresql.Driver");
connection =
DriverManager.getConnection("jdbc:postgresql://host:port/" + base,
username, password);
Remarque : On devrait plutôt utiliser un PreparedStatement pour éviter l'injection de code. Ici on va utiliser le contrôleur pour valider la chaîne (exemple) mais ce n'est pas idéal
La classe HttpServlet permet d'implémenter le contrôleur. C'est vers cette classe que son compilées les pages JSP, mais dans le contrôleur, on ne va faire aucun affichage, mais calculer un résultat et le stocker pour que la vue puisse l'afficher.
//Cette annotation permet de dire que le Servlet sera
//associée à l'URL /APPNAME/PersonListServlet
@WebServlet("/PersonListServlet")
public class PersonListServlet extends HttpServlet {
//Tomcat se sert de l'ID Pour savoir qu'un servlet a été
//modifié et donc que la version en cache doit être invalidée
private static final long serialVersionUID = 1234L;
//La méthode appelée si la requête est POST
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(request, response);
}
//La méthode appelée si la requête est GET
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
…
}
}
Dans notre exemple on dit que si on est appelé en POST alors on fait la même chose qu'en GET.
try {
//On crée un Modèle et on le stocke dans la session
PersonDB db = (PersonDB) request.getSession().getAttribute("db");
if (db == null) {
db = new PersonDB();
request.getSession().setAttribute("db", db);
}
//Récupération du paramètre GET comme dans un JSP
String s = request.getParameter("s");
//Netoyage de la chaîne
String ss = s.replaceAll("([%_0-9;,]|--)+", "");
Vector<Person> v = db.getPersons(ss);
request.setAttribute("people", v);
RequestDispatcher rd =
request.getRequestDispatcher("/person_list.jsp");
rd.forward(request,response);
} catch (Exception e) { throw new ServletException(e); }
request.getRequestDispatcher("S2").forward (request,response)
request.getRequestDispatcher("S3").forward (request,response)
request.getRequestDispatcher("view.jsp").forward (request,response)
On peut ainsi enchaîner les servlets. Les Si peuvent travailler sur l'objet request ainsi que sur les headers de la réponse via l'objet response (.addCookie, addHeader, …).
Les Si ne doivent pas écrire le contenu de la réponse (i.e. pas de .getWriter() dans les servlets), sinon Si+1 renverra une erreur.
JSP Standard Tag Library : un outil définissant des balises spéciales dans des pages JSP. Ces balises permettent de faire de la publication de données (transformer des structures de données Java en balises HTML) de manière déclarative (sans écrire de code bas niveau).
Cet outil n'est pas intégré directement à J2E mais est très utilisé. Il faut placer le .jar correspondant dans le classpath de l'application.
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<html>
…
<body>
<form method="get" action="PersonListServlet">
Rechercher dans le nom : <input type="text" name="s" />
<button type="submit">Rechercher</button>
</form>
<c:choose>
<c:when test="${! empty people}">
<ul>
<c:forEach var="p" items="${people}">
<li> ${p.firstname} <b>${p.lastname}</b></li>
</c:forEach>
</ul>
</c:when>
<c:otherwise> Il n'y a pas de résultats ! </c:otherwise>
</c:choose>
</body>
<html>
JSTL propose deux outils :
Il existe d'autres balises spécialisées que celles présentées dans la suite, par exemple pour faire du XML ou des requêtes SQL depuis la vue.
Ce sont les balises « de base », auxquelles ont donne le préfixe c:
Une expression peut apparaître n'importe où dans des attributs ou des éléments de la page. Les expressions sont délimitées par ${…} et ne sont pas typées (il y aura une exception lors de l'exécution en cas d'erreur). Les expressions peuvent contenir :
On ne présente ici qu'une petite partie de JSTL. Il est important en particulier de bien se concentrer sur le TP et de lire les corrigés en ligne!
La classe RequestDispatcher permet d'effectuer une « redirection interne » (côté serveur) : la paire d'objet HttpServletRequest, HttpServletResponse est transmise à la ressource demandée (et la même requête y est donc effectuée)
// On suppose que l'on est dans la méthode doGet() de Servlet1
RequestDispatcher rd = request.getRequestDispatcher("/Servlet2");
rd.forward(request, response);
//a pour effet d'appeler Servlet2.doGet(request, response)
Ce comportement est transparent pour le client. De son point de vue, c'est toujours l'URL initiale (/Servlet1 dans l'exemple) qui répond à la requête.
Le chemin donné à request.getRequestDispatcher peut avoir deux formes :
L'accès aux fichiers se faisant uniquement côté serveur, on peut référencer des ressources se trouvant dans /WEB-INF qui sont inaccessibles au client.
Le protocole HTTP propose un ensemble de redirections. Ces
dernières sont des réponses à une requête HTTP indiquant
que la ressource demandée via GET ou POST a
été déplacée.
Il existe different code de redirection (301, 302, …, 308) avec
des significations différentes (redirection temporaire,
définitive, ré-écriture de POST en GET, …).
// On suppose que l'on est dans la méthode doGet() de Servlet1
response.sendRedirect("Servlet2");
Le client recharge la page demandée (/Servlet2 dans l'exemple).
La résolution de ressources faites par un sendRedirect() est faite par le client. Les règles standard s'appliquent :
Redirections internes
Dans les deux cas, la redirection ne peut être faite que
si la réponse n'a pas été envoyée au client (pas de sortie
HTML et aucune utilisation de out)
⇒ IllegalStateException.
Il est souvent utile d'appliquer du code générique pour un
grand nombre d'URLs/Servlets (par exemple loguer la page accédée,
vérifier que le client est authentifié etc…).
On utilise la
classe javax.servlet.Filter qui se décompose
en deux parties :
@WebFilter (urlPatterns = { "/*" })
public class CountFilter implements Filter {
public void doFilter(ServletRequest req_, ServletResponse resp_,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) req_;
ServletContext app = req.getServletContext();
synchronized (app) {
Map<String, Integer> map = (Map<String, Integer>) app.getAttribute("map");
if (map == null) {
map = new HashMap<>();
app.setAttribute("map", map);
}
String uri = req.getRequestURI(); // URI de la requête.
Integer c = map.get(uri);
if (c == null) c = 0;
map.put(uri, c + 1);
}
chain.doFilter(req, rep_);
}
À chaque fois que l'on accède à une ressource :
Un filtre doit :
Note : l'ordre des filtres ne peut être spécifié que dans le
fichier globale web.xml (non abordé).
On peut associer un filtre ou un servlet à plusieurs urls :
@WebFilter (urlPatterns = { "motif1", "motif2", … })
ou
@WebServlet (urlPatterns = { "motif1", "motif2", … })
avant la classe en question. La syntaxe des motifs est la suivante :
Des mappings complexes sont possibles via le fichier web.xml. On se contente des annotations sur les classes Java.
Si on suit le modèle MVC avec une architecture de Servlet :
Problème : le client peut accéder à un fichier .jsp sans être passé par le contrôleur en écrivant directement l'URL.
Solution : il suffit de placer les fichiers .jsp dans un répertoire WEB-INF/jsp/. Ces fichiers resterons accessibles via RequestDispatcher mais ne pourront être atteint par une requête HTTP
On ne place que les fichiers .jsp de vue dans WEB-INF. Les resources qui doivent rester accessible doivent être à l'extérieur (fichiers HTML, CSS, images, …)