CodeGym /Cours /Java Server /Lancement du mode server

Lancement du mode server

Java Server
Niveau 22, Leçon 2
Disponible

1. Démarrage du serveur et configuration

Lorsque nous démarrons pour la première fois un serveur HTTP local, on a très envie de faire « vite fait » : écrire 8080 directement dans le code, lancer — et profiter de la vie. Et oui, pendant les cinq premières minutes de vie du projet, on a envie de vivre comme ça. Le problème, c'est que le code backend vit généralement plus de cinq minutes, et le port 8080 dans le monde des développeurs, c'est comme une prise à côté du frigo : on croit qu'elle est libre jusqu'au moment où quelqu'un y branche une bouilloire, un micro-ondes et le chargeur du portable en même temps.

Pour le mode server, nous n'avons besoin que de quelques paramètres de démarrage. Mais ces paramètres ne font pas « partie de la logique de l'application », mais « partie de l'environnement où l'application est exécutée ». Aujourd'hui c'est votre portable, demain — celui du mentor, après-demain — le CI, et ensuite vous ouvrirez simplement un deuxième projet qui voudra aussi le 8080 (et, bien sûr, il gagnera la bagarre).

La carte des objets JDK est déjà là : nous comprenons que HttpServer vit longtemps, tandis que HttpExchange arrive à chaque requête. Maintenant, à partir de ces briques, il faut assembler un processus vivant qui se binde réellement sur host et port.

C'est pourquoi le démarrage du serveur doit ressembler à une séquence claire : nous obtenons ServerConfig à partir du modèle de configuration générale du projet, assemblons InetSocketAddress, créons HttpServer, appelons start(), et immédiatement nous écrivons un journal compréhensible : quelle adresse, quel port, quel nom d'application. Ce n'est pas du « radotage pour le radotage » — c'est une habitude de base qui distingue « du code pour soi le soir » de « du code que vous pourrez expliquer et maintenir ».

Pour faciliter l'orientation, voici un extrait minimal de application.properties dont nous avons besoin spécifiquement pour le mode serveur :

Fichier : src/main/resources/application.properties

app.name=readlater-starter
server.host=localhost
server.port=8080

Nous ne répétons pas ici le mécanisme de lecture des properties (cela a été traité dans le jour sur la configuration), mais nous gardons à l'esprit une idée importante : le serveur démarre sur ce qui vient de la configuration, et non sur ce qui a été saisi par hasard dans la classe LocalApiServer.

2. ServerConfig : record au lieu de paramètres

Lorsque vous lancez un serveur, il est facile de laisser s'infiltrer « quelques arguments supplémentaires ». D'abord, on a besoin de host et port. Ensuite, on a soudainement envie d'ajouter appName (et c'est logique — on veut le retourner dans /health). Puis encore autre chose. Si toutes ces valeurs sont passées en paramètres séparés, votre code se transforme rapidement en « petit train d'arguments », où il est facile de confondre l'ordre, les types et le sens.

Pour ces cas-là, il est très pratique d'extraire un petit objet de configuration. En Java 25, le format idéal pour cela est le record : il est court, compréhensible et par son sens convient parfaitement comme « transporteur de paramètres ».

Voici la forme minimale de ServerConfig qui suffira pour le lancement d'aujourd'hui :

package com.example.readlater.config;

/**
 * Configuration de démarrage du serveur HTTP.
 * Seulement ce qui concerne l'environnement (où et comment on démarre), pas la logique métier.
 */
public record ServerConfig(
        String host,   // Hôte/interface pour le bind (par exemple localhost)
        int port,      // Port que le serveur va écouter
        String appName // Nom de l'application pour les logs/health etc.
) {
}

Remarquez un détail : nous plaçons ServerConfig dans le package config. Cela reflète honnêtement le rôle de l'objet : ce n'est pas de la « logique serveur », mais des paramètres pour le mode serveur.

Maintenant la question : d'où tirer ServerConfig ? Idéalement — depuis votre modèle de configuration générale (disons AppConfig). Le nom des classes peut être différent chez vous, mais l'idée est la même : la configuration de l'application stocke appName, server.host, server.port. Ensuite, soit nous stockons ServerConfig comme partie de AppConfig, soit nous l'assemblons sur place.

Exemple d'assemblage (court et sans approfondir la lecture des properties) :

import com.example.readlater.config.ServerConfig;

// Dans l'exemple, les valeurs sont "en dur" uniquement pour la démonstration.
// Dans une application réelle, il faut les prendre depuis la configuration déjà chargée (properties/env/etc).
ServerConfig serverConfig = new ServerConfig(
        "localhost",
        8080,
        "readlater-starter"
);

Dans un projet réel, au lieu de littéraux, on utilisera les valeurs provenant d'un AppConfig déjà chargé. Et le principal avantage est que par la suite, tout le code de démarrage du serveur accepte un seul objet. Pas trois paramètres, pas cinq, pas « et encore un petit drapeau », mais un seul ServerConfig compact.

3. LocalApiServer : une classe de démarrage séparée

Il y a une vieille légende : si on écrit longtemps tout dans main(), alors un jour main() commencera à écrire le code à votre place. La légende, bien sûr, est fausse. Mais que main() gonfle jusqu'à des centaines de lignes et devienne l'endroit le plus effrayant du projet — c'est un destin tout à fait réel.

ReadLaterApplication (ou quel que soit le nom de votre point d'entrée) doit faire une seule chose : choisir le mode et assembler les dépendances. Mais lancer le serveur, construire l'adresse, attraper IOException, écrire le journal de démarrage — c'est une responsabilité séparée. C'est précisément pour cela que nous extrayons le lancement dans une classe séparée, par exemple LocalApiServer.

Il sera très petit. Et c'est une bonne chose. C'est la « télécommande du serveur », pas un « framework serveur à notre nom ». Il sait démarrer le serveur sur l'adresse provenant de ServerConfig et journaliser le résultat.

À cette étape, LocalApiServer est uniquement responsable du démarrage et du bind sur l'adresse. C'est suffisant pour prouver que le processus est devenu un serveur. Lorsque le premier path externe apparaîtra, la même classe obtiendra simplement l'enregistrement de /health et la réponse JSON.

Ébauche minimale :

package com.example.readlater.app.server;

import com.example.readlater.config.ServerConfig;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.net.InetSocketAddress;

public class LocalApiServer {

    public HttpServer start(ServerConfig config) throws IOException {
        // Transforme host/port de la config en "adresse réseau" sur laquelle le serveur va écouter
        InetSocketAddress address =
                new InetSocketAddress(config.host(), config.port());

        // backlog = 0 => utilise la valeur par défaut (n'approfondissons pas les détails aujourd'hui)
        HttpServer server = HttpServer.create(address, 0);

        // Démarre l'acceptation des connexions entrantes
        server.start();

        // Retourne le serveur afin de pouvoir éventuellement l'arrêter proprement (server.stop(...))
        return server;
    }
}

Pour l'instant, il n'y a pas de journalisation — nous l'ajouterons dans la section suivante. Et oui, la méthode retourne HttpServer. Même si pour l'instant il semble « pourquoi retourner, je ne fais rien avec », c'est une habitude utile : nous conservons la possibilité à l'avenir d'arrêter correctement le serveur via server.stop(...), sans transformer l'application en un ensemble de variables globales.

Maintenant raccordons cela au point d'entrée. Important : ReadLaterApplication ne doit pas « construire le serveur brique par brique ». Il doit créer LocalApiServer et appeler start(...). C'est tout.

Par exemple, fragment de la branche SERVER (nous supposons que launchMode est déjà parsé) :

import com.example.readlater.app.server.LocalApiServer;
import com.example.readlater.config.ServerConfig;

// Pour l'instant le serveur ne fait que démarrer et se binder sur l'adresse, donc il a un minimum de dépendances
LocalApiServer localApiServer = new LocalApiServer();

// Supposons que appConfig a déjà été construit à partir de application.properties (ou autre source)
ServerConfig serverConfig = appConfig.server(); // par exemple, comme ceci

localApiServer.start(serverConfig);

Remarquez comme le code se lit. Il est presque « conversationnel » : il y a un serveur, il y a une config, on lance. Et surtout, dans ReadLaterApplication n'apparaissent pas les détails comme InetSocketAddress et HttpServer.create(...). Ce sont des détails de la partie serveur, leur place est dans LocalApiServer.

4. Création de HttpServer : adresse et start()

Nous allons maintenant faire l'étape la plus importante de la leçon : passer de « nous avons un mode server en paroles » à « nous avons réellement un serveur HTTP qui démarre ». Et à cet endroit, il est très utile de dire clairement ce qui se passe au niveau du code, sans essayer de tout mémoriser comme une incantation.

Nous prenons host et port, nous en faisons un InetSocketAddress, et nous le passons à HttpServer.create(...). Le second argument de create(...) est le backlog. Pour parler simplement, le backlog — c'est « combien de connexions entrantes peuvent être mises en attente pendant que le serveur est occupé ». En production, cela peut être un sujet intéressant, mais aujourd'hui non. Nous mettons 0 pour utiliser la valeur par défaut, et volontairement nous ne nous aventurons pas dans le réglage de bas niveau.

Ajoutons maintenant dans LocalApiServer la journalisation, la gestion des exceptions au niveau de l'appel, et un log de démarrage normal. Commençons par le logger :

package com.example.readlater.app.server;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LocalApiServer {
    // Logger sur la classe : dans les logs, on saura d'où vient le message
    private static final Logger log =
            LoggerFactory.getLogger(LocalApiServer.class);
}

Maintenant étendons la méthode start(...) pour qu'elle journalise le démarrage. Important : nous voulons voir dans les logs un lien tout prêt vers lequel nous irons dans l'API. Donc il est préférable d'écrire le log sous la forme http://host:port.

import com.example.readlater.config.ServerConfig;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.net.InetSocketAddress;

public HttpServer start(ServerConfig config) throws IOException {
    // Prépare l'adresse sur laquelle le serveur se bind
    InetSocketAddress address =
            new InetSocketAddress(config.host(), config.port());

    // Crée le serveur et le démarre
    HttpServer server = HttpServer.create(address, 0);
    server.start();

    // Écrit un log de démarrage compréhensible : on voit immédiatement l'URL et le nom de l'application
    // {} — placeholders SLF4J (sans concaténation de chaînes)
    log.info("Local API démarrée : http://{}:{}, appName={}",
            config.host(), config.port(), config.appName());

    // Retourne le server pour que le code appelant puisse éventuellement l'arrêter plus tard
    return server;
}

Remarquez ce qu'il n'y a pas encore : le serveur écoute déjà l'adresse et vit après start(), mais de l'extérieur, il n'a encore rien à répondre honnêtement. Tant qu'au moins un createContext(...) n'apparaît pas, un client externe ne verra pas de endpoint utile avec un contrat prévisible.

Remarquez le style SLF4J : nous ne concaténons pas les chaînes avec +, mais utilisons les placeholders {}. C'est à la fois plus court, plus efficace, et tout simplement « la façon de faire » dans les projets réels. Et oui, c'est ce petit détail qui, à l'avenir, distinguera votre code du code où la journalisation est devenue un « println, mais avec des annotations ».

Encore un point important. HttpServer.create(...) et start() peuvent lancer IOException. En pratique, la raison la plus fréquente est que le port est occupé. C'est normal. Ce n'est pas « Java qui a cassé », ni « un mauvais JDK », mais simplement quelqu'un qui écoute déjà ce port. C'est pourquoi, au niveau de ReadLaterApplication, nous devons honnêtement attraper l'exception, la journaliser et nous terminer.

Exemple (court, sans architecture superflue) :

try {
    // Si le port est libre — le serveur démarre et le processus continue de vivre en mode attente de requêtes
    localApiServer.start(serverConfig);
} catch (IOException e) {
    // Important de passer e : ainsi dans les logs on aura la stack trace (souvent on voit directement BindException et sa cause)
    log.error("Le serveur n'a pas pu démarrer sur {}:{}",
            serverConfig.host(), serverConfig.port(), e);
}

Ici, nous faisons deux choses utiles à la fois. Premièrement, le log montrera sur quelle adresse nous avons essayé de démarrer. Deuxièmement, nous conservons la stack trace via e — elle sera utile si ce n'est pas « port occupé » mais quelque chose de plus rare.

5. Journal de démarrage du serveur

En journalisation, il y a une règle simple : le log doit répondre à la question « que dois-je faire ensuite ? ». Avec le démarrage du serveur, cette règle fonctionne parfaitement. Après le lancement de l'application, vous voulez généralement comprendre trois choses : le processus a-t-il démarré, sur quelle adresse le serveur écoute-t-il, et de quelle application s'agit-il (si vous avez deux terminaux et trois projets ouverts en même temps — et cela arrivera).

C'est pourquoi un bon log de démarrage pour le mode serveur contient généralement host, port, appName, et parfois aussi une indication comme « Appuyez sur Ctrl+C pour arrêter ». Ce n'est pas une « habitude professionnelle », c'est simplement un petit soin pour vous-même du futur. Et vous-même du futur, d'ailleurs, il faut l'aimer : il va devoir dépouiller vos propres logs.

Voici un exemple de format suffisamment informatif sans devenir un roman :

// La barre oblique à la fin — pur détail visuel : le lien ressemble à "une URL", pratique à copier
log.info("Serveur démarré sur http://{}:{}/ (appName={})",
        config.host(), config.port(), config.appName());

Pourquoi ai-je ajouté / à la fin ? C'est un pur détail visuel : le lien semble « complet ». Lorsque vous le copiez avec la souris depuis le terminal, vous souhaitez parfois qu'il soit sous forme d'URL. Ce n'est pas obligatoire, mais agréable.

On peut ajouter une ligne supplémentaire (si on ne veut pas une seule ligne trop longue) :

// Indication pour l'utilisateur : dans un scénario d'apprentissage, c'est le moyen le plus simple d'arrêter le serveur
log.info("Appuyez sur Ctrl+C pour arrêter le serveur.");

Et oui — c'est un cas où un peu de texte dans les logs est plus utile que le silence. Souvenez-vous : nos logs sont le « tableau de bord » de l'application. Si les instruments n'affichent pas la vitesse, vous roulez quand même, mais avec un peu de nervosité.

Encore une nuance : au démarrage du mode serveur, il est utile de journaliser que nous sommes en mode serveur, surtout si l'application est lancée depuis l'IDE et que vous avez parfois des commandes client en parallèle. Vous seriez surpris de voir à quelle fréquence « je pensais avoir lancé le serveur, mais j'ai lancé catalog details ». Cela arrive à l'homme ; l'homme n'est pas une JVM.

Exemple :

log.info("Mode de lancement : SERVEUR");

L'essentiel est de ne pas en faire trop. Deux à trois lignes — parfait. Vingt-trois lignes avec de l'ASCII art — c'est… aussi de l'art, mais pas celui qui aide.

6. Cycle de vie après start()

Lorsque vous écrivez des programmes console, main() se termine généralement — et le processus se termine. C'est habituel : on a fait le travail, affiché le résultat, on sort. Une application serveur vit différemment. Elle démarre puis « attend des événements » — des requêtes entrantes. Un serveur n'a pas de « j'ai terminé » naturel (sauf si vous avez écrit un serveur qui part en vacances après la première requête).

Sur HttpServer après start(), des threads de travail apparaissent, qui continuent d'écouter le port et d'accepter les connexions. Donc même si votre méthode startServer(...) est retournée, la JVM reste en vie. C'est ce moment où le débutant regarde le terminal et pense : « Oh, ça a planté ». Non, ça n'a pas planté. Ça marche. C'est un serveur. Il attend.

Ce qu'il est important de faire au niveau de la réflexion : distinguer deux états. « Le programme a planté » — quand il ne peut pas continuer et ne fait rien. « Le serveur fonctionne » — quand il ne fait principalement rien (parce qu'il n'y a pas de requêtes), mais est prêt à réagir instantanément lorsqu'elles arrivent. C'est comme un vigile : s'il est assis tranquillement, ça ne veut pas dire qu'il est cassé. Ça veut dire que la journée est calme (pour l'instant).

