JavaRush /Курсы /Java Server /Внешний вызов в ReadLaterA...

Внешний вызов в ReadLaterApplication

Java Server
14 уровень , 4 лекция
Открыта

1. От примера к приложению

Когда мы пишем код в формате «попробовали — получилось», он часто живёт как отдельный кусочек: метод, который делает запрос, и один System.out.println(). Это нормально для обучения, но плоховато для проекта: через два дня вы забудете, как этот метод запускать, и он превратится в музейный экспонат. Сейчас наша цель — сделать так, чтобы вызов жил в ReadLaterApplication и запускался командой.

Если сказать грубо, у нас есть два мира. Первый мир — это «демо‑код», который работает только если вы его вручную вызываете, а иногда ещё и если у вас правильное настроение. Второй мир — это «приложение», которое запускается воспроизводимо через Gradle, получает аргументы командной строки и делает полезное действие. Мы строим второй мир.

Важно, что мы пока не строим “красивую архитектуру на века”. Здесь мы делаем маленький, понятный, запускаемый фрагмент. Если вы сейчас попробуете сразу построить «универсальный HTTP‑клиент, адаптеры, фасады, фабрики, репозитории и шину событий», то единственное, что вы реально добьётесь — это потеряете нить: где же сам HTTP‑вызов, ради которого всё затевалось.

Поэтому критерий успеха здесь простой и очень приземлённый: вы запускаете приложение командой вида catalog search ..., оно делает внешний GET, получает ответ и выводит статус плюс короткий фрагмент JSON, чтобы вы глазами подтвердили: «да, мы реально сходили наружу».

2. Точка входа и класс клиента

Сейчас хочется, чтобы код был одновременно и «в проекте», и «не превращал main() в простыню на 200 строк». Компромисс очень простой: оставляем ReadLaterApplication как дирижёра, который понимает аргументы запуска и решает, какой сценарий выполняем, а сам HTTP‑вызов прячем в один маленький класс, условно CatalogHttpClient. Это не “слои”, не “архитектура”, а просто санитария для мозга.

И тут важно не потерять одну вещь, которая уже стала видна по ответу внешнего сервиса: он не сводится к одной строке. У ответа есть status, headers и body, поэтому даже в самом простом встроенном варианте мы хотя бы фиксируем статус и Content-Type, а само body показываем только как preview. Raw JSON здесь — временное упрощение, чтобы команда приложения стала запускаемой.

Вот как это можно представить:

flowchart LR
    CLI["./gradlew run --args='catalog search clean code'"] --> App["ReadLaterApplication"]
    App --> Client["CatalogHttpClient"]
    Client --> API["Внешний каталог книг"]
    API --> Client
    Client --> App
    App --> Out["Console output: статус + Content-Type + preview JSON"]

Чтобы не запутаться в «кто за что отвечает», полезно держать в голове такую мини‑таблицу (да, она скучная, зато спасает от хаоса):

Часть Что делает Чего не делает
ReadLaterApplication Читает args, выбирает сценарий (catalog search), выводит результат Не должен знать детали HttpRequest.Builder и BodyHandlers
CatalogHttpClient Делает HTTP‑вызов, фиксирует минимум диагностики и возвращает raw JSON Не должен заниматься разбором аргументов командной строки

Обратите внимание на тонкость: мы не пытаемся сейчас идеально распределить ответственность и «спрятать провайдера навсегда». На этом шаге нормально, что URL будет простым и местами даже захардкоженным. Главное — чтобы вызов был встроен и воспроизводим.

3. Пишем CatalogHttpClient: простой HTTP-класс

Когда вы впервые выделяете класс под HTTP‑вызов, мозг начинает нашёптывать: «А давай сделаем универсально! Пусть он умеет GET/POST/PUT, ретраи, таймауты, логирование, метрики и готовит кофе». Это опасный момент. Нам сейчас нужна не “платформа”, а один честный “провод” наружу. Поэтому держим класс максимально простым: внутри один HttpClient, снаружи один метод, который возвращает raw JSON строкой. Да, именно raw: это сознательное временное упрощение для первого рабочего запуска, а не новая норма на весь проект.

Начнём с самого скелета. Важно, что HttpClient лучше создать один раз и переиспользовать (да, даже в учебном примере). Так вы не делаете «нового клиента на каждый запрос», как будто каждый раз покупаете новый телефон ради одного звонка.

