CodeGym /Cursos /Java Server /Inicio del modo server

Inicio del modo server

Java Server
Nivel 22, Lección 2
Disponible

1. Inicio del servidor y configuración

Cuando levantamos por primera vez un servidor HTTP local, es muy tentador hacerlo «rápidamente»: escribir 8080 directamente en el código, ejecutarlo y alegrarse de la vida. Y sí, los primeros cinco minutos de vida del proyecto, uno quiere vivir así. El problema es que el código backend suele vivir más de cinco minutos, y el puerto 8080 en el mundo de los desarrolladores es como el enchufe del frigorífico: parece que está libre hasta que alguien enchufa allí el hervidor, el microondas y el cargador del portátil al mismo tiempo.

Para el modo server solo necesitamos unos pocos parámetros de inicio. Pero estos parámetros no son «parte de la lógica de la aplicación», sino «parte del entorno donde se ejecuta la aplicación». Hoy es tu portátil, mañana el portátil del mentor, pasado mañana el CI, y luego simplemente abrirás un segundo proyecto que también querrá el 8080 (y, por supuesto, ganará la pelea).

Ya tenemos el mapa de objetos JDK: entendemos que HttpServer vive mucho tiempo, y HttpExchange llega con cada petición. Ahora, con estos ladrillos, debemos construir un proceso vivo que realmente se enlace a host y port.

Por lo tanto, el inicio del servidor debe verse como una secuencia clara: obtenemos ServerConfig del modelo de configuración general del proyecto, creamos InetSocketAddress, creamos HttpServer, llamamos a start(), e inmediatamente escribimos un log comprensible: qué dirección, qué puerto, qué nombre de aplicación. Esto no es «manía por la manía» — es un hábito básico que distingue el «código para uno mismo por la noche» del «código que puedes explicar y mantener».

Para que sea más fácil orientarse, aquí tienes el fragmento mínimo de application.properties que necesitamos justo para el server-mode:

Archivo: src/main/resources/application.properties

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

No repetimos aquí la mecánica de lectura de properties (eso fue en el día sobre configuración), pero mantenemos una idea importante en mente: el servidor se inicia con lo que viene de la configuración, no con lo que se escribió al azar en la clase LocalApiServer.

2. ServerConfig: record en lugar de parámetros

Cuando inicias un servidor, es fácil que se cuele «un par de argumentos extra» en el código. Primero necesitas host y port. Luego de repente quieres añadir appName (y tiene sentido — queremos devolverlo en /health). Luego, algo más. Si pasas todos estos valores como parámetros separados, tu código se convierte rápidamente en un «tren de argumentos», donde es fácil confundir el orden, los tipos y el significado.

Para estos casos, es muy conveniente crear un pequeño objeto de configuración. En Java 21+ (y versiones posteriores como Java 25), el formato ideal para esto es un record: es corto, claro y encaja perfectamente como «transportador de ajustes».

Aquí está la forma mínima de ServerConfig que será suficiente para el inicio de hoy:

package com.example.readlater.config;

/**
 * Configuración de inicio del servidor HTTP.
 * Aquí solo lo que pertenece al entorno (dónde y cómo iniciamos), no a la lógica de negocio.
 */
public record ServerConfig(
        String host,   // Host/interfaz para el bind (por ejemplo, localhost)
        int port,      // Puerto que escuchará el servidor
        String appName // Nombre de la aplicación para logs/health, etc.
) {
}

Observa un detalle: colocamos ServerConfig en el paquete config. Esto refleja honestamente el rol del objeto: no es «lógica de servidor», sino ajustes para el modo servidor.

Ahora la pregunta: ¿de dónde sacamos ServerConfig? Lo ideal es de tu modelo de configuración general (llamémoslo AppConfig). El nombre de las clases puede ser diferente, pero la idea es la misma: la configuración de la aplicación almacena appName, server.host, server.port. Luego, o almacenamos ServerConfig como parte de AppConfig, o lo ensamblamos sobre la marcha.

Ejemplo de ensamblado (breve y sin profundizar en la lectura de properties):

import com.example.readlater.config.ServerConfig;

// En el ejemplo, los valores están "hardcodeados" solo para demostración.
// En una aplicación real, deben tomarse de la configuración ya cargada (properties/env/etc).
ServerConfig serverConfig = new ServerConfig(
        "localhost",
        8080,
        "readlater-starter"
);

En un proyecto real, en lugar de literales, habrá valores del AppConfig ya cargado. Y la principal ventaja es que a partir de ahora, todo el código de inicio del servidor recibe un solo objeto. No tres parámetros, no cinco, no «y además una banderita», sino un compacto ServerConfig.

3. LocalApiServer: una clase separada para el inicio

