1. Présentation et objectifs

inline

Le but est de créer une architecture "à la microservice".

Dans cette architecture, chaque composant a son rôle précis :

  • la servlet reçoit les requêtes HTTP, et les envoie au bon controller (rôle de point d’entrée de l’application)

  • le controlleur implémente une méthode Java par route HTTP, récupère les paramètres, et appelle le service (rôle de routage)

  • le service implémente le métier de notre micro-service

  • le repository représente les accès aux données (avec potentiellement une base de données)

Et pour s’amuser un peu, nous allons réaliser un micro-service qui nous renvoie des données sur les Pokemons !

micro service poke

On retrouve en général le même découpage dans les micro-services NodeJS avec express :

  • La déclaration de l’application (express)

  • La déclaration des routeurs (express.Router)

  • L’implémentation du code métier et les accès à une base de données

Nous allons donc développer un micro-service, qui exposera un canal de communication REST/JSON.

Pour ce faire, nous allons :

  • Créer des annotations Java pour représenter nos objects

  • Créer une servlet, qui se configurera dynamiquement pour router les requêtes au bon controlleur

  • Implémenter un petit service

2. La première Servlet et la structure projet

Pour commencer, créons une première servlet.

2.1. Initialisation du projet

2.1.1. Création de l’arborescence projet

Ajoutez-y les répertoires de sources java et de test :

$ mkdir -p src/main/java
$ mkdir -p src/test/java

Initialiser un fichier pom.xml à la racine du projet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.miage.alom.tp</groupId>
    <artifactId>handcrafting</artifactId>
    <version>0.1.0</version>
    <packaging>war</packaging> (1)

    <properties>
        <maven.compiler.source>21</maven.compiler.source> (2)
        <maven.compiler.target>21</maven.compiler.target> (3)
    </properties>

    <dependencies>
    </dependencies>

</project>
1 On va fabriquer un war
2 On indique à maven quelle version de Java utiliser pour les sources !
3 On indique à maven quelle version de JVM on cible !

2.2. Ecriture de la première servlet

Pour écrire notre première servlet, nous avons besoin de la dépendance jakarta.servlet-api. Cette dépendance aura le scope provided puisque:

  • nous en avons besoin à la compilation

  • à l’exécution, c’est Tomcat qui portera la librairie

Ajouter la dépendance suivante dans votre pom.xml

1
2
3
4
5
6
<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.0.0</version>
    <scope>provided</scope> (1)
</dependency>
1 On précise bien un scope provided à Maven

Écrire une première servlet :

src/main/java/FirstServlet.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

public class FirstServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        var writer = resp.getWriter();
        writer.println("Hello !"); (1)
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);

        System.out.println("Initialisation de la servlet"); (2)
    }
}
1 On dit bonjour !
2 On affiche un log au démarrage

Écrire un fichier web.xml pour déclarer la servlet :

src/main/webapp/WEB-INF/web.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>