import java.net.http.HttpClient;

public class CatalogHttpClient {
    // Один HttpClient на экземпляр клиента: переиспользуем, не создаём на каждый запрос
    private final HttpClient client = HttpClient.newHttpClient();
}

Теперь добавим метод, который принимает URL строкой. Да, строкой. Да, это не идеально. Но сейчас это максимально прозрачно: вы видите, куда именно идёт вызов, и не прячетесь за абстракциями. Для первого шага это плюс.

Собираем HttpRequest

import java.net.URI;
import java.net.http.HttpRequest;

// Собираем GET-запрос к конкретному URL и явно говорим "ожидаю JSON"
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
        .header("Accept", "application/json") // чтобы сервер понимал формат ответа, который нам нужен
        .GET() // явно фиксируем HTTP-метод
        .build();

Здесь полезно проговорить вслух (да, можно шёпотом, никто не осудит): мы создаём запрос, указываем адрес, говорим серверу «я ожидаю JSON» и выбираем метод GET. Именно так ваш код начинает отражать HTTP‑контракт, который вы уже видели в Postman.

Отправляем запрос и берём raw JSON

import java.net.http.HttpResponse;

// Отправляем запрос синхронно (поток будет ждать ответа) и читаем тело как строку
HttpResponse<String> response = client.send(
        request,
        HttpResponse.BodyHandlers.ofString() // превращает body в String (удобно для "сырого" JSON на старте)
);

return response.body();

Мы используем BodyHandlers.ofString(), поэтому response.body() — это String. Это идеально для текущего шага: мы не спорим с JSON, не маппим его в DTO, не делаем “умный парсинг”. Мы просто возвращаем raw факт: «вот тело ответа».

Минимальная диагностика в клиенте

Сейчас нам полезно видеть хотя бы статус и Content-Type. Это тот минимум, который не даёт body() притвориться единственной важной частью ответа. Пока метод всё равно возвращает строку, эту диагностику удобно оставить прямо рядом с вызовом.

int status = response.statusCode();
System.out.println("Catalog HTTP status = " + status); // минимальная диагностика: 200/404/500 и т.д.

String contentType = response.headers()
        .firstValue("Content-Type")
        .orElse("<no Content-Type>");
System.out.println("Catalog Content-Type = " + contentType);

И вот теперь у нас получается цельный метод. Я специально оставлю его в «чуть длиннее 10 строк», потому что иначе вы начнёте прыгать по файлу глазами туда‑сюда. Но обратите внимание: он всё ещё читается линейно и без “магии”.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class CatalogHttpClient {
    // Переиспользуем клиент: так быстрее и проще, чем создавать новый на каждый вызов
    private final HttpClient client = HttpClient.newHttpClient();

    public String getRawJson(String url) throws Exception {
        // Собираем HTTP-запрос (на этом шаге URL приходит строкой — максимально прозрачно)
        HttpRequest request = HttpRequest.newBuilder(URI.create(url))
                .header("Accept", "application/json") // просим JSON
                .GET()
                .build();

        // Делаем реальный внешний вызов и читаем ответ как String
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        // Не теряем главное: статус и тип содержимого — это тоже часть ответа
        System.out.println("Catalog HTTP status = " + response.statusCode()); // Catalog HTTP status = 200
        System.out.println("Catalog Content-Type = " +
                response.headers().firstValue("Content-Type").orElse("<no Content-Type>"));

        // Возвращаем "сырой" JSON: парсинг будет позже, когда он станет реально нужен
        return response.body();
    }
}

throws Exception здесь — осознанная “учебная поблажка”. Мы ещё не строим систему обработки ошибок, нам важно не утонуть в try/catch и не потерять механику запроса.

4. Вызов в ReadLaterApplication: catalog search

Теперь самое главное: наш HTTP‑код должен перестать быть «классом, который существует в проекте, но никто его не зовёт». Идеальная точка для вызова — ветка аргументов запуска. Мы ожидаем запуск вида:

./gradlew run --args="catalog search clean code"

Gradle передаст это в main(String[] args) так, что args[0] = "catalog", args[1] = "search", а дальше будут слова запроса.

Аккуратно распознаём команду

Главная ошибка новичка — сразу писать args[0], не проверив длину массива. Это как открыть холодильник и сразу взять “первую полку”, не убедившись, что холодильник вообще есть (в программировании это обычно заканчивается ArrayIndexOutOfBoundsException).

