En el JDK hay un servidor HTTP desde Java 6 y casi nadie lo usa. No sustituye a Spring, pero para una herramienta interna, un punto de salud o una maqueta, evita añadir un marco de trabajo entero.
También te puede interesar
Levantarlo
HttpServer s = HttpServer.create(new InetSocketAddress(0), 0); // 0 = puerto libre
s.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
s.createContext("/hola", ic -> responder(ic, 200, "text/plain; charset=utf-8", "Hola desde el JDK"));
s.start();
salidaescuchando en http://localhost:33657
Tres detalles de esas cuatro líneas. El puerto 0 hace que el sistema asigne uno libre, que es lo que conviene en pruebas para no chocar con nada; se recupera luego con s.getAddress().getPort(). El segundo argumento de create es el tamaño de la cola de conexiones pendientes. Y el ejecutor de hilos virtuales es lo que convierte este servidor de juguete en algo que aguanta miles de peticiones simultáneas: sin él, atiende de una en una.
Responder
void responder(HttpExchange ic, int codigo, String tipo, String cuerpo) throws IOException {
byte[] b = cuerpo.getBytes(StandardCharsets.UTF_8);
ic.getResponseHeaders().set("Content-Type", tipo);
ic.sendResponseHeaders(codigo, b.length);
try (var os = ic.getResponseBody()) { os.write(b); }
}
sendResponseHeaders; después ya se han enviado y no se puede cambiar nada. Y la longitud va en bytes, no en caracteres: con acentos no es lo mismo. Si te equivocas ahí, el cliente se queda esperando. El try con recursos cierra el flujo, que es lo que da por terminada la respuesta.Parámetros de consulta
s.createContext("/saluda", ic -> {
var q = consulta(ic.getRequestURI().getQuery());
String nombre = q.getOrDefault("nombre", "mundo");
responder(ic, 200, "application/json; charset=utf-8",
"{\"saludo\":\"hola, " + nombre + "\"}");
});
No hay nada que analice la cadena de consulta: llega tal cual en getQuery() y hay que partirla a mano, sin olvidar URLDecoder para los %20 y los acentos.
POST con cuerpo, y el método equivocado
s.createContext("/eco", ic -> {
if (!"POST".equals(ic.getRequestMethod())) { responder(ic, 405, "text/plain", "solo POST"); return; }
String cuerpo = new String(ic.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
responder(ic, 200, "text/plain; charset=utf-8", "recibí " + cuerpo.length() + " bytes: " + cuerpo);
});
Probándolo todo desde el mismo programa, con el cliente HTTP del JDK:
salidaGET /hola -> 200 Hola desde el JDK
GET /saluda?nombre=Ana -> 200 {"saludo":"hola, Ana"}
GET /noexiste -> 404 <h1>404 Not Found</h1>No context found for request
POST /eco -> 200 recibí 17 bytes: mensaje de prueba
GET /eco -> 405 solo POST
El 404 lo da el servidor solo cuando ninguna ruta encaja. Las rutas se emparejan por prefijo y gana la más larga, así que un contexto en / actúa de comodín para todo lo demás.
Lo que no tiene
Conviene saber dónde está el límite: no hay rutas con parámetros (/libros/{id}), ni inyección de dependencias, ni serialización de JSON, ni HTTPS cómodo, ni middleware. Todo eso hay que escribirlo o traerlo de fuera.
Para qué sí vale: un punto de salud o de métricas dentro de un proceso que ya existe, una maqueta sin dependencias, una herramienta interna, o un servidor falso en las pruebas de integración. Para una aplicación web de verdad, Spring Boot, Quarkus o Javalin.
jwebserver -p 8000 -d /ruta. Sin escribir nada.Todo el código se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.