Objetivo: construir un mini-servidor que recibe peticiones HTTP, consulta varias APIs públicas en paralelo (bitcoin, clima, USD) y responde combinando los resultados. Se refuerza la asincronía para I/O-bound y el manejo de timeouts/errores.
Exponer /resumen que devuelva un JSON con precio BTC, clima y tipo de cambio USD en paralelo, con límites de tiempo para evitar que un proveedor lento bloquee la respuesta. /salud verifica disponibilidad.
HttpClient.sendAsync y CompletableFuture en lugar de muchos hilos bloqueados.HttpClient compartido por request, con timeout total.CompletableFuture.allOf agrupa las llamadas en paralelo lógico; si alguna falla, no derriba las demás.HttpServer con rutas /resumen y /salud.HttpClient por request con timeout total.sendAsync para cada API y recogerlos con allOf.import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ServidorAsyncApis {
private static final Map<String, String> APIS = Map.of(
"btc", "https://api.coinbase.com/v2/prices/BTC-USD/spot",
"clima", "https://api.open-meteo.com/v1/forecast?latitude=-34.61&longitude=-58.38¤t=temperature_2m",
"usd", "https://api.exchangerate.host/latest?base=USD&symbols=EUR,ARS"
);
public static void main(String[] args) throws Exception {
int port = Integer.parseInt(System.getProperty("port", "8080"));
HttpServer server;
try {
server = HttpServer.create(new InetSocketAddress("0.0.0.0", port), 0);
} catch (java.net.BindException be) {
System.err.println("Puerto " + port + " ocupado, intentando puerto libre...");
server = HttpServer.create(new InetSocketAddress("0.0.0.0", 0), 0);
}
ExecutorService vThreads = Executors.newVirtualThreadPerTaskExecutor();
server.setExecutor(vThreads);
server.createContext("/resumen", new ResumenHandler());
server.createContext("/salud", exchange -> responder(exchange, 200, "{\"status\":\"ok\"}"));
server.start();
System.out.println("Servidor en http://localhost:" + server.getAddress().getPort());
}
static class ResumenHandler implements HttpHandler {
public void handle(HttpExchange exchange) throws IOException {
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
responder(exchange, 405, "{\"error\":\"metodo no permitido\"}");
return;
}
double timeoutS = leerTimeout(exchange.getRequestURI());
Duration timeout = Duration.ofMillis((long) (timeoutS * 1000));
Instant inicio = Instant.now();
HttpClient client = HttpClient.newBuilder()
.connectTimeout(timeout)
.build();
var tareas = APIS.entrySet().stream()
.map(e -> llamarApi(client, e.getKey(), e.getValue(), timeout))
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(tareas).join();
Map<String, Object> resultados = new LinkedHashMap<>();
for (CompletableFuture<ResultadoApi> f : (CompletableFuture<ResultadoApi>[]) tareas) {
ResultadoApi r = f.join();
resultados.put(r.nombre(), r.data());
}
long msTotal = Duration.between(inicio, Instant.now()).toMillis();
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("origen", "httpclient-sendAsync");
payload.put("timeout_s", timeoutS);
payload.put("asincronia", "Un hilo de servidor + futures; ideal para I/O-bound.");
payload.put("multithreading", "Sin crear cientos de hilos; las esperas aparcan hilos virtuales.");
payload.put("ms_total", msTotal);
payload.put("resultados", resultados);
responder(exchange, 200, Json.simple(payload));
}
}
record ResultadoApi(String nombre, Map<String, Object> data) {}
private static CompletableFuture<ResultadoApi> llamarApi(HttpClient client, String nombre, String url, Duration timeout) {
Instant inicio = Instant.now();
HttpRequest req = HttpRequest.newBuilder(URI.create(url)).timeout(timeout).GET().build();
return client.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)
.handle((resp, err) -> {
Map<String, Object> res = new LinkedHashMap<>();
res.put("ms", Duration.between(inicio, Instant.now()).toMillis());
if (err != null) {
res.put("ok", false);
res.put("error", err.toString());
} else if (resp.statusCode() >= 200 && resp.statusCode() < 300) {
res.put("ok", true);
res.put("status", resp.statusCode());
String cuerpo = resp.body();
res.put("body", esJson(cuerpo) ? new RawJson(cuerpo) : recortar(cuerpo, 400));
} else {
res.put("ok", false);
res.put("status", resp.statusCode());
res.put("error", "HTTP " + resp.statusCode());
}
return new ResultadoApi(nombre, res);
});
}
private static double leerTimeout(URI uri) {
String q = uri.getQuery();
if (q == null) return 4.0;
for (String part : q.split("&")) {
String[] kv = part.split("=");
if (kv.length == 2 && "timeout".equals(kv[0])) {
try { return Double.parseDouble(kv[1]); } catch (NumberFormatException ignored) {}
}
}
return 4.0;
}
private static void responder(HttpExchange ex, int status, String body) throws IOException {
byte[] data = body.getBytes(StandardCharsets.UTF_8);
ex.getResponseHeaders().add("Content-Type", "application/json; charset=utf-8");
ex.sendResponseHeaders(status, data.length);
ex.getResponseBody().write(data);
ex.close();
}
private static String recortar(String body, int max) {
if (body == null) return "";
return body.length() <= max ? body : body.substring(0, max) + "...";
}
private static boolean esJson(String body) {
if (body == null) return false;
String t = body.stripLeading();
return t.startsWith("{") || t.startsWith("[");
}
}
// Serializador JSON simple (sin dependencias externas)
class Json {
static String simple(Map<String, Object> map) {
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, Object> e : map.entrySet()) {
if (!first) sb.append(",");
first = false;
sb.append('"').append(e.getKey()).append('"').append(":");
sb.append(toJsonValue(e.getValue()));
}
sb.append("}");
return sb.toString();
}
@SuppressWarnings("unchecked")
private static String toJsonValue(Object v) {
if (v == null) return "null";
if (v instanceof Number || v instanceof Boolean) return v.toString();
if (v instanceof RawJson) return ((RawJson) v).raw;
if (v instanceof Map) return simple((Map<String, Object>) v);
String s = v.toString()
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
return "\"" + s + "\"";
}
}
// Marca para enviar JSON crudo sin escaparlo
class RawJson {
final String raw;
RawJson(String raw) { this.raw = raw; }
}
La lógica replica el tutorial en Python: sendAsync dispara las llamadas en paralelo, allOf espera todas y se arma un JSON con tiempos parciales. Al ser I/O-bound, los hilos virtuales evitan crear cientos de hilos nativos.
http://localhost:8080/resumen para ver el JSON combinado.http://localhost:8080/resumen?timeout=1 y observa los errores reportados.http://localhost:8080/salud en el navegador.CompletableFuture y allOf: corrutinas lógicas en paralelo con manejo de errores.