Скорочення URL у Java - це один POST-запит через java.net.http.HttpClient. Створіть запит, установіть свій API-ключ як Bearer-заголовок, надішліть його та прочитайте short_url з тіла відповіді. Клієнт входить до JDK від Java 11, тому для першої робочої версії взагалі не потрібні залежності.
Більшість результатів пошуку Java для цього завдання пропонують "створити скорочувач URL за допомогою Spring Boot і JPA", але це проблема сховища, а не та, яку ми розглядаємо. Якщо вам потрібно саме це, у матеріалі як створити скорочувач URL описано дизайн; якщо ж ви хочете отримати коротке посилання від наявного сервісу, почніть з огляду безкоштовного API скорочувача URL, щоб розібратися зі структурою кінцевої точки та автентифікацією.
Та сама інструкція для інших середовищ виконання: Python, JavaScript, Go та Ruby.
Найшвидший спосіб: один запит через спільний клієнт
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"}
}
}
Два тайм-аути виконують різні завдання. connectTimeout у білдері обмежує час, протягом якого можуть тривати TCP- і TLS-рукостискання; timeout у запиті обмежує весь обмін. Якщо встановити лише перший, сервер, який прийме з'єднання, а потім замовкне, утримуватиме потік безстроково.
Перевірка statusCode() >= 400 - це не захисне програмування, а контракт. send створює IOException, коли сокет завершує роботу, і HttpTimeoutException, коли спливає дедлайн, але ніколи не створює виняток для помилки HTTP. Відкликаний ключ повертає цілком звичайний об'єкт відповіді з помилкою JSON у тілі.
У робочому коді розбирайте тіло за допомогою Jackson. Текстовий блок доречний для одного жорстко заданого поля, але перестає бути надійним тієї миті, коли URL призначення містить лапки.
Повторно використовуйте один клієнт, а не створюйте його для кожного виклику
HttpClient є безпечним для потоків і незмінним після створення, а кожен екземпляр володіє пулом з'єднань і виконавцем. Створення клієнта всередині допоміжного методу означає, що кожен виклик повторно виконує TLS-рукостискання та залишає виконавець, якого згодом має помітити збирач сміття. Один статичний екземпляр на застосунок - ось і все правило.
Повторні спроби для 429 і 5xx без створення дублікатів
Є одна помилка, яку варто врахувати в дизайні. POST-запит надходить, посилання створюється, але відповідь губиться на зворотному шляху. Ваш код бачить тайм-аут, повторює запит, і тепер два короткі посилання ведуть до одного призначення.
Idempotency-Key це виправляє, а створення цього ключа на основі призначення, а не випадкового UUID, означає, що повторний запуск того самого пакета не створює дублікатів і не потребує додаткових витрат.
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");
}
401 або 422 завершує роботу з першої спроби, а не витрачає три спроби на те, що не зміниться. Чекати варто лише на 429 і 5xx, а гілка 429 враховує число, надане самим сервером, замість здогадок. Логіку всього цього розглянуто в детальному матеріалі про ліміти швидкості та ідемпотентність.
Хочете запустити це проти робочої кінцевої точки? Створіть ключ на безкоштовному плані, експортуйте ELIDO_API_KEY, і наведений вище код скомпілюється та запуститься без змін у Java 21.
Масове скорочення: віртуальні потоки з обмеженням
Віртуальні потоки стали повноцінною можливістю в Java 21 і змінили форму цієї задачі. Блокувальний ввід-вивід у віртуальному потоці майже нічого не коштує, тому один потік на URL більше не є безрозсудним рішенням. Однак ліміт швидкості не змінився, тому Semaphore важливіший за виконавець.
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;
}
Блок try-with-resources виконує справжню роботу: закриття виконавця віртуальних потоків очікує завершення всіх надісланих задач, тому немає потреби в танці з awaitTermination. ConcurrentHashMap із ключами у вигляді початкових URL робить часткову помилку видимою, а запуск - повторюваним.
У Java 11-17 та сама схема працює із sendAsync і фіксованим пулом потоків. У жодній версії не працює CompletableFuture.allOf над необмеженим списком: він запускає все одночасно й накопичує купу відповідей 429.
Коли згенерований клієнт виправдовує себе
Для однієї кінцевої точки наведений вище код є всією інтеграцією, і залежність нічого не дає. Ситуація змінюється, щойно ви починаєте отримувати посилання з пагінацією, фільтрувати їх за тегом і зіставляти пів десятка форм відповідей: згенеровані моделі та клієнт, який уже знає правила повторних спроб, окупають себе. На сторінці API та SDK наведено актуальний список, а в короткому посібнику SDK показано типізовану версію цього самого виклику.
У будь-якому разі правила однакові: один спільний клієнт, дедлайн для кожного запиту, читання коду стану перед тілом і стабільний ключ ідемпотентності для всього, що може запуститися двічі. Коли посилання створюються сервісом, а не ноутбуком, вебхуки для подій посилань кращі за опитування, якщо потрібно дізнатися, що сталося далі.
Читайте основну серію
Цей матеріал належить до інженерного кластера. Почніть із посібника з безкоштовного API скорочувача URL, а потім прочитайте матеріал про ліміти швидкості та ідемпотентність. Актуальний довідник - це документація API, а рішення для розробників охоплюють решту можливостей.
У блозі також
Поширені запитання
Як скоротити URL у Java?
Створіть POST-запит за допомогою HttpRequest, надішліть його через спільний HttpClient і прочитайте short_url з тіла відповіді. java.net.http входить до JDK від Java 11, тому для робочої версії взагалі не потрібна залежність Maven: установіть заголовок Authorization, надішліть невелике тіло JSON і перевірте statusCode() перед розбором.
Чи потрібні мені OkHttp або Apache HttpClient для виклику REST API у Java?
Для цього - ні. Вбудований java.net.http.HttpClient підтримує POST із JSON, тайм-аути та асинхронне надсилання. Сторонній клієнт вартий використання, коли вам потрібні інтерсептори, метрики з'єднань або обробка HTTP/2 push, які JDK-клієнт не надає, але для одного виклику створення це не потрібно.
Чи створює HttpClient у Java виняток для 404 або 500?
Ні. Він створює IOException лише у разі помилки передавання, а HttpTimeoutException - коли спливає тайм-аут запиту. Відповідь 4xx або 5xx повертається як звичайний HttpResponse, тому statusCode() потрібно прочитати самостійно. Пропуск цієї перевірки зазвичай і є причиною, чому відкликаний ключ виглядає як успішний виклик.
Чи слід створювати новий HttpClient для кожного запиту?
Ні. Кожен HttpClient має власний пул з'єднань і виконавець, тому створення окремого екземпляра для кожного запиту позбавляє повторного використання з'єднань і може накопичувати потоки. Створіть один статичний екземпляр для застосунку та використовуйте його спільно: клас документовано як безпечний для потоків і розрахований на повторне використання.
Як скоротити багато URL одночасно в Java?
Запускайте виклики у виконавці віртуальних потоків із Semaphore, який обмежує кількість одночасних запитів, зазвичай приблизно до восьми. Віртуальні потоки роблять схему один потік на URL достатньо дешевою, але ліміт швидкості API не змінився, тому саме semaphore підтримує працездатність пакета.
Як створити тіло JSON без бібліотеки?
Для одного поля підійде текстовий блок із екранованим значенням. Щойно тіло містить рядки, надані користувачем, або більше двох полів, використовуйте Jackson чи реалізацію JSON-B. Створений вручну JSON зламається на першому URL призначення, що містить лапки, і ця помилка виглядатиме як помилка сервера, а не як помилка форматування.
Спробуйте Elido
Вставте URL - отримайте коротке посилання
Без реєстрації. Посилання живе 30 днів. Зареєструйтесь, щоб зберегти назавжди.
Безкоштовно, без реєстрації · 2 на день