// Сначала проверяем длину args, и только потом читаем args[0], args[1] и т.д.
boolean isCatalogSearch = args.length >= 3
        && "catalog".equals(args[0])
        && "search".equals(args[1]);

Почему >= 3? Потому что нам нужен хотя бы один токен запроса после catalog search. Иначе получится «ищи что-нибудь… что именно — сам придумай».

Собираем запрос из аргументов

Теперь нам нужно из ["clean","code"] получить строку "clean code". Самый короткий способ — Arrays.copyOfRange(args, 2, args.length) + String.join(" ", ...). Да, звучит как заклинание, но по сути это «взяли хвост массива и склеили через пробел».

import java.util.Arrays;

// Берём все аргументы после "catalog search" и склеиваем в одну строку запроса
String query = String.join(" ", Arrays.copyOfRange(args, 2, args.length));

Теперь маленький хак для первого дня: заменим пробелы на %20. Это не полноценное кодирование, но для начала работает и не уводит нас в отдельную тему.

// Минимально делаем URL "без пробелов", чтобы URI.create(...) не ругался
String encodedQuery = query.replace(" ", "%20");

URL и вызов CatalogHttpClient

На этом шаге базовый адрес каталога можно держать как константу. Даже если он пока фиксирован прямо в коде, вы хотя бы не размножаете строку по всему проекту. Чтобы вызов в приложении оставался тем же самым вызовом, используем тот же поисковый endpoint, который уже отработал на сыром GET.

String baseUrl = "https://openlibrary.org";
String url = baseUrl + "/search.json?q=" + encodedQuery; // на этом шаге достаточно простого склеивания

И теперь — сам вызов. Создаём клиент (один объект), делаем запрос, получаем JSON строкой.

CatalogHttpClient catalogClient = new CatalogHttpClient(); // отдельный класс, чтобы main не превратился в кашу

String rawJson = catalogClient.getRawJson(url);
System.out.println(rawJson);

Если склеить всё это в читаемый кусок внутри main(), получится примерно так:

import com.example.readlater.catalog.CatalogHttpClient;
import java.util.Arrays;

if (isCatalogSearch) {
    String query = String.join(" ", Arrays.copyOfRange(args, 2, args.length));
    String url = "https://openlibrary.org/search.json?q=" + query.replace(" ", "%20");

    CatalogHttpClient catalogClient = new CatalogHttpClient();
    String rawJson = catalogClient.getRawJson(url);

    System.out.println(rawJson);
}

Да, пока URL строится прямо здесь, и да — это ещё не “красота”. Но у этого есть огромный плюс: вы видите весь сценарий целиком и понимаете, что именно запускается, откуда берутся входные данные и куда уходит результат.

5. Дружелюбный вывод: preview JSON

Если внешний сервис вернёт большой JSON, вы быстро поймёте, что “печать всего” — это способ превратить консоль в полотно современного искусства. Иногда полезнее увидеть первые 200 символов, убедиться, что это действительно JSON, и дальше двигаться. Поэтому добавим маленькую функцию preview.

// Утилита для "безопасного" превью: не падаем на null и не заспамливаем консоль гигантским ответом
static String preview(String text, int limit) {
    if (text == null) return "<null>";
    return text.length() <= limit ? text : text.substring(0, limit) + "...";
}

И используем:

String rawJson = catalogClient.getRawJson(url);
System.out.println(preview(rawJson, 200)); // {"numFound":123,"docs":[... (и дальше обрезано)

Если вы хотите сделать вывод чуть более информативным, можно напечатать и сам запрос (URL). Только не надо превращать это в болтливого попугая, который повторяет всё подряд. Одной строки достаточно, чтобы понять, “куда пошли”.

System.out.println("GET " + url); // GET https://openlibrary.org/search.json?q=clean%20code

Так вы получаете очень приятный учебный эффект: запускаете команду, видите URL, видите статус и Content-Type (их печатает CatalogHttpClient), и видите короткий preview тела ответа. Это уже похоже на маленький backend‑инструмент, а не на случайный метод.

6. Проверяем запуск через Gradle

Очень хочется нажать “Run” в IDE и на этом успокоиться. Но наша цель курса — чтобы проект жил воспроизводимо, поэтому запускаем через Gradle. Для этого достаточно команды:

./gradlew run --args="catalog search clean code"

Почему важны кавычки? Потому что --args= получает одну строку, а уже внутри Gradle раскладывает её на отдельные аргументы. Без кавычек часть терминалов или IDE-запусков могут начать трактовать пробелы по-своему, и вы получите не то, что ожидали.

Для отладки можно временно вывести args:

System.out.println(Arrays.toString(args)); // [catalog, search, clean, code]

Когда всё работает, этот вывод лучше убрать, иначе в консоли будет вечный шум. Но как “рентген” на пару минут — отличный инструмент: вы сразу видите, в каком виде ваша команда реально дошла до main().

И ещё одна важная деталь: сейчас вызов ходит в сеть. Если вы случайно указали неправильный домен, если внешний сервис недоступен, или если вы вообще без интернета, приложение может упасть с исключением. Пока это нормально: мы ещё не строим удобную обработку с красивыми сообщениями. Но чтобы студенту было психологически спокойнее, можно обернуть вызов в минимальный try/catch и хотя бы сказать по‑человечески, что произошло.

try {
    String rawJson = catalogClient.getRawJson(url);
    System.out.println(preview(rawJson, 200));
} catch (Exception e) {
    System.out.println("Catalog request failed: " + e.getMessage());
}

Это не “система ошибок”, это просто «не пугай пользователя стек‑трейсами на весь экран». Минимум.

На этом шаге baseline грубый, но честный: URL пока склеивается вручную, пробелы кодируются через replace(" ", "%20"), а наружу выходит raw JSON‑preview. Это нормально. Сейчас важнее другое — команда уже повторяемо проходит весь маршрут: аргументы запуска → внешний GET → статус и Content-Type → короткое превью тела.

7. Типичные ошибки при внешнем вызове

Ошибка №1: весь HTTP‑код остаётся внутри main() и превращает точку входа в кашу.
Это выглядит так: вы вставляете HttpClient.newHttpClient(), HttpRequest.newBuilder(...), send(...) и печать ответа прямо в main(), и через пару правок уже не понимаете, где команда запуска, а где механика HTTP. Лечится очень просто: выделите один маленький класс (CatalogHttpClient) и один метод, который делает вызов.

Ошибка №2: обращение к args[0] и args[1] без проверки длины массива.
Запустили приложение без аргументов — и оно падает. Запустили как catalog search без текста запроса — и опять падает. В main() всегда сначала проверяйте args.length. Это банально, но именно на таких “банальностях” новички чаще всего спотыкаются и начинают думать, что «Gradle сломан», «Java сломалась», «компьютер против меня».

Ошибка №3: пробелы в URL и «почему URI ругается».
URI.create("...q=clean code") — плохая идея. Пробел в URL — это не «просто символ», его нужно кодировать. В этой лекции мы сделали минимальный вариант replace(" ", "%20"), чтобы не уходить в отдельную тему. Если вы забыли даже это, URI может выбросить исключение или запрос просто не будет корректным.

Ошибка №4: печатать весь JSON целиком и удивляться, что консоль стала “очень длинной”.
Большие ответы — это норма для внешних API. Если вы печатаете всё подряд, вы теряете полезную информацию в шуме. Делайте preview на 200–300 символов, а полный JSON выводите только когда реально нужно и вы понимаете зачем. Консоль — не архиватор и не база данных.

Ошибка №5: “я не посмотрел статус, но JSON вроде есть, значит всё хорошо”.
Иногда сервис возвращает JSON и при ошибке, просто это JSON ошибки. Если вы игнорируете statusCode(), вы можете радостно считать ошибку успешным ответом. Даже если вы пока не делаете правильную обработку статусов, хотя бы выводите HTTP 200/404/500, чтобы голова привыкала: body без статуса — это половина картины.

1
Задача
Java Server, 14 уровень, 4 лекция
Недоступна
`ReadLaterApplication` с командой `catalog search-demo`
`ReadLaterApplication` с командой `catalog search-demo`
1
Задача
Java Server, 14 уровень, 4 лекция
Недоступна
`ReadLaterApplication` с командой `catalog search `
`ReadLaterApplication` с командой `catalog search `
1
Опрос
HTTP клиент, 14 уровень, 4 лекция
Недоступен
HTTP клиент
Работа с запросами
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