Comprendre le fonctionnement interne de SpringMVC et créer une implémentation simplifiée

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 .properties ou .xml.
  • Scanner un package pour trouver les classes annotées avec @MyController.
  • Enregistrer les méthodes annotées avec @MyRequestMapping dans 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ées
  • www.mxh.com.controller : contrôleurs d'exemple
  • www.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);
    }
}

Étiquettes: SpringMVC DispatcherServlet Annotation Java Servlet

Publié le 5 juillet à 17h32