Activité Premier serveur HTTP

Consigne

  1. La durée de l’activité est estimée à environ 180 minutes.
  2. Avant de commencer l’activité :
    • Lire la mise en situation, prendre connaissance des objectifs, du résultat attendu, et des ressources à disposition.
    • Lire une première fois en entier la section Mise en route.
  3. Effectuez le travail décrit dans la section Mise en route et effectuez les tâches demandées dans la section Tâches.

Mise en situation

Vous avez été chargé de réaliser une application de messagerie instantanée pour votre site web. L’entreprise développe en Java avec le framework Spring et Spring Boot. Comme vous n’avez jamais pratiqué le développement d’application web en Java et que vous ne connaissez pas le framework Spring, vous décidez de vous lancer dans une activité d’exploration afin d’acquérir quelques bases.

Objectifs

À la fin de ce travail, vous devez :

  1. Connaître la différence entre le framework Spring et Spring Boot.
  2. Connaître la notion d’annotation en Java.
  3. Connaître la structure d’une application Spring Boot.
  4. Connaître l’annotation @SpringBootApplication et l’instruction SpringApplication.run(App.class, args);.
  5. Connaître les annotations @Controller, @RequestMapping, et @RequestBody.
  6. Connaître les annotations @GetMapping, @PostMapping, @PutMapping, @DeleteMapping .
  7. Connaître les utilitaires curl et postman.

Résultat attendu

  • Un projet Java fonctionnel
  • Une section dans votre rapport

Ressources

Matériel :

  • Visual Studio Code
  • Docker Desktop, Colima, Rancher Desktop

Mise en route

Installer Docker

Dans ce module, nous aurons besoin de conteneurs pour exécuter un serveur HTTP, notre application, notre SGBD, etc.

Pour cela, nous avons besoin d’une distribution de docker compatible avec Visual Studio Code :

  • Docker Desktop : C’est la solution la plus simple si vous avez votre propre matériel. Pour utiliser cette solution sur un PC d’entreprise, votre entreprise doit avoir une licence pour Docker Desktop.
  • Rancher Desktop : C’est la solution recommandée si vous avez un PC d’entreprise et que votre entreprise n’a pas de licence pour Docker Desktop.
  • Colima : C’est la solution recommandée pour macOS et Linux. C’est une solution libre et plus légère que Docker Desktop.

Installez la solution la plus appropriée en utilisant votre gestionnaire de paquets (brew, choco, etc.)

Installer les packs d’extensions pour VSCode

Pour ce module nous avons besoin de plusieurs extensions pour Visual Studio Code (VSCode) :

  • Extension Pack for Java
  • Spring Boot Extension Pack
  • Editor config

Ouvrez VSCode et installez ces trois extensions.

Récupérer le projet depuis Gitlab

Pour récupérer le projet, restez dans VSCode, ouvrez une fenêtre de terminal intégré, rendez-vous dans votre répertoire de projet (cd $HOME/projects), créez un répertoire pour le module 295 (mkdir m295), rendez-vous dans ce répertoire (cd m295), et lancez les commandes suivantes :

1
2
git clone git@gitlab.epai-ict.ch:m295/java/firstserver.git
code -r firstserver

Si la commande git clone vous renvoie une erreur, cela signifie probablement que votre clé SSH n’est pas chargée, ou que vous ne l’avez pas ajouté à votre compte Gitlab. Si vous ne savez pas comment faire, demandez de l’aide à votre enseignant.

Lorsque le projet est chargé, lancez les commandes ci-dessous pour builder le projet et exécuter le fichier .jar.

1
2
./mvnw verify
java -jar target/firstserver-0.0.1-SNAPSHOT.jar
1
2
3
4
5
6
7
8
9
...
[INFO] The original artifact has been renamed to /Users/frossardj/projects/firstserver/target/firstserver-0.0.1-SNAPSHOT.jar.original
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.543 s
[INFO] Finished at: 2024-10-10T17:05:17+02:00
[INFO] ------------------------------------------------------------------------
Hello, World!

Si tout fonctionne correctement, vous devriez avoir un résultat similaire à celui de la figure ci-dessus.

Spring et Spring Boot

