Encurtar uma URL em Java é fazer um POST usando java.net.http.HttpClient. Monte a requisição, defina sua chave de API como um cabeçalho Bearer, envie-a e leia short_url no corpo da resposta. O cliente faz parte do JDK desde o Java 11, então a primeira versão funcional não precisa de nenhuma dependência.
A maioria dos resultados sobre Java para esse assunto fala em "criar um encurtador de URL com Spring Boot e JPA", que é o problema de armazenamento, não este. Se é isso que você quer, como criar um encurtador de URL cobre o design; se quer um link curto a partir de um serviço existente, comece pela visão geral da API gratuita de encurtamento de URLs para entender o formato do endpoint e a autenticação.
O mesmo passo a passo em outras linguagens: Python, JavaScript, Go e Ruby.
A maneira mais rápida: uma requisição por um cliente compartilhado
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"}
}
}
Dois timeouts, com funções diferentes. connectTimeout no builder limita quanto tempo o handshake TCP e TLS pode levar; timeout na requisição limita a troca inteira. Definir apenas o primeiro permite que um servidor que aceita sua conexão e depois não responde mantenha a thread ocupada indefinidamente.
A verificação statusCode() >= 400 não é programação defensiva, é o contrato. send lança IOException quando o socket cai e HttpTimeoutException quando o prazo termina, mas nunca por causa de um erro HTTP. Uma chave revogada produz um objeto de resposta perfeitamente normal, com um erro JSON no corpo.
Faça o parsing do corpo com Jackson em código real. Um text block é adequado para um campo codificado diretamente e deixa de ser assim no momento em que uma URL de destino contém aspas.
Reutilize um cliente, não um por chamada
HttpClient é seguro para threads e imutável depois de criado, e cada instância é dona de um pool de conexões e de um executor. Criá-lo dentro de um método auxiliar faz cada chamada repetir o handshake TLS e deixar um executor para o coletor de lixo encontrar mais tarde. Uma instância estática por aplicação é toda a regra necessária.
Faça retry para 429 e 5xx sem criar duplicatas
Há uma falha que vale a pena considerar no design. O POST chega, o link é criado, mas a resposta se perde no caminho de volta. Seu código vê um timeout, faz retry e agora dois links curtos apontam para o mesmo destino.
Uma Idempotency-Key resolve isso, e derivar essa chave do destino em vez de usar um UUID aleatório faz com que uma nova execução do mesmo lote seja gratuita, sem criar duplicatas.
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");
}
Um 401 ou 422 sai na primeira tentativa, em vez de gastar três tentativas com algo que nunca vai mudar. Só vale a pena esperar por 429 e 5xx, e o ramo de 429 respeita o valor enviado pelo servidor em vez de adivinhar. O raciocínio por trás de tudo isso está no aprofundamento sobre limites de taxa e idempotência.
Quer executar isso contra um endpoint ativo? Crie uma chave no plano gratuito, exporte ELIDO_API_KEY e o código acima será compilado e executado como está no Java 21.
Em massa: threads virtuais com um limite
Threads virtuais chegaram definitivamente no Java 21 e mudam o formato desse problema. E/S bloqueante em uma thread virtual custa quase nada, então uma thread por URL já não é uma imprudência. O limite de taxa, porém, não mudou, e é por isso que o Semaphore importa mais que o executor.
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;
}
O bloco try-with-resources está fazendo um trabalho real: fechar um executor de threads virtuais espera todas as tarefas enviadas, então não há uma sequência de awaitTermination. O ConcurrentHashMap indexado pela URL original mantém visível uma falha parcial e torna a execução repetível.
No Java 11 a 17, o mesmo formato funciona com sendAsync e um pool fixo de threads. O que não funciona em nenhuma versão é usar CompletableFuture.allOf sobre uma lista sem limite: ele inicia tudo de uma vez e acumula uma pilha de 429.
Quando um cliente gerado faz sentido
Para um endpoint, o código acima é a integração inteira e uma dependência não traz nada. Isso muda quando você passa a listar links com paginação, filtrar por tag e mapear meia dúzia de formatos de resposta: modelos gerados e um cliente que já conhece as regras de retry se pagam. A página API e SDKs tem a lista atual, e o quickstart do SDK mostra a versão tipada dessa mesma chamada.
De qualquer forma, os hábitos são os mesmos: um cliente compartilhado, um prazo em toda requisição, o código de status lido antes do corpo e uma chave de idempotência estável para qualquer coisa que possa ser executada duas vezes. Quando os links são criados por um serviço em vez de um laptop, webhooks para eventos de links são melhores que polling para descobrir o que aconteceu depois.
Leia a série principal
Este artigo faz parte do cluster de engenharia. Comece pelo guia da API gratuita de encurtamento de URLs e depois leia limites de taxa e idempotência. A referência ativa está na documentação da API, e soluções para desenvolvedores cobre o restante da plataforma.
Relacionados no blog
Perguntas frequentes
Como encurto uma URL em Java?
Monte um POST com HttpRequest, envie-o por meio de um HttpClient compartilhado e leia short_url no corpo da resposta. java.net.http faz parte do JDK desde o Java 11, então uma versão funcional não precisa de nenhuma dependência do Maven: defina o cabeçalho Authorization, envie um corpo JSON pequeno e verifique statusCode() antes de fazer o parsing.
Preciso do OkHttp ou do Apache HttpClient para chamar uma API REST em Java?
Não para isso. O java.net.http.HttpClient integrado cobre um POST JSON, timeouts e envios assíncronos. Um cliente de terceiros vale a pena quando você precisa de interceptors, métricas de conexão ou tratamento de push HTTP/2 que o cliente do JDK não expõe, algo que uma única chamada de criação não exige.
O HttpClient do Java lança uma exceção para um 404 ou 500?
Não. Ele só lança IOException em uma falha de transporte e HttpTimeoutException quando o timeout da requisição é atingido. Um 4xx ou 5xx volta como um HttpResponse normal, então você precisa ler statusCode() por conta própria. Ignorar essa verificação é o motivo mais comum para uma chave revogada parecer uma chamada bem-sucedida.
Devo criar um novo HttpClient para cada requisição?
Não. Cada HttpClient tem seu próprio pool de conexões e executor, então criar um por requisição desperdiça o reuso de conexões e pode acumular threads. Crie uma única instância estática para a aplicação e compartilhe-a; a classe é documentada como segura para threads e projetada para reuso.
Como encurto muitas URLs de uma vez em Java?
Execute as chamadas em um executor de threads virtuais com um Semaphore limitando quantas ficam em andamento, geralmente cerca de oito. Threads virtuais tornam barato o modelo de uma thread por URL, mas o limite de taxa da API não mudou, então o semáforo é o que efetivamente mantém o lote funcionando.
Como monto o corpo JSON sem uma biblioteca?
Para um único campo, um text block com o valor escapado é suficiente. Quando o corpo tiver strings fornecidas pelo usuário ou mais de dois campos, use Jackson ou uma implementação de JSON-B. JSON montado manualmente quebra na primeira URL de destino que contém aspas, e essa falha parece um erro do servidor, não um problema de formatação.
Experimente Elido
Cole uma URL, obtenha um link curto
Sem cadastro. O link vive 30 dias. Cadastre-se para mantê-lo para sempre.
Grátis, sem necessidade de registo · 2 por dia