<web-app>

    <display-name>handcraft</display-name> (1)

    <servlet>
        <servlet-name>dispatcherServlet</servlet-name> (2)
        <servlet-class>FirstServlet</servlet-class>
        <load-on-startup>1</load-on-startup> (4)
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/*</url-pattern> (3)
    </servlet-mapping>

</web-app>
1 Notre application
2 Notre servlet
3 On écoute l’ensemble des URLs !
4 load-on-startup permet de préciser qu’on souhaite démarrer la servlet immédiatement (sans attendre la première requête)

2.3. Installation de Tomcat

Nous avons besoin de Tomcat pour exécuter notre Servlet !

Télécharger tomcat depuis la page officielle : https://tomcat.apache.org/download-10.cgi

Prenez bien la 'Binary Distribution', sous la section 'Core'. Si vous prenez la source vous devrez compiler Tomcat vous-même ! Sous Linux, privilégiez le format .tar.gz, qui conserve les bons droits sur les fichiers.

2.3.1. Configuration pour IntelliJ IDEA

Ajouter le serveur Tomcat à IntelliJ

01 add tomcat intellij
02 tomcat intellij added

Créer une configuration d’exécution utilisant le Tomcat

03 tomcat run config server
04 tomcat run config artifacts

2.4. Démarrer notre première Servlet

Démarrez votre serveur Tomcat, avec votre servlet, et allez constater le résultat !

Votre application est disponible à l’URL http://localhost:8080

3. Passer votre servlet en mode "annotations" servlet-api 3.0

3.1. Le code

Depuis la version 3.0 de servlet-api, les servlets supportent les annotations Java.

Plus besoin de web.xml!

Supprimer le fichier web.xml, et le répertoire src/main/webapp.

Modifier la servlet pour ajouter une annotation java :

src/main/java/FirstServlet.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@WebServlet(urlPatterns = "/*",  (1) (2)
  loadOnStartup = 1) (3)
public class FirstServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        PrintWriter writer = resp.getWriter();
        writer.println("Hello !");
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);

        System.out.println("Initialisation de la servlet"); (2)
    }
}
1 On déclare la servlet avec une annotation java !
2 On déclare les URL d’écoute
3 et on déclare souhaiter démarrer la servlet sans attendre de première requête

3.2. Le packaging

Par défaut, Maven ne connaît pas les servlets 3.0. Il s’attend donc à trouver un fichier web.xml dans le répertoire src/main/webapp/WEB-INF.

Si on lance un mvn package après avoir supprimé le web.xml et le répertoire webapp, on obtient l’erreur suivante :

mvn package
$> mvn clean package
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------< com.miage.alom.tp:handcrafting >-------------------
[INFO] Building handcrafting 0.1.0
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ handcrafting ---
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ handcrafting ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /home/jwittouck/workspaces/alom/alom-2020-2021/tp/02-handcrafting/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ handcrafting ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 1 source file to /home/jwittouck/workspaces/alom/alom-2020-2021/tp/02-handcrafting/target/classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ handcrafting ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /home/jwittouck/workspaces/alom/alom-2020-2021/tp/02-handcrafting/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ handcrafting ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ handcrafting ---
[INFO] No tests to run.
[INFO]
[INFO] --- maven-war-plugin:2.2:war (default-war) @ handcrafting ---
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.thoughtworks.xstream.core.util.Fields (file:/home/jwittouck/.m2/repository/com/thoughtworks/xstream/xstream/1.3.1/xstream-1.3.1.jar) to field java.util.Properties.defaults
WARNING: Please consider reporting this to the maintainers of com.thoughtworks.xstream.core.util.Fields
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
[INFO] Packaging webapp
[INFO] Assembling webapp [handcrafting] in [/home/jwittouck/workspaces/alom/alom-2020-2021/tp/02-handcrafting/target/handcrafting-0.1.0]
[INFO] Processing war project
[INFO] Webapp assembled in [23 msecs]
[INFO] Building war: /home/jwittouck/workspaces/alom/alom-2020-2021/tp/02-handcrafting/target/handcrafting-0.1.0.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.635 s
[INFO] Finished at: 2019-01-11T14:55:59+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-war-plugin:2.2:war (default-war) on project handcrafting: Error assembling WAR: webxml attribute is required (or pre-existing WEB-INF/web.xml if executing in update mode) -> [Help 1] (1)
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
1 Maven n’est pas content, et veut un fichier web.xml !

Pour corriger ce comportement, il faut utiliser une version récente du plugin maven war. Pour ce faire, ajouter dans votre pom.xml le bloc suivant (en dessous de votre bloc dependencies)

pom.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version> (1)
            </plugin>
        </plugins>
    </pluginManagement>
</build>
1 La version 3.4.0 du maven-war-plugin ne nécessite pas de fichier web.xml par défaut, comme précisé dans la documentation

On relance un mvn package pour valider la configuration

mvn package
$> mvn clean package
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------< com.miage.alom.tp:w01-servlet >--------------------
[INFO] Building w01-servlet 0.1.0
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ w01-servlet ---
[INFO] Deleting /home/jwittouck/workspaces/univ-lille/alom-2024/exercices/corrections/w01-servlet/target
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ w01-servlet ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /home/jwittouck/workspaces/univ-lille/alom-2024/exercices/corrections/w01-servlet/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ w01-servlet ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 1 source file to /home/jwittouck/workspaces/univ-lille/alom-2024/exercices/corrections/w01-servlet/target/classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ w01-servlet ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /home/jwittouck/workspaces/univ-lille/alom-2024/exercices/corrections/w01-servlet/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ w01-servlet ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ w01-servlet ---
[INFO] No tests to run.
[INFO]
[INFO] --- maven-war-plugin:3.4.0:war (default-war) @ w01-servlet ---
[INFO] Packaging webapp
[INFO] Assembling webapp [w01-servlet] in [/home/jwittouck/workspaces/univ-lille/alom-2024/exercices/corrections/w01-servlet/target/w01-servlet-0.1.0]
[INFO] Processing war project
[INFO] Building war: /home/jwittouck/workspaces/univ-lille/alom-2024/exercices/corrections/w01-servlet/target/w01-servlet-0.1.0.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS (1)
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.915 s
[INFO] Finished at: 2024-09-15T14:55:58+02:00
[INFO] ------------------------------------------------------------------------
1 Maven est content !
Validez que votre servlet fonctionne toujours en la démarrant et en allant voir http://localhost:8080

4. La servlet dynamique

4.1. Les annotations

Nous allons utiliser des annotations Java customisées pour créer notre couche de routage. Ces annotations seront analysées par la servlet, avec l’aide des api java.lang.reflect, afin de configurer le routage des requêtes HTTP vers le bon controller.

Pour la couche Controller, nous allons créer 2 annotations :

  • @ServletController : afin de marquer une classe comme étant un controller dans notre architecture

  • @ServletRequestMapping : afin de marquer une méthode de controller comme devant recevoir des requêtes HTTP

Créer les annotations suivantes dans votre projet :

Positionnez votre code dans un package Java ! Par exemple dans com.miage.alom.servlet.
L’annotation @ServletController
1
2
3
@Retention(RetentionPolicy.RUNTIME) (1)
public @interface ServletController {
}
1 On met une rétention au runtime, puisque nous allons utiliser l’annotation à l’exécution
L’annotation ServletRequestMapping
1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME) (1)
public @interface ServletRequestMapping {
    // uri à écouter
    String uri(); (2)
}
1 On a encore une rétention au runtime
2 Notre annotation utilise un paramètre uri, permettant de déclarer quelle URI sera écoutée (comme ce qu’on peut faire avec une servlet)

4.2. Notre premier controller

Un controller simple qui dit bonjour
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@ServletController (1)
public class HelloController {

    @ServletRequestMapping(uri="/hello") (2)
    public String sayHello(){
        return "Hello World !";
    }

    @ServletRequestMapping(uri="/bye")
    public String sayGoodBye(){
        return "Goodbye !";
    }

    @ServletRequestMapping(uri="/boum")
    public String explode(){
        throw new RuntimeException("Explosion !"); (3)
    }

}
1 Nous utilisons ici notre annotation
2 La méthode sayHello écoute à l’URI /hello et renvoie une chaîne de caractères
3 La méthode explode lève une exception !

4.3. L’analyse dynamique du code

Notre servlet, que l’on nommera DispatcherServlet va analyser le code de notre controller, pour être capable de router les requêtes HTTP, et récupérer les résultats

Supprimez votre servlet précédente, elle ne nous sera plus utile pour la suite.

Pour réaliser notre servlet, nous allons travailler en TDD (test-driven-development).

J’ai implémenté pour vous les tests, il ne reste plus qu’à les faire passer !

4.3.1. JUnit et Maven

Pour utiliser les tests unitaires, il faut rajouter JUnit en dépendance maven.

Ajoutez les dépendances suivant dans votre pom.xml

pom.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId> (1)
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId> (2)
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.5.0</version>
    <scope>test</scope>
</dependency>
1 L’API de JUnit 5
2 Le moteur d’exécution

Il vous faut également surcharger la version du maven-surefire-plugin (qui est le plugin maven qui implémente la phase d’exécution des tests).

pom.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<pluginManagement>
    <plugins>
        <plugin>
            <artifactId>maven-war-plugin</artifactId>
            <version>3.4.0</version>
        </plugin>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.1.2</version> (1)
        </plugin>
    </plugins>
</pluginManagement>
1 On a besoin de la version 2.22.0 minimum pour JUnit 5 comme indiqué dans la documentation junit

4.3.2. Le test unitaire

Implémentez le test unitaire suivant :

DispatcherServletTest.java
  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
package com.miage.alom.servlet;

import com.miage.alom.controller.HelloController;
import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

class DispatcherServletTest { (1)

    @Test (2)
    void registerController_throwsIllegalArgumentException_forNonControllerClasses() {
        var servlet = new DispatcherServlet();

        assertThrows(IllegalArgumentException.class,
                () -> servlet.registerController(String.class));
        assertThrows(IllegalArgumentException.class,
                () -> servlet.registerController(SomeEmptyClass.class));
    }

    @Test
    void registerController_doesNotRegisters_nonAnnotatedMethods() {
        var servlet = new DispatcherServlet();

        servlet.registerController(SomeControllerClassWithAMethod.class);

        assertTrue(servlet.getMappings().isEmpty());
    }

    @Test
    void registerController_doesNotRegisters_voidReturningMethods() {
        var servlet = new DispatcherServlet();

        servlet.registerController(SomeControllerClassWithAVoidMethod.class);

        assertTrue(servlet.getMappings().isEmpty());
    }

    @Test (4)
    void registerController_shouldRegisterCorrectyMethods(){
        var servlet = new DispatcherServlet();

        servlet.registerController(SomeControllerClass.class);
        servlet.registerController(SomeOtherControllerClass.class);

        assertEquals("someGoodMethod",
                servlet.getMappingForUri("/test").getName());
        assertEquals("someOtherNiceMethod",
                servlet.getMappingForUri("/otherTest").getName());
    }

    @Test
    void registerHelloController_shouldWorkCorrectly(){
        var servlet = new DispatcherServlet();
        servlet.registerController(HelloController.class);

        assertEquals("sayHello", servlet.getMappingForUri("/hello").getName());
        assertEquals("sayGoodBye", servlet.getMappingForUri("/bye").getName());
        assertEquals("explode", servlet.getMappingForUri("/boum").getName());
    }
}


class SomeEmptyClass{}

(3)
@com.miage.alom.servlet.ServletController
class SomeControllerClassWithAMethod{
    public String myMethod(){
        return "test";
    }
}

@com.miage.alom.servlet.ServletController
class SomeControllerClassWithAVoidMethod{
    @com.miage.alom.servlet.ServletRequestMapping(uri="/test")
    public void myMethod(){}
}

@com.miage.alom.servlet.ServletController
class SomeControllerClass {
    @com.miage.alom.servlet.ServletRequestMapping(uri="/test")
    public String someGoodMethod(){
        return "Hello";
    }

    @com.miage.alom.servlet.ServletRequestMapping(uri="/test-throwing")
    public String someThrowingMethod(){
        throw new RuntimeException("some exception message");
    }

    @com.miage.alom.servlet.ServletRequestMapping(uri="/test-with-params")
    public String someThrowingMethod(Map<String, String[]> params){
        return params.get("id")[0];
    }
}

@com.miage.alom.servlet.ServletController
class SomeOtherControllerClass {
    @com.miage.alom.servlet.ServletRequestMapping(uri="/otherTest")
    public String someOtherNiceMethod(){
        return "Hello again";
    }
}
1 Notre classe de test
2 Nos tests sont annotés @Test
3 Quelques controlleurs d’exemple pour valider le fonctionnement de votre implémentation
4 On teste l’enregistrement du HelloController

4.3.3. La DispatcherServlet (code à trous)

Implémentez la servlet suivante :

La DispatcherServlet
 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
package com.miage.alom.servlet;

import com.miage.alom.controller.HelloController;

import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

@WebServlet(urlPatterns = "/*", loadOnStartup = 1)
public class DispatcherServlet extends HttpServlet {

    private Map<String, Method> uriMappings = new HashMap<>(); (1)

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        System.out.println("Getting request for " + req.getRequestURI());
        // TODO (3)
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        // on enregistre notre controller au démarrage de la servlet
        this.registerController(HelloController.class);
    }

    /**
     * This methods checks the following rules :
     * - The controllerClass is annotated with @ServletController
     * Then all methods are scanned and processed by the registerMethod method
     * @param controllerClass the controller to scan
     */
    protected void registerController(Class controllerClass){
        System.out.println("Analysing class " + controllerClass.getName());
        // TODO (2)
    }

    /**
     * This methods checks the following rules :
     * - The method is annotated with @ServletRequestMapping
     * - The @ServletRequestMapping annotation has a URI
     * - The method does not return void
     * If these rules are followed, the method and its URI are added to the uriMapping map.
     * @param method the method to scan
     */
    protected void registerMethod(Method method) {
        System.out.println("Registering method " + method.getName());
        // TODO (2)
    }

    protected Map<String, Method> getMappings(){
        return this.uriMappings;
    }

    protected Method getMappingForUri(String uri){
        return this.uriMappings.get(uri);
    }
}
1 Cette Map va contenir l’association entre une URI et la méthode Java qui l’écoute (annotée @ServletRequestMapping)
2 C’est là qu’il faut coder !
3 Cette méthode sera implémentée dans la partie 4.4