Encore un point : vous pourriez avoir envie de « bloquer le thread principal » pour que « ça ne se termine surtout pas ». Avec HttpServer, ce n'est généralement pas nécessaire, il maintient déjà le processus en vie. Et c'est bien : moins de code, moins de magie, moins de « pourquoi y a-t-il un CountDownLatch ici ? ». Nous gardons la simplicité tant qu'elle ne gêne pas.

À cette étape, il est important d'accepter simplement : en mode server, l'application ne doit pas se terminer immédiatement. Elle doit démarrer, journaliser l'adresse — et vivre jusqu'à ce que vous l'arrêtiez. L'arrêt dans notre scénario d'apprentissage, c'est banalement Ctrl+C. Le sujet de l'arrêt gracieux (graceful shutdown) est intéressant, mais ce n'est clairement pas le but de la journée (et encore moins de cette leçon).

7. Erreurs typiques lors du démarrage du serveur

C'est à cet endroit que survient généralement la première série de petits « pourquoi ça ne marche pas ». C'est normal : vous passez pour la première fois de « tout à l'intérieur du processus » à « il y a un port, un serveur, un monde extérieur ». Les erreurs ici ne sont souvent pas liées à Java, mais à l'environnement et à la discipline de lancement.

Erreur n°1 : port en dur dans le code.
Il semble inoffensif d'écrire new InetSocketAddress("localhost", 8080) parce que « ben, on est sur notre machine ». Mais sur votre machine, le port 8080 peut être occupé par n'importe quoi : un autre de vos projets, Docker (si vous en avez), une démo, un plugin IDE. La bonne stratégie est de prendre host/port depuis la configuration et d'avoir la possibilité de changer de port sans recompilation. Même dans un projet d'apprentissage, cela fait gagner du temps.

Erreur n°2 : log "Serveur démarré" sans adresse.
Un tel log a l'air énergique, mais il est inutile. Vous vous poserez de toute façon la question la seconde suivante : « Où dois-je taper ? ». C'est pourquoi, dans le démarrage du serveur, on journalise presque toujours l'URL ou au moins host:port. Un bon log réduit le nombre de « pourquoi ça ne marche pas » d'environ la moitié — vérifié sur des humains (y compris l'auteur de ces lignes).

Erreur n°3 : attraper IOException mais ne pas journaliser la stack trace.
Parfois on écrit catch(IOException e) { log.error("Échec"); }. C'est comme « le médecin a dit : vous allez mal ». Oui, merci. Si le port est occupé, vous verrez dans l'exception BindException (souvent à l'intérieur), et cela explique immédiatement le problème. C'est pourquoi, dans le log en cas d'erreur de démarrage, on passe presque toujours e pour obtenir la stack trace. Dans un projet d'apprentissage, c'est particulièrement important : vous apprenez à lire les erreurs, pas à les craindre.

Erreur n°4 : lancer le mode serveur mais penser avoir lancé le mode client (ou vice versa).
C'est une erreur très humaine. Vous modifiez les args, vous lancez, vous voyez quelque chose s'afficher, et le cerveau dit : « Bon, c'est le serveur ». Ensuite il s'avère que vous avez encore fait catalog details, et il n'y a pas de serveur. On y remédie par un log du mode de lancement (Mode de lancement : SERVEUR) et par l'habitude de regarder le log de démarrage.

Erreur n°5 : entasser le code de démarrage du serveur dans ReadLaterApplication jusqu'à ce qu'il devienne illisible.
Au début, on se dit que « c'est plus simple comme ça ». Mais déjà dans une leçon, vous ajouterez /health, ObjectMapper apparaîtra, des handlers apparaîtront — et ReadLaterApplication se transformera en un long script où il est difficile de trouver ce qui se passe. Il est bien plus sain d'extraire LocalApiServer et de garder le code de démarrage là-bas, et de laisser au point d'entrée le rôle de « chef d'orchestre » : choisir le mode, assembler les dépendances, appeler le composant nécessaire.

1
Mission
Java Server, niveau 22, leçon 2
Bloqué
`ServerConfig` de `application.properties`
`ServerConfig` de `application.properties`
1
Mission
Java Server, niveau 22, leçon 2
Bloqué
Le port des arguments a la priorité
Le port des arguments a la priorité
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION