6 min de lecturaIngeniería

Cómo acortar una URL en Java con el HttpClient integrado

Acorta una URL en Java con java.net.http y sin dependencias, y añade un tiempo de espera, reintentos con una Idempotency-Key y acortamiento masivo con hilos virtuales.

Marius Voß
DevRel · edge infra
Cómo acortar una URL en Java: un POST de HttpClient que envía una URL de destino a una API de acortamiento y lee el enlace corto del cuerpo de la respuesta

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.

Un POST de Java HttpClient que lleva un token Bearer y una Idempotency-Key al endpoint de enlaces del acortador, la API devuelve HTTP 201 con short_url y una ruta de reintento aplica una espera progresiva ante respuestas 429 y 5xx

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.

sendAsync sin límite con CompletableFuture.allOf comparado con hilos virtuales más un semáforo para el acortamiento masivo de URL en Java, mostrando peticiones limitadas por tasa frente a un número acotado de peticiones en curso

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

Prueba Elido

Acortador de URL alojado en la UE: dominios personalizados, análisis profundo y API abierta. Plan gratuito - sin tarjeta de crédito.

Etiquetas
how to shorten a url in java
java url shortener api
java httpclient post json
shorten url java
java virtual threads http
bulk shorten urls java

Seguir leyendo