Spring est un framework développé par VMware (Broadcom), depuis l’acquisition de SpringSource en 2009. Au moment de l’acquisition, SpringSource était l’un des principaux contributeurs au projet Apache Tomcat.

Spring Boot est une extension du framework Spring qui permet de produire une application serveur directement exécutable avec un serveur HTTP embarqué (typiquement Tomcat ou Jetty).

Pour réaliser un premier serveur HTTP avec Spring et Spring Boot, on a besoin des bibliothèques suivantes :

  • spring-boot-starter-web
  • spring-boot-starter-test (pour les tests unitaires)

Et pour produire un fichier jar exécutable avec un serveur HTTP embarqué, on a besoin du plugin suivant :

  • spring-boot-maven-plugin

Ces dépendances ont déjà été ajoutées dans le fichier pom.xml de votre projet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    ...
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    ...

Tâches

Créer le serveur HTTP

Pour créer le serveur HTTP avec le framework Spring, on comment par annoter la classe principale avec l’annotation @SpringBootApplication :

1
2
3
4
5
6
7
8
9
10
11
package ch.epai.ict.m295.firstserver;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FirstserverApplication {

    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Puis de créer et lancer le serveur HTTP dans la méthode main, avec la méthode SpringApplication.run :

1
2
3
4
5
6
7
8
9
10
11
12
package ch.epai.ict.m295.firstserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FirstserverApplication {

    public static void main(String[] args) {
        SpringApplication.run(FirstserverApplication.class, args);
    }
}

Vous pouvez maintenant lancer le projet pour vérifier qu’il fonctionne, en cliquant le petit run qui devrait se trouver juste au-dessus de la méthode main. Le démarrage du serveur devrait ressembler à cela :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.4)

