Acortar una URL en Java consiste en un POST mediante java.net.http.HttpClient. Construye la petición, establece tu clave de API como cabecera Bearer, envíala y lee short_url del cuerpo de la respuesta. El cliente forma parte del JDK desde Java 11, así que la primera versión funcional no necesita ninguna dependencia.
La mayoría de los resultados sobre Java para esto son "construye un acortador de URL con Spring Boot y JPA", que aborda el problema del almacenamiento, no este. Si eso es lo que buscas, cómo construir un acortador de URL cubre el diseño; si quieres obtener un enlace corto de un servicio existente, empieza por la introducción a la API gratuita de acortamiento de URL para consultar la forma del endpoint y la autenticación.
El mismo recorrido en los otros runtimes: Python, JavaScript, Go y Ruby.
La forma más rápida: una petición mediante un cliente compartido
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public final class Shortener {
private static final HttpClient CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
public static void main(String[] args) throws Exception {
String body = """
{"destination_url":"https://example.com/spring-sale?utm_source=newsletter"}""";
HttpRequest request = HttpRequest
.newBuilder(URI.create("https://api.elido.app/v1/links"))
.header("Authorization", "Bearer " + System.getenv("ELIDO_API_KEY"))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
throw new IllegalStateException("shorten failed: " + response.statusCode());
}
System.out.println(response.body()); // {"id":"...","short_url":"https://s.elido.me/ab12cd"}
}
}
Hay dos tiempos de espera y hacen trabajos distintos. connectTimeout del builder limita cuánto pueden durar el handshake TCP y TLS; timeout de la petición limita todo el intercambio. Si estableces solo el primero, un servidor que acepte la conexión y después no diga nada retendrá el hilo indefinidamente.
La comprobación statusCode() >= 400 no es programación defensiva, sino el contrato. send lanza IOException cuando muere el socket y HttpTimeoutException cuando vence el plazo, pero nunca por un error HTTP. Una clave revocada produce un objeto de respuesta perfectamente normal con un error JSON en el cuerpo.
En código real, analiza el cuerpo con Jackson. Un bloque de texto es una solución honesta para un campo codificado a mano y deja de serlo en cuanto una URL de destino contiene comillas.
Reutiliza un cliente, no uno por llamada
HttpClient es seguro para hilos e inmutable una vez construido, y cada instancia es propietaria de un grupo de conexiones y un ejecutor. Construir uno dentro de un método auxiliar hace que cada llamada repita el handshake TLS y deje un ejecutor para que el recolector de basura lo detecte más tarde. La regla completa es una instancia estática por aplicación.
Reintenta con 429 y 5xx sin crear duplicados
Hay un fallo que merece la pena contemplar. El POST llega, se crea el enlace y la respuesta se pierde durante el camino de vuelta. Tu código ve un tiempo de espera, reintenta y ahora dos enlaces cortos apuntan al mismo destino.
Una Idempotency-Key lo soluciona, y derivar esa clave del destino en lugar de usar un UUID aleatorio hace que volver a ejecutar el mismo lote no tenga coste ni cree duplicados.
static String shorten(String destination, int attempts) throws Exception {
String key = HexFormat.of().formatHex(
MessageDigest.getInstance("SHA-256").digest(destination.getBytes(UTF_8)));
for (int attempt = 0; attempt < attempts; attempt++) {
HttpRequest request = HttpRequest
.newBuilder(URI.create("https://api.elido.app/v1/links"))
.header("Authorization", "Bearer " + System.getenv("ELIDO_API_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.timeout(Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(
Map.of("destination_url", destination))))
.build();
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();
if (status < 400) {
return MAPPER.readTree(response.body()).get("short_url").asText();
}
if (status == 429) {
Thread.sleep(Duration.ofSeconds(response.headers()
.firstValue("Retry-After").map(Long::parseLong).orElse(2L)));
} else if (status >= 500) {
Thread.sleep(Duration.ofSeconds(1L << attempt)); // 1s, 2s, 4s
} else {
throw new IllegalStateException("shorten failed: " + status + " " + response.body());
}
}
throw new IllegalStateException("shorten failed after " + attempts + " attempts");
}
Un 401 o un 422 termina en el primer intento, en lugar de gastar tres en algo que nunca cambiará. Solo merece la pena esperar ante 429 y 5xx, y la rama del 429 respeta el valor del servidor en lugar de adivinarlo. El razonamiento completo está en el análisis detallado de límites de tasa e idempotencia.
¿Quieres ejecutarlo contra un endpoint activo? Crea una clave en el plan gratuito, exporta ELIDO_API_KEY y el código anterior compilará y se ejecutará tal cual en Java 21.
Masivo: hilos virtuales con un límite
Los hilos virtuales llegaron de forma estable en Java 21 y cambian la forma de abordar este problema. La E/S bloqueante en un hilo virtual cuesta casi nada, así que un hilo por URL ya no es una temeridad. Sin embargo, el límite de tasa no se ha movido, por lo que el Semaphore importa más que el ejecutor.
static Map<String, String> shortenAll(List<String> urls) throws Exception {
Map<String, String> out = new ConcurrentHashMap<>();
Semaphore gate = new Semaphore(8); // 8 requests in flight, whatever urls.size() is
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (String url : urls) {
executor.submit(() -> {
gate.acquire();
try {
out.put(url, shorten(url, 3));
} catch (Exception e) {
out.put(url, "ERROR: " + e.getMessage()); // one bad URL, not a dead batch
} finally {
gate.release();
}
return null;
});
}
} // close() waits for every task to finish
return out;
}
El bloque try-with-resources hace un trabajo real: cerrar un ejecutor de hilos virtuales espera a que terminen todas las tareas enviadas, así que no hace falta el baile de awaitTermination. El ConcurrentHashMap indexado por la URL original mantiene visible un fallo parcial y permite repetir la ejecución.
En Java 11 a 17, la misma forma funciona con sendAsync y un grupo de hilos fijo. Lo que no funciona en ninguna versión es usar CompletableFuture.allOf sobre una lista sin límite: lo inicia todo a la vez y acumula una pila de 429.
Cuándo merece la pena un cliente generado
Para un solo endpoint, el código anterior es toda la integración y una dependencia no aporta nada. Eso cambia cuando enumeras enlaces con paginación, filtras por etiqueta y mapeas media docena de formas de respuesta: los modelos generados y un cliente que ya conoce las reglas de reintento se amortizan solos. La página de API y SDK contiene la lista actual, y el inicio rápido del SDK muestra la versión tipada de esta misma llamada.
En cualquier caso, los hábitos son los mismos: un cliente compartido, un plazo en cada petición, leer el código de estado antes del cuerpo y usar una clave de idempotencia estable en todo lo que pueda ejecutarse dos veces. Cuando los enlaces se crean desde un servicio en lugar de un portátil, los webhooks para eventos de enlaces son mejores que consultar periódicamente para saber qué ocurre después.
Lee la serie principal
Este artículo pertenece al clúster de ingeniería. Empieza por la guía de la API gratuita de acortamiento de URL y después consulta límites de tasa e idempotencia. La referencia en vivo es la documentación de la API, y soluciones para desarrolladores cubre el resto de la superficie.
Relacionado en el blog
Preguntas frecuentes
¿Cómo acorto una URL en Java?
Construye un POST con HttpRequest, envíalo mediante un HttpClient compartido y lee short_url del cuerpo de la respuesta. java.net.http forma parte del JDK desde Java 11, así que una versión funcional no necesita ninguna dependencia de Maven: establece la cabecera Authorization, envía un cuerpo JSON pequeño y comprueba statusCode() antes de analizarlo.
¿Necesito OkHttp o Apache HttpClient para llamar a una API REST en Java?
No para esto. El java.net.http.HttpClient integrado cubre los POST JSON, los tiempos de espera y los envíos asíncronos. Un cliente de terceros merece la pena cuando necesitas interceptores, métricas de conexión o gestión de HTTP/2 push que el cliente del JDK no expone, algo que una sola llamada de creación no requiere.
¿El HttpClient de Java lanza una excepción con un 404 o un 500?
No. Solo lanza IOException cuando falla el transporte y HttpTimeoutException cuando vence el tiempo de espera de la petición. Un 4xx o un 5xx vuelve como un HttpResponse normal, así que tienes que leer statusCode() tú mismo. Omitir esa comprobación es la razón habitual por la que una clave revocada parece una llamada correcta.
¿Debería crear un HttpClient nuevo para cada petición?
No. Cada HttpClient lleva su propio grupo de conexiones y ejecutor, así que construir uno por petición desperdicia la reutilización de conexiones y puede acumular hilos que el recolector de basura tendrá que detectar después. Crea una instancia estática para la aplicación y compártela; la clase está documentada como segura para hilos y diseñada para reutilizarse.
¿Cómo acorto muchas URL a la vez en Java?
Ejecuta las llamadas en un ejecutor de hilos virtuales con un Semaphore que limite cuántas están en curso, normalmente unas ocho. Los hilos virtuales hacen que un hilo por URL sea lo bastante barato como para resultar razonable, pero el límite de tasa de la API no ha cambiado, así que el semáforo es lo que realmente mantiene vivo el lote.
¿Cómo construyo el cuerpo JSON sin una biblioteca?
Para un solo campo, basta con un bloque de texto cuyo valor esté escapado. En cuanto el cuerpo tenga cadenas proporcionadas por el usuario o más de dos campos, usa Jackson o una implementación de JSON-B. El JSON construido a mano falla con la primera URL de destino que contenga comillas, y ese fallo parece un error del servidor en lugar de un error de formato.
Prueba Elido
Pega una URL, obtén un enlace corto
Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.
Gratis, sin registro · 2 por día