Il faut maintenant implémenter les méthodes registerController et registerMethod pour faire passer les tests unitaires.

Cette partie fait un usage intensif de l’api java.lang.reflect

Vous aurez surement besoin des méthodes

  • getAnnotation

  • getDeclaredMethods

  • getDeclaredAnnotation

  • newInstance

  • etc…​

4.4. Le routage des requêtes (code à trous)

Une fois les annotations analysées, le routage des requêtes se fait de la manière suivante :

  1. Récupération de l’URI entrante (depuis l’objet HttpServletRequest)

  2. Récupération de la méthode implémentant l’URI (issue de l’analyse du code)

    • Si aucune méthode n’est trouvée, renvoyer une erreur 404

  3. Instanciation du controller

  4. Récupération des paramètres (depuis l’objet HttpServletRequest)

  5. Appel de la méthode (avec les paramètres ou non)

    • En cas d’exception, renvoyer une erreur 500 avec le message de l’exception

    • En cas de succès, récupérer le résultat de l’appel, et renvoyer le résultat convertit en chaîne de caractères

Nous devons donc ici, implémenter la méthode doGet de notre DispatcherServlet.

4.4.1. Les tests unitaires du routage

Ajoutez les tests suivants dans le test unitaire de la DispatcherServlet :

Les tests unitaires du routage
 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