Existe una vieja leyenda: si escribes todo en main() durante mucho tiempo, un día main() empezará a escribir código por ti. La leyenda, por supuesto, es falsa. Pero el hecho de que main() se hinche hasta cientos de líneas y se convierta en el lugar más temible del proyecto — eso es un destino muy real.

ReadLaterApplication (o como se llame tu punto de entrada) debe hacer una sola cosa: elegir el modo y ensamblar las dependencias. Pero iniciar el servidor, construir la dirección, capturar IOException, escribir el log de inicio — eso es una responsabilidad separada. Por eso extraemos el inicio en una clase separada, por ejemplo LocalApiServer.

Será muy pequeña. Y eso está bien. Es el «panel de control del servidor», no el «framework servidor con nuestro nombre». Sabe iniciar el servidor en la dirección de ServerConfig y registrar el resultado.

En este paso, LocalApiServer solo se encarga del inicio y del bind a la dirección. Es suficiente para demostrar que el proceso se ha convertido en un servidor. Cuando aparezca la primera ruta externa, la misma clase simplemente obtendrá el registro de /health y la respuesta JSON.

Estructura mínima:

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 {
        // Convertimos host/port de la configuración en una "dirección de red" donde escuchará el servidor
        InetSocketAddress address =
                new InetSocketAddress(config.host(), config.port());

        // backlog = 0 => usamos el valor por defecto (hoy no profundizamos en detalles)
        HttpServer server = HttpServer.create(address, 0);

        // Iniciamos la recepción de conexiones entrantes
        server.start();

        // Devolvemos el servidor para poder detenerlo correctamente si es necesario (server.stop(...))
        return server;
    }
}

De momento no hay logging — lo añadiremos en la siguiente sección. Y sí, el método devuelve HttpServer. Aunque ahora parezca «para qué devolverlo si igual no hago nada con él», es un hábito útil: conservamos la posibilidad de detener el servidor correctamente en el futuro mediante server.stop(...), sin convertir la aplicación en un conjunto de variables globales.

Ahora conectemos esto en el punto de entrada. Es importante: ReadLaterApplication no debe «construir el servidor ladrillo a ladrillo». Debe crear LocalApiServer y llamar a start(...). Eso es todo.

Por ejemplo, un fragmento de la rama SERVER (suponemos que launchMode ya está analizado):

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

// De momento el servidor solo se encarga del inicio y del bind a la dirección, por lo que tiene mínimas dependencias
LocalApiServer localApiServer = new LocalApiServer();

// Suponemos que appConfig ya se ha construido a partir de application.properties (u otra fuente)
ServerConfig serverConfig = appConfig.server(); // digamos que así

localApiServer.start(serverConfig);

Observa cómo se lee el código. Es casi «conversacional»: hay un servidor, hay una configuración, lo iniciamos. Y lo principal es que en ReadLaterApplication no aparecen detalles como InetSocketAddress y HttpServer.create(...). Estos son detalles de la parte servidora, su lugar está en LocalApiServer.

4. Creando HttpServer: dirección y start()

Ahora vamos a dar el paso más importante de la lección: desde «tenemos el modo server en palabras» hasta «realmente se inicia un servidor HTTP». Y en este punto es muy útil explicar directamente qué ocurre a nivel de código, sin intentar memorizarlo todo como un conjuro.

Tomamos host y port, construimos con ellos un InetSocketAddress, y lo pasamos a HttpServer.create(...). El segundo argumento de create(...) es el backlog. En lenguaje humano, backlog es «cuántas conexiones entrantes se pueden mantener en cola mientras el servidor está ocupado». En producción puede ser un tema interesante, pero hoy no. Ponemos 0 para usar el valor por defecto, y deliberadamente no entramos en la configuración de bajo nivel.

Añadamos a LocalApiServer el logging, el manejo de excepciones a nivel de llamada, y un log de inicio normal. Empecemos con el logger:

package com.example.readlater.app.server;

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

public class LocalApiServer {
    // Logger a nivel de clase: en los logs quedará claro de dónde viene el mensaje
    private static final Logger log =
            LoggerFactory.getLogger(LocalApiServer.class);
}

Ahora ampliemos el método start(...) para que registre el inicio. Es importante: queremos ver en los logs el enlace listo al que podremos acceder en la API. Por lo tanto, es mejor escribir el log en formato 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 {
    // Preparamos la dirección a la que se enlazará el servidor
    InetSocketAddress address =
            new InetSocketAddress(config.host(), config.port());

    // Creamos el servidor y lo iniciamos
    HttpServer server = HttpServer.create(address, 0);
    server.start();

    // Escribimos un log de inicio claro: se ve inmediatamente la URL y el nombre de la aplicación
    // {} son placeholders de SLF4J (sin concatenación de cadenas)
    log.info("Local API started: http://{}:{}, appName={}",
            config.host(), config.port(), config.appName());

    // Devolvemos server para que el código llamante pueda detenerlo más tarde si lo desea
    return server;
}

