1. Aperçu du cycle de vie de SpringMVC et de ses neuf composants principaux
SpringMVC est un framework web basé sur le modèle MVC (Modèle-Vue-Contrôleur). Lorsqu'une requête HTTP arrive, le DispatcherServlet intercepte l'appel, le distrbiue au contrôleur approprié, puis traite la réponse. Ce processus peut être décomposé en plusieurs phases clés, orchestrées par des composants spécifiques.
- DispatcherServlet : Point d'entrée central qui reçoit toutes les requêtes et gère la délégation.
- HandlerMapping : Associe une URL entrante à un contrôleur et une méthode.
- HandlerAdapter : Invoque la méthode du contrôleur en adaptant la requête.
- ViewResolver : Résout le nom logique de la vue en une vue concrète.
- View : Génère le rendu final, par exemple une page JSP ou HTML.
Les autres composants incluent : Controller, ModelAndView, LocaleResolver, ThemeResolver, MultipartResolver, RequestToViewNameTranslator, etc.
2. Conception d'une version simplifiée de SpringMVC
Avant de coder, nous établissons une architecture minimale :
- Afficher une configuration basée sur un fichier
.propertiesou.xml. - Scanner un package pour trouver les classes annotées avec
@MyController. - Enregistrer les méthodes annotées avec
@MyRequestMappingdans une table de routage. - Utiliser un mécanisme de réflexion pour invoquer ces méthodes lors de l'appel HTTP.
- Gérer les paramètres des requêtes avec
@MyRequestParam.
3. Implémentation pas à pas
3.1 Structure du projet
Tous les fichiers sont situés dans le répertoire src/main/java avec les paquets suivants :
www.mxh.com.annotation: annotations personnaliséeswww.mxh.com.controller: contrôleurs d'exemplewww.mxh.com.servlet: servlet de distribution
3.2 Fichier pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>www.mxh.com</groupId>
<artifactId>MiniSpringMVC</artifactId>
<packaging>war</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>MiniSpringMVC Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>MiniSpringMVC</finalName>
</build>
</project>
3.3 Configuration web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>www.mxh.com.servlet.MiniDispatcherServlet</servlet-class>
<init-param>
<param-name>configLocation</param-name>
<param-value>config.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
3.4 Fichier de configuration config.properties
scanPackage=www.mxh.com.controller
3.5 Annotations personnalisées
MyController.java
package www.mxh.com.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyController {
String value() default "";
}
MyRequestMapping.java
package www.mxh.com.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestMapping {
String value() default "";
}
MyRequestParam.java
package www.mxh.com.annotation;
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestParam {
String value();
}
3.6 Contrôleur de démonsrtation
TestController.java
package www.mxh.com.controller;
import javax.servlet.http.*;
import www.mxh.com.annotation.*;
@MyController
@MyRequestMapping("/demo")
public class TestController {
@MyRequestMapping("/hello")
public void handleHello(HttpServletRequest req, HttpServletResponse resp,
@MyRequestParam("message") String msg) throws Exception {
System.out.println("Paramètre reçu : " + msg);
resp.getWriter().write("Bonjour ! Message = " + msg);
}
@MyRequestMapping("/status")
public void handleStatus(HttpServletRequest req, HttpServletResponse resp) throws Exception {
resp.getWriter().println("Statut du système : OK");
}
}
3.7 Servlet de distribution principal
MiniDispatcherServlet.java
package www.mxh.com.servlet;
import java.io.*;
import java.lang.reflect.*;
import java.net.URL;
import java.util.*;
import java.util.Map.Entry;
import javax.servlet.*;
import javax.servlet.http.*;
import www.mxh.com.annotation.*;
@SuppressWarnings("serial")
public class MiniDispatcherServlet extends HttpServlet {
private Properties config = new Properties();
private List<String> classNameList = new ArrayList<>();
private Map<String, Object> beanContainer = new HashMap<>();
private Map<String, Method> routeTable = new HashMap<>();
private Map<String, Object> controllerInstances = new HashMap<>();
@Override
public void init(ServletConfig cfg) throws ServletException {
super.init(cfg);
String configFile = cfg.getInitParameter("configLocation");
loadConfiguration(configFile);
scanPackageClasses(config.getProperty("scanPackage"));
instantiateBeans();
buildRouteTable();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
processRequest(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
processRequest(req, resp);
}
private void processRequest(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String uri = req.getRequestURI();
String ctxPath = req.getContextPath();
uri = uri.replace(ctxPath, "").replaceAll("/{2,}", "/");
if (!routeTable.containsKey(uri)) {
resp.getWriter().write("404 - Ressource non trouvée");
return;
}
Method targetMethod = routeTable.get(uri);
Class<?>[] paramTypes = targetMethod.getParameterTypes();
Map<String, String[]> params = req.getParameterMap();
Object[] args = new Object[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++) {
String typeName = paramTypes[i].getSimpleName();
if (typeName.equals("HttpServletRequest")) {
args[i] = req;
} else if (typeName.equals("HttpServletResponse")) {
args[i] = resp;
} else if (typeName.equals("String")) {
// On prend la première valeur du paramètre par défaut
for (Entry<String, String[]> entry : params.entrySet()) {
args[i] = entry.getValue()[0];
break;
}
}
}
try {
Object instance = controllerInstances.get(uri);
targetMethod.invoke(instance, args);
} catch (Exception e) {
resp.getWriter().write("500 - Erreur serveur : " + e.getMessage());
}
}
private void loadConfiguration(String configPath) {
try (InputStream is = getClass().getClassLoader().getResourceAsStream(configPath)) {
config.load(is);
} catch (IOException e) {
e.printStackTrace();
}
}
private void scanPackageClasses(String pkg) {
String path = pkg.replace(".", "/");
URL url = getClass().getClassLoader().getResource("/" + path);
if (url == null) return;
File dir = new File(url.getFile());
for (File f : dir.listFiles()) {
if (f.isDirectory()) {
scanPackageClasses(pkg + "." + f.getName());
} else if (f.getName().endsWith(".class")) {
classNameList.add(pkg + "." + f.getName().replace(".class", ""));
}
}
}
private void instantiateBeans() {
for (String className : classNameList) {
try {
Class<?> clazz = Class.forName(className);
if (clazz.isAnnotationPresent(MyController.class)) {
String beanName = lowerFirstLetter(clazz.getSimpleName());
beanContainer.put(beanName, clazz.getDeclaredConstructor().newInstance());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void buildRouteTable() {
for (Object bean : beanContainer.values()) {
Class<?> clazz = bean.getClass();
if (!clazz.isAnnotationPresent(MyController.class)) continue;
String basePath = "";
if (clazz.isAnnotationPresent(MyRequestMapping.class)) {
basePath = clazz.getAnnotation(MyRequestMapping.class).value();
}
for (Method m : clazz.getMethods()) {
if (!m.isAnnotationPresent(MyRequestMapping.class)) continue;
MyRequestMapping rm = m.getAnnotation(MyRequestMapping.class);
String url = (basePath + "/" + rm.value()).replaceAll("/{2,}", "/");
routeTable.put(url, m);
try {
controllerInstances.put(url, clazz.getDeclaredConstructor().newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
private String lowerFirstLetter(String name) {
char[] chars = name.toCharArray();
if (chars[0] >= 'A' && chars[0] <= 'Z') {
chars[0] += 32;
}
return new String(chars);
}
}