@Test
void doGet_shouldReturn404_whenNotMethodIsFound() throws IOException {
    var servlet = new DispatcherServlet();

    var req = mock(HttpServletRequest.class);
    var resp = mock(HttpServletResponse.class);
    when(req.getRequestURI()).thenReturn("/test");

    servlet.doGet(req, resp);

    verify(resp).sendError(404, "no mapping found for request uri /test");
}

@Test
void doGet_shouldReturn500WithMessage_whenMethodThrowsException() throws IOException {
    var servlet = new DispatcherServlet();

    servlet.registerController(SomeControllerClass.class);

    var req = mock(HttpServletRequest.class);
    var resp = mock(HttpServletResponse.class);
    when(req.getRequestURI()).thenReturn("/test-throwing");

    servlet.doGet(req, resp);

    verify(resp).sendError(500,
        "exception when calling method someThrowingMethod : some exception message");
}

@Test
void doGet_shouldReturnAResult_whenMethodSucceeds() throws IOException {
    var servlet = new DispatcherServlet();

    servlet.registerController(SomeControllerClass.class);

    var req = mock(HttpServletRequest.class);
    var resp = mock(HttpServletResponse.class);
    var printWriter = mock(PrintWriter.class);

    when(resp.getWriter()).thenReturn(printWriter);
    when(req.getRequestURI()).thenReturn("/test");

    servlet.doGet(req, resp);

    verify(printWriter).print((Object)"Hello");
}

@Test
void doGet_shouldReturnAResult_whenMethodWithParametersSucceeds() throws IOException {
    var servlet = new DispatcherServlet();

    servlet.registerController(SomeControllerClass.class);

    var req = mock(HttpServletRequest.class);
    var resp = mock(HttpServletResponse.class);
    var printWriter = mock(PrintWriter.class);

    when(req.getRequestURI()).thenReturn("/test-with-params");
    when(req.getParameterMap()).thenReturn(Map.of("id", new String[]{"12"}));
    when(resp.getWriter()).thenReturn(printWriter);

    servlet.doGet(req, resp);

    verify(printWriter).print((Object)"12");
}

@Test
void doGet_shouldReturnAResult_forHelloController() throws IOException {
    var servlet = new DispatcherServlet();
    servlet.registerController(HelloController.class);

    var req = mock(HttpServletRequest.class);
    var resp = mock(HttpServletResponse.class);
    var printWriter = mock(PrintWriter.class);

    when(req.getRequestURI()).thenReturn("/hello");
    when(resp.getWriter()).thenReturn(printWriter);

    servlet.doGet(req, resp);

    verify(printWriter).print((Object)"Hello World !");
}

Ces tests unitaires valident que les méthodes sont correctement appelées et que les erreurs sont renvoyées.

Vous devrez probablement ajouter l’import java suivant

import static org.mockito.Mockito.*;

Une fois tous les tests au vert , vous pouvez démarrer votre projet et requêter via votre navigateur web :

5. Le micro-service PokemonType

Pour la suite de ce TP, nous allons développer un micro-service pokemon-type, qui s’appuiera sur notre DispatcherServlet. Ce micro-service a pour but de gérer les données de référence des pokémons, à savoir les 151 types de pokemon existants.

pokemon service

Le micro-service sera composé de 3 niveaux:

  1. La DispatcherServlet

  2. Le PokemonController, qui va exposer une route dédiée

  3. Le PokemonRepository, qui va consommer un fichier JSON

Pour avoir quelques données à disposition, nous utiliserons les données de l’API https://pokeapi.co

5.1. La structure

Nous allons donner une structure à notre micro-service. Cette structure prendra la forme de packages Java.

On retrouvera cette organisation de packages dans l’ensemble de nos TPs.

Créez les packages suivants :

  • com.miage.alom.bo

  • com.miage.alom.controller

  • com.miage.alom.repository

Créez également le répertoire src/main/resources.

packages

5.2. La classe PokemonType

Pour commencer, nous allons créer notre objet métier.

Pour implémenter notre objet, nous devons nous inspirer des champs que propose l’API https://pokeapi.co.

Par exemple, voici ce qu’on obtient en appelant l’API (un peu simplifiée) :

Electhor !
{
    "base_experience": 261,
    "height": 16,
    "id": 145,
    "moves": [],
    "name": "zapdos",
    "sprites": {
        "back_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/145.png",
        "back_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/145.png",
        "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/145.png",
        "front_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/145.png"
    },
    "stats": [
        {
            "base_stat": 100,
            "effort": 0,
            "stat": {
                "name": "speed",
                "url": "https://pokeapi.co/api/v2/stat/6/"
            }
        },
        {
            "base_stat": 90,
            "effort": 0,
            "stat": {
                "name": "special-defense",
                "url": "https://pokeapi.co/api/v2/stat/5/"
            }
        },
        {
            "base_stat": 125,
            "effort": 3,
            "stat": {
                "name": "special-attack",
                "url": "https://pokeapi.co/api/v2/stat/4/"
            }
        },
        {
            "base_stat": 85,
            "effort": 0,
            "stat": {
                "name": "defense",
                "url": "https://pokeapi.co/api/v2/stat/3/"
            }
        },
        {
            "base_stat": 90,
            "effort": 0,
            "stat": {
                "name": "attack",
                "url": "https://pokeapi.co/api/v2/stat/2/"
            }
        },
        {
            "base_stat": 90,
            "effort": 0,
            "stat": {
                "name": "hp",
                "url": "https://pokeapi.co/api/v2/stat/1/"
            }
        }
    ],
    "types": [
        {
            "slot": 2,
            "type": {
                "name": "flying",
                "url": "https://pokeapi.co/api/v2/type/3/"
            }
        },
        {
            "slot": 1,
            "type": {
                "name": "electric",
                "url": "https://pokeapi.co/api/v2/type/13/"
            }
        }
    ],
    "weight": 526
}

Nous allons donc créer une classe Java qui reprend cette structure, mais en ne conservant que les champs qui nous intéressent.

com.miage.alom.bo.PokemonType.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.miage.alom.bo;

public class PokemonType { (1)

    private int id;
    private int baseExperience;
    private int height;
    private String name;
    private Sprites sprites; (3)
    private Stats stats; (3)
    private int weight;

    (2)

}
1 On sélectionne les champs "id", "name", et "sprites"
2 On a besoin des getters et setters par la suite (pour les générer, utilisez Alt+Inser sous IntelliJ)
3 Pour les objets imbriqués, on utilise d’autres classes
com.miage.alom.bo.Sprites.java
1
2
3
4
5
6
7
8
package com.miage.alom.bo;

public class Sprites {

    private String back_default;
    private String front_default;

}
com.miage.alom.bo.Stats.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.miage.alom.bo;

public class Stats {

    private Integer speed;
    private Integer defense;
    private Integer attack;
    private Integer hp;

}

5.3. Le PokemonTypeRepository

Le repository est donc la classe qui va consommer notre fichier JSON et retourner notre Pokemon.

Le repository va utiliser l’API jackson-databind pour convertir le JSON en objet Java

5.3.1. jackson-databind

Ajouter la dépendance suivante à votre projet :

pom.xml
1
2
3
4
5
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.2</version>
</dependency>

Ecrire un test unitaire pour apprendre à manipuler jackson-databind :

JacksonDatabindTest.java
 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
class JacksonDatabindTest {

    public static class Car { (1)
        public String color; (2)
        public String brand;
    }

    @Test
    void testWriteJson() throws JsonProcessingException { (3)
        var objectMapper = new ObjectMapper();
        var car = new Car();
        car.color = "yellow";
        car.brand = "renault";
        var json = objectMapper.writeValueAsString(car);
        assertEquals("{\"color\":\"yellow\",\"brand\":\"renault\"}", json);
    }

    @Test
    void testReadJson() throws IOException { (4)
        var objectMapper = new ObjectMapper();
        var json = "{ \"color\" : \"black\", \"brand\" : \"opel\" }";
        var car = objectMapper.readValue(json, Car.class);
        assertEquals("black", car.color);
        assertEquals("opel", car.brand);
    }

}
1 La classe qui représente nos données
2 On positonne les champs en visibilité public pour ne pas avoir à écrire de getters/setters sur ce cas de test
3 L’écriture de JSON depuis notre objet
4 La lecture d’un JSON pour reconstruire un objet

Plus d’infos sur le Github de jackson-databind

Dans la DispatcherServlet, on peut utiliser jackson-databind pour transformer le résultat de nos appels de controllers en JSON !

5.3.2. Le jeu de données du repository

Récupérez le fichier pokemons.json et enregistrez-le dans le répertoire src/main/resources de votre projet.

5.3.3. Les tests unitaires du repository

Comme pour la DispatcherServlet, nous allons travailler en TDD.

Voici la classe de tests unitaires à implémenter

com.miage.alom.repository.PokemonTypeRepositoryTest.java
 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
package com.miage.alom.repository;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class PokemonTypeRepositoryTest {

    private PokemonTypeRepository repository = new PokemonTypeRepository();

    @Test
    void findPokemonById_with25_shouldReturnPikachu(){ (1)
        var pikachu = repository.findPokemonById(25);
        assertNotNull(pikachu);
        assertEquals("pikachu", pikachu.getName());
        assertEquals(25, pikachu.getId());
    }

    @Test
    void findPokemonById_with145_shouldReturnZapdos(){ (1)
        var zapdos = repository.findPokemonById(145);
        assertNotNull(zapdos);
        assertEquals("zapdos", zapdos.getName());
        assertEquals(145, zapdos.getId());
    }

    @Test
    void findPokemonByName_withEevee_shouldReturnEevee(){ (2)
        var eevee = repository.findPokemonByName("eevee");
        assertNotNull(eevee);
        assertEquals("eevee", eevee.getName());
        assertEquals(133, eevee.getId());
    }

    @Test
    void findPokemonByName_withMewTwo_shouldReturnMewTwo(){ (2)
        var mewtwo = repository.findPokemonByName("mewtwo");
        assertNotNull(mewtwo);
        assertEquals("mewtwo", mewtwo.getName());
        assertEquals(150, mewtwo.getId());
    }

    @Test
    void findAllPokemon_shouldReturn151Pokemons(){
        var pokemons = repository.findAllPokemon();
        assertNotNull(pokemons);
        assertEquals(151, pokemons.size());
    }

}
1 On valide la récupération d’un pokemon par son id
2 et par son nom

5.3.4. Le PokemonTypeRepository

Et voici la classe du repository, à compléter !

com.miage.alom.repository.PokemonTypeRepository.java
 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
package com.miage.alom.repository;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.miage.alom.bo.PokemonType;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

public class PokemonTypeRepository {

    private List<PokemonType> pokemons;

    public PokemonTypeRepository() {
        try {
            var pokemonsStream = this.getClass().getResourceAsStream("/pokemons.json"); (1)

            var objectMapper = new ObjectMapper(); (2)
            var pokemonsArray = objectMapper.readValue(pokemonsStream, PokemonType[].class);
            this.pokemons = Arrays.asList(pokemonsArray);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public PokemonType findPokemonById(int id) {
        System.out.println("Loading Pokemon information for Pokemon id " + id);

        // TODO (3)
    }

    public PokemonType findPokemonByName(String name) {
        System.out.println("Loading Pokemon information for Pokemon name " + name);

        // TODO (3)
    }

    public List<PokemonType> findAllPokemon() {
        // TODO (3)
    }
}
1 On charge le fichier json depuis le classpath (maven ajoute le répertoire src/main/resources au classpath java !)
2 On utilise l’ObjectMapper de jackson-databind pour transformer les objets JSON en objets JAVA
3 On a un peu de code à compléter !

5.4. Le PokemonTypeController

Écrire un controller qui expose une route "/pokemon". Cette route pourra être appelée avec des paramètres éventuels, id ou name.

Les requêtes devant être implémentées sont donc, par exemple :

5.4.1. Les tests unitaires du PokemonTypeController

Implémenter les tests unitaires suivants :

com.miage.alom.controller.PokemonTypeControllerTest.java
 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
package com.miage.alom.controller;

import com.miage.alom.bo.PokemonType;
import com.miage.alom.repository.PokemonTypeRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class PokemonTypeControllerTest {

    @InjectMocks
    PokemonTypeController controller;

    @Mock
    PokemonTypeRepository pokemonRepository;

    @BeforeEach
    void init(){
        MockitoAnnotations.initMocks(this);
    }

    @Test
    void getPokemon_shouldRequireAParameter(){
        var exception = assertThrows(IllegalArgumentException.class,
                () -> controller.getPokemon(null));
        assertEquals("parameters should not be empty", exception.getMessage());
    }

    @Test
    void getPokemon_shouldRequireAKnownParameter(){
        var parameters = Map.of("test", new String[]{"25"});
        var exception = assertThrows(IllegalArgumentException.class,
                () -> controller.getPokemon(parameters));
        assertEquals("unknown parameter", exception.getMessage());
    }

    @Test
    void getPokemon_withAnIdParameter_shouldReturnAPokemon(){
        var pikachu = new PokemonType();
        pikachu.setId(25);
        pikachu.setName("pikachu");
        when(pokemonRepository.findPokemonById(25)).thenReturn(pikachu);

        var parameters = Map.of("id", new String[]{"25"});
        var pokemon = controller.getPokemon(parameters);
        assertNotNull(pokemon);
        assertEquals(25, pokemon.getId());
        assertEquals("pikachu", pokemon.getName());

        verify(pokemonRepository).findPokemonById(25);
        verifyNoMoreInteractions(pokemonRepository);
    }

    @Test
    void getPokemon_withANameParameter_shouldReturnAPokemon(){
        var zapdos = new PokemonType();
        zapdos.setId(145);
        zapdos.setName("zapdos");
        when(pokemonRepository.findPokemonByName("zapdos")).thenReturn(zapdos);

        var parameters = Map.of("name", new String[]{"zapdos"});
        var pokemon = controller.getPokemon(parameters);
        assertNotNull(pokemon);
        assertEquals(145, pokemon.getId());
        assertEquals("zapdos", pokemon.getName());

        verify(pokemonRepository).findPokemonByName("zapdos");
        verifyNoMoreInteractions(pokemonRepository);
    }

    @Test
    void pokemonTypeController_shouldBeAnnotated(){
        var controllerAnnotation =
                PokemonTypeController.class.getAnnotation(ServletController.class);
        assertNotNull(controllerAnnotation);
    }

    @Test
    void getPokemon_shouldBeAnnotated() throws NoSuchMethodException {
        var getPokemonMethod =
                PokemonTypeController.class.getDeclaredMethod("getPokemon", Map.class);
        var requestMappingAnnotation =
                getPokemonMethod.getAnnotation(ServletRequestMapping.class);

        assertNotNull(requestMappingAnnotation);
        assertEquals("/pokemons", requestMappingAnnotation.uri());
    }

}

5.4.2. Le PokemonTypeController (code à trous)

Implémenter le PokemonTypeController et compléter la méthode !

com.miage.alom.controller.PokemonTypeController.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.miage.alom.controller;

import com.miage.alom.bo.PokemonType;
import com.miage.alom.repository.PokemonTypeRepository;

import java.util.Map;

public class PokemonTypeController {
    private PokemonTypeRepository repository = new PokemonTypeRepository();

    public PokemonType getPokemon(Map<String,String[]> parameters){
        // TODO
    }
}
Peut-être faut-il ajouter des annotations java sur le controller pour l’enregistrer auprès de la DispatcherServlet.

5.5. Modifications de la DispatcherServlet

Enfin, pour finaliser notre développement, nous devons :

  1. Enregistrer notre PokemonTypeController dans la DispatcherServlet (en modifiant la méthode init de la DispatcherServlet)

  2. Utiliser jackson-databind pour transformer les résultats de nos controlleurs en JSON

  3. Ne pas oublier de transmettre les paramètres reçus en requête au controlleur !

Testez votre micro-service en consultant les urls suivantes :