Observa lo que todavía no hay: el servidor ya escucha la dirección y sigue vivo después de start(), pero desde fuera aún no tiene nada que responder de verdad. Hasta que no aparezca al menos un createContext(...), un cliente externo no verá ningún endpoint útil con un contrato predecible.

Observa el estilo de SLF4J: no concatenamos cadenas con +, sino que usamos placeholders {}. Es a la vez más corto, más eficiente, y simplemente «así se hace» en proyectos reales. Y sí, es ese pequeño detalle que en el futuro separará tu código del código donde el logging se ha convertido en “println, pero con anotaciones”.

Otro punto importante. HttpServer.create(...) y start() pueden lanzar IOException. En la práctica, la causa más frecuente es que el puerto esté ocupado. Es normal. No es «se ha roto Java», no es «un JDK malo», es simplemente que alguien ya está escuchando en ese puerto. Por lo tanto, a nivel de ReadLaterApplication debemos capturar honestamente la excepción, registrarla y finalizar.

Ejemplo (breve, sin arquitectura innecesaria):

try {
    // Si el puerto está libre, el servidor se inicia y el proceso continúa viviendo en espera de peticiones
    localApiServer.start(serverConfig);
} catch (IOException e) {
    // Es importante pasar e: así en los logs aparecerá el stack trace (a menudo se ve inmediatamente BindException y la causa)
    log.error("Server failed to start on {}:{}",
            serverConfig.host(), serverConfig.port(), e);
}

Aquí hacemos dos cosas útiles a la vez. Primero, en el log se verá en qué dirección intentábamos iniciar. Segundo, conservamos el stack trace a través de e — será útil si no es «puerto ocupado», sino algo más raro.

5. Log de inicio del servidor

En el logging hay una regla simple: el log debe responder a la pregunta «¿qué hago ahora?». Con el inicio del servidor, esta regla funciona perfectamente. Después de iniciar la aplicación, normalmente quieres saber tres cosas: si el proceso se ha iniciado siquiera, en qué dirección escucha el servidor, y qué aplicación es (si tienes dos terminales y tres proyectos abiertos al mismo tiempo — y así será).

Por lo tanto, un buen log de inicio para server-mode suele contener host, port, appName, y a veces también una pista como «Presiona Ctrl+C para detener». Esto no es un «hábito profesional», es solo un pequeño cuidado hacia ti mismo del futuro. Al yo del futuro, por cierto, hay que quererlo: él tendrá que revisar tus propios logs.

Aquí hay un ejemplo de formato que es suficientemente informativo sin convertirse en una novela:

// La barra al final es un pequeño detalle visual: el enlace se ve "como una URL", es cómodo de copiar
log.info("Server started on http://{}:{}/ (appName={})",
        config.host(), config.port(), config.appName());

¿Por qué añadí / al final? Es un pequeño detalle visual: el enlace se ve «completo». Cuando lo copias con el ratón desde la terminal, a veces quieres que tenga forma de URL. No es obligatorio, pero es agradable.

Se puede añadir otra línea (si no quieres una demasiado larga):

// Indicación al usuario: en un escenario de aprendizaje, esta es la forma más sencilla de detener el servidor
log.info("Press Ctrl+C to stop the server.");

Y sí — este es un caso en el que un poco de texto en los logs es más útil que el silencio. Recuerda: nuestros logs son el «panel de instrumentos» de la aplicación. Si los instrumentos no muestran la velocidad, igual estás conduciendo, pero con un poco de nerviosismo.

Otro matiz: al iniciar el server-mode, es útil registrar que estamos en server-mode, especialmente si la aplicación se ejecuta desde el IDE y tienes comandos de cliente en paralelo. Te sorprenderá la frecuencia con la que alguien piensa «creí que había iniciado el servidor, pero inicié catalog details». Eso le pasa a la gente; la gente no es la JVM.

Ejemplo:

log.info("Launch mode: SERVER");

Lo principal es no pasarse. Dos o tres líneas está perfecto. Veintitrés líneas con arte ASCII ya es… también arte, pero no el que ayuda.

6. Ciclo de vida después de start()

Cuando escribes programas de consola, main() normalmente termina y el proceso finaliza. Es familiar: hiciste el trabajo, imprimiste el resultado, saliste. Una aplicación servidor vive de otra manera. Se inicia y luego «espera eventos» — peticiones entrantes. El servidor no tiene un «he terminado» natural (a menos que hayas escrito un servidor que se va de vacaciones después de la primera petición).