2024-10-10T18:27:02.047+02:00  INFO 36322 --- [firstserver] [           main] c.e.i.m.f.FirstserverApplication         : Starting FirstserverApplication using Java 23 with PID 36322 (/Users/frossardj/projects/firstserver/target/classes started by frossardj in /Users/frossardj/projects/firstserver)
2024-10-10T18:27:02.050+02:00  INFO 36322 --- [firstserver] [           main] c.e.i.m.f.FirstserverApplication         : No active profile set, falling back to 1 default profile: "default"
2024-10-10T18:27:02.613+02:00  INFO 36322 --- [firstserver] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2024-10-10T18:27:02.622+02:00  INFO 36322 --- [firstserver] [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2024-10-10T18:27:02.622+02:00  INFO 36322 --- [firstserver] [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.30]
2024-10-10T18:27:02.670+02:00  INFO 36322 --- [firstserver] [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2024-10-10T18:27:02.671+02:00  INFO 36322 --- [firstserver] [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 579 ms
2024-10-10T18:27:02.944+02:00  INFO 36322 --- [firstserver] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2024-10-10T18:27:02.949+02:00  INFO 36322 --- [firstserver] [           main] c.e.i.m.f.FirstserverApplication         : Started FirstserverApplication in 1.157 seconds (process running for 1.473)

En parcourant ces informations de démarrage, on peut apprendre que le serveur Tomecat a démarré sur le port 8080. Ouvrez un navigateur et naviguez vers l’URI http://localhost:8080/.

Que remarquez-vous ? Est-ce que le serveur HTTP fonctionne ?

Pour arrêter le serveur, placez le curseur dans la fenêtre de terminal où le serveur est lancé et pressez les touches Ctrl-c.

Créer un gestionnaire de requête

Pour créer un gestionnaire de requête, nous allons commencer par créer un nouveau ficher que nous appellerons HomeController.java dans le même répertoire que le fichier FirstserverApllication.java.

Ce fichier devrait avoir le contenu ci-dessous. Si ce n’est pas le cas, copiez ce code dans le fichier.

1
2
3
4
5
package ch.epai.ict.m295.firstserver;

public class HomeController {
    
}

Dans cette classe, nous allons ajouter une méthode handleHome et pour le moment, nous allons simplement écrire dans la sortie standard, un message qui nous indique que la méthode a bien été appelée.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package ch.epai.ict.m295.firstserver;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class HomeController {

    public void handleHome(
            HttpServletRequest request, 
            HttpServletResponse response) {

        System.out.println("Home page requested");
    }
}

Comme nous l’avons vue, nous devrions maintenant ajouter ce gestionnaire de requête à la liste des gestionnaires de requête du serveur HTTP.

Avec Spring, cela ne se fait pas de manière impérative (avec des instructions), mais de manière déclarative avec des annotations.

Pour informer le framework que notre classe est un contrôleur, nous lui ajoutons l’annotation @Controller et pour indiquer que la méthode handeHome est le gestionnaire de requête pour l’URI racine / et pour la méthode GET, on utilise l’annotation @RequestMapping :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package ch.epai.ict.m295.firstserver;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Controller
public class HomeController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public void handleHome(
            HttpServletRequest request, 
            HttpServletResponse response) {

        System.out.println("Home page requested");
    }
}

Nous pouvons maintenant relancer l’application de la même manière que précédemment, ouvrir un navigateur et naviguer sur l’URI http://localhost:8080/.

Cette fois, la page n’affiche rien du tout puisque nous n’avons rien écrit dans la réponse, mais on peut voir dans la fenêtre du terminal intégré que le gestionnaire de requête a bien été invoqué.

Écrire des données dans la réponse

On peut utiliser les méthodes de la réponse pour indiquer le code de statut et le type de contenu de la réponse, et le flux de sortie du socket attaché à la réponse pour envoyer le contenu.

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
package ch.epai.ict.m295.firstserver;

import java.io.IOException;
import java.io.PrintWriter;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Controller
public class HomeController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public void handleHome(
            HttpServletRequest request,
            HttpServletResponse response) throws IOException {

        response.setContentType("text/html");
        response.setStatus(200);

        PrintWriter out = response.getWriter();
        out.println("<html>");
        out.println("<head>");
        out.println("<title>Home</title>");
        out.println("</head>");
        out.println("<body>");
        out.println("<h1>Welcome to the home page</h1>");
        out.println("</body>");
        out.println("</html>");
        out.flush();
    }
}

On relance le serveur et cette fois le navigateur affiche un contenu en HTML lorsque l’on navigue vers l’URI http://localhost:8080/.

Répondre avec un contenu JSON

On peut utiliser les méthodes de la réponse pour indiquer le code de statut et le type de contenu de la réponse, et le flux de sortie du socket attaché à la réponse pour envoyer le contenu.

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
package ch.epai.ict.m295.firstserver;

import java.io.IOException;
import java.io.PrintWriter;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Controller
public class HomeController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public void handleHome(
            HttpServletRequest request,
            HttpServletResponse response) throws IOException {

        response.setContentType("application/json");
        response.setStatus(200);

        PrintWriter out = response.getWriter();
        out.println("{");
        out.println("  \"content\" : \"Welcome to the home page\"");
        out.println("}");
        out.flush();
    }
}

On relance le serveur et cette fois le navigateur affiche un contenu en HTML lorsque l’on navigue vers l’URI http://localhost:8080/.

Des annotations qui simplifient le code

Le framework Spring propose des annotations qui permettent de simplifier le code.

Dans notre exemple, comme le code renvoie toujours le code de statut 200, on peut utiliser l’annotation @ResponseStatus. On peut également indiquer le type de contenu produit par le gestionnaire de requête avec l’attribut produces de l’annotation @RequestMapping.

De plus, comme notre code se limite maintenant à écrire une chaîne de caractère dans le flux de sortie, on peut utiliser l’annotation @ResponseBody pour indiquer la valeur de retour de la méthode est le contenu de la réponse. Et comme nous n’utilisons plus ni le paramètre request ni le paramètre response, nous pouvons purement et simplement les supprimer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package ch.epai.ict.m295.firstserver;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

@Controller
public class HomeController {

    @RequestMapping(value = "/",
            method = RequestMethod.GET ,
            produces = "application/json")
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public String handleHome() {
        return "{ \"content\" : \"Welcome to the home page\" }";
    }
}

Pour finir, on peut encore éliminer un peu de code en utilisant l’annotation @GetMapping plutôt que l’annotation @RequestMapping avec l’attribut method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package ch.epai.ict.m295.firstserver;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

@Controller
public class HomeController {

    @GetMapping(value = "/", produces = "application/json")
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public String handleHome() {
        return "{ \"content\" : \"Welcome to the home page\" }";
    }
}

Produire du JSON directement avec des objets

Produire du JSON avec des chaînes de caractère, ça fonctionne, mais ce n’est pas très pratique. Comme une réponse JSON représente soit un objet soit un tableau, on peut demander au framework de produire une représentation JSON d’un objet ou d’une liste Java.

Définissons par exemple, la classe Message:

1
2
3
4
5
6
7
8
9
10
11
12
13
package ch.epai.ict.m295.firstserver;

public class Message {
    private String content;

    public Message(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}

Nous pouvons maintenant renvoyer une instance de cette classe comme contenu de notre réponse.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package ch.epai.ict.m295.firstserver;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

@Controller
public class HomeController {

    @GetMapping(value = "/", produces = "application/json")
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Message handleHome() {
        return new Message("Welcome to the home pages");
    }
}

Enfin, comme les gestionnaires de requête d’un contrôleur pour une API REST vont toujours renvoyer le contenu de la réponse, on peut encore simplifier en utilisant l’annotation @RestController plutôt que l’annotation @Controller et supprimer l’annotation @ResponseBody sur les gestionnaires de requête. Et comme, le code de statut 200 est le code de statut par défaut, il n’est pas nécessaire de spécifier le code de statut si l’on renvoie 200.

1
2
3
4
5
6
7
8
9
10
11
12
13
package ch.epai.ict.m295.firstserver;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HomeController {

    @GetMapping(value = "/", produces = "application/json")
    public Message handleHome() {
        return new Message("Welcome to the home pages");
    }
}

Ajouter une gestionnaire de requête pour la méthode POST

Ajoutons maintenant un handler pour la méthode POST. Dans la classe HomeController, ajouter la méthode suivante :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @PostMapping(value = "/", produces = "application/json")
    public String handleHomePost(
            HttpServletRequest request, 
            HttpServletResponse response) throws IOException {

            BufferedReader in = request.getReader();
            String requestContent = in.readLine();

            System.out.println(requestContent);

            PrintWriter out = response.getWriter();
            out.println("The content was : " + requestContent);
            out.flush();
    }

Pour effectuer une requête avec la méthode POST, on peut utiliser l’utilitaire curl. Pour cela, lancez la commande suivante dans une fenêtre de terminal.

1
curl -H "Content-Type: text/plain" -d "Coucou le monde !" http://localhost:8080

Dans la méthode handleHomePost, on utilise la méthode getReader de la requête pour lire son contenu. Mais là encore, les annotations du framework Spring peuvent nous faciliter la vie. Plutôt que de recevoir la requête et la réponse, on peut demander au framework de nous passer le contenu de la requête en paramètre avec l’annotation @RequestBody.

1
2
3
4
5
6
7
    @PostMapping(value = "/", produces = "application/json")
    public String handleHomePost(
            @RequestBody String requestContent) throws IOException {

            System.out.println(requestContent);
            return "The content was : " + requestContent;
    }

Lancez une nouvelle fois la commande curl.

1
curl -H "Content-Type: text/plain" -d "Coucou le monde !" http://localhost:8080

Enfin, le framework Spring est capable d’instancier directement un objet si on lui fournit du JSON. Essayons de passer un objet de type Message.

Mais pour cela, on doit d’abord modifier un peu la classe Message pour lui ajouter un constructeur sans paramètre. Ce constructeur est nécessaire pour que le framework puisse créer une instance avant d’utiliser la réflexion pour initialiser les variables membres.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package ch.epai.ict.m295.firstserver;

public class Message {
    private String content;

    public Message() {}

    public Message(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}
1
2
3
4
5
6
    @PostMapping(value = "/", produces = "application/json")
    public String handleHomePost(
            @RequestBody Message message) throws IOException {

            return "The content was : " + message.getContent() + "\n";
    }

Exécutez la commande ci-dessous pour essayer cette nouvelle version du gestionnaire de requête.

1
2
3
4
5
# powershell
curl -H "Content-Type: application/json" -d "{ \`"content\`": \`"Coucou le monde, depuis JSON\`" }"  http://localhost:8080

# bash ou zsh
curl -H "Content-Type: application/json" -d '{ "content": "Coucou le monde, depuis JSON" }'  http://localhost:8080