En HttpServer, después de start() aparecen hilos de trabajo que siguen escuchando el puerto y aceptando conexiones. Por lo tanto, aunque tu método startServer(...) haya retornado, la JVM sigue viva. Ese es el momento en que un principiante mira la terminal y piensa: «¡Vaya, se ha colgado!». No, no se ha colgado. Está funcionando. Es un servidor. Está esperando.

Lo importante a nivel de mentalidad: separar dos estados. «El programa se ha colgado» es cuando no puede continuar y no hace nada. «El servidor está funcionando» es cuando básicamente no hace nada (porque no hay peticiones), pero está listo para reaccionar al instante cuando aparezcan. Es como un guardia de seguridad: si está sentado tranquilamente, no significa que se haya roto. Significa que el día está tranquilo (de momento).

Otro punto: puede que quieras «bloquear el hilo principal» para que «no termine seguro». En HttpServer normalmente no es necesario, él mismo mantiene el proceso vivo. Y eso está bien: menos código, menos magia, menos «¿y por qué está aquí un CountDownLatch?». Mantenemos la simplicidad mientras no estorbe.

En esta etapa, es importante simplemente aceptar: en el modo server, la aplicación no debe finalizar inmediatamente. Debe iniciarse, registrar la dirección y vivir hasta que la detengas. La detención en nuestro escenario de aprendizaje es simplemente Ctrl+C. El tema del graceful shutdown es interesante, pero claramente no es el objetivo de hoy (y desde luego no el objetivo de esta lección).

7. Errores típicos al iniciar el servidor

En este punto suele ocurrir la primera serie de pequeños «¿por qué no funciona?». Es normal: estás pasando por primera vez de «todo dentro del proceso» a «hay un puerto, hay un servidor, hay un mundo exterior». Los errores aquí a menudo no son sobre Java, sino sobre el entorno y la disciplina de inicio.

Error nº1: hardcodear el puerto en el código.
Parece inofensivo escribir new InetSocketAddress("localhost", 8080) porque «bueno, estamos en nuestra máquina». Pero en tu propia máquina, el puerto 8080 puede estar ocupado por cualquier cosa: otro proyecto tuyo, Docker (si lo tienes), una demo de otro, un plugin del IDE. La estrategia correcta es tomar host/port de la configuración y tener la posibilidad de cambiar el puerto sin recompilar. Incluso en un proyecto de aprendizaje, esto ahorra tiempo.

Error nº2: un log «Server started» sin dirección.
Ese log parece animado, pero es inútil. Al segundo siguiente te preguntarás: «¿Y a dónde llamo?». Por eso, en el inicio del servidor casi siempre se registra la URL o al menos host:port. Un buen log reduce a la mitad la cantidad de «¿por qué no funciona?» — probado en personas (incluido el autor de estas líneas).

Error nº3: capturar IOException pero no registrar el stack trace.
A veces se escribe catch(IOException e) { log.error("Failed"); }. Es como «el médico te dijo: estás mal». Sí, gracias. Si el puerto está ocupado, verás en la excepción BindException (a menudo dentro), y eso explica el problema de inmediato. Por eso, en el log de error de inicio casi siempre se pasa e para obtener el stack trace. En un proyecto de aprendizaje, esto es especialmente importante: aprendes a leer los errores, no a temerlos.

Error nº4: iniciar el server-mode pero pensar que has iniciado el client-mode (o al revés).
Este es un error muy humano. Cambias args, ejecutas, ves que se ha impreso algo, y el cerebro dice: «Bueno, entonces es el servidor». Y luego resulta que has vuelto a hacer catalog details, y no hay ningún servidor. Se soluciona con un log del modo de inicio (Launch mode: SERVER) y el hábito de mirar el log de startup.

Error nº5: acumular código de inicio del servidor en ReadLaterApplication hasta que se vuelva ilegible.
Al principio parece que «es más fácil así». Pero ya en la siguiente lección añadirás /health, aparecerá ObjectMapper, aparecerán manejadores — y ReadLaterApplication se convertirá en un guión largo donde es difícil encontrar qué está pasando realmente. Es mucho más sano extraer LocalApiServer y mantener el código de inicio allí, dejando el punto de entrada como un «director de orquesta»: elegir el modo, ensamblar dependencias, llamar al componente adecuado.

1
Tarea
Java Server, nivel 22, lección 2
Bloqueada
`ServerConfig` de `application.properties`
`ServerConfig` de `application.properties`
1
Tarea
Java Server, nivel 22, lección 2
Bloqueada
El puerto de los argumentos tiene prioridad
El puerto de los argumentos tiene prioridad
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION