В предыдущей статье мы рассмотрели основы парсинга данных в Java.

В этой статье мы пошагово разберём, как с помощью Java 11+ быстро отправлять HTTP GET‑запросы, получать JSON от Binance REST API и извлекать из него символ пары (symbol) и цену (price) — без сторонних зависимостей.

Binance — одна из крупнейших криптобирж мира, публикует публичный REST‑интерфейс /api/v3/ticker/price, который возвращает объект вида:

{ "symbol": "BTCUSDT", "price": "60713.44000000" } 

или массив таких объектов при запросе без параметра symbol. Доступ к этим данным совершенно открытый, не требует API‑ключа или авторизации.

Мы реализуем на Java‑решение с минимальным количеством строк:

  • отправляем HTTP GET‑запрос с помощью встроенного HttpClient (является частью JDK с Java 11);

  • обрабатываем статус код ответа (например, 200 OK);

  • получаем тело в виде строки JSON с помощью BodyHandlers.ofString();

  • парсим JSON (Jackson/Gson) в POJO и извлекаем только поля symbol и price.

Также в статье мы рассмотрим обход блокировки запросов со стороны сервера.

Шаги выполнения HTTP-запроса (кодовая часть)

Создание HttpClient

HttpClient client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_2)
        .connectTimeout(Duration.ofSeconds(10))
        .build();

Подробное объяснение мы оставили в предыдущей нашей статье, касающуюся основ парсинга в Java.

Формирование URL и HttpRequest

URI uri = URI.create("https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT");
HttpRequest req = HttpRequest.newBuilder(uri)
        .timeout(Duration.ofSeconds(5))
        .header("Accept", "application/json")
        .GET()
        .build(); 

Указываем таймаут, Accept, HTTPS

Отправка запроса и обработка статуса

HttpResponse<String> resp = client.send(req, BodyHandlers.ofString());
int code = resp.statusCode(); 
if (code != 200) {
        throw new RuntimeException("BTC price HTTP error: " + code); 
}

Проверяем 2xx.

Создание подкласса для хранения данных

class Ticker {
    private String symbol;
    private String price;

    public String getSymbol() {
        return symbol;
    }

    public void setSymbol(String symbol) {
        this.symbol = symbol;
    }

    public String getPrice() {
        return price;
    }

    public void setPrice(String price) {
        this.price = price;
    }
}

Класс Ticker — это POJO (Plain Old Java Object), который представляет структуру данных, возвращаемых API Binance. В этом случае он соответствует JSON-объекту с полями symbol (тикер, например, BTCUSDT) и price (текущая цена).

Геттеры (get...()) и сеттеры (set...()) — это стандартные методы в Java для доступа к приватным полям класса.

Геттеры (getters) возвращают значение поля и используются для чтения данных из объекта.

Сеттеры (setters) устанавливают значение поля и используются для изменения данных в объекте.

Чтение и десериализация JSON

Десериализацию реализуем по принципу отображения (маппинга) структуры JSON-ответа на Java-объекты с использованием библиотеки Jackson. 

Когда сервер Binance возвращает JSON-массив с данными о тикерах, мы будем автоматически преобразовывать его в массив объектов класса Ticker, где каждое поле JSON symbol, price сопоставляется с соответствующим полем Java-класса через геттеры и сеттеры.

String json = resp.body();
Ticker t = new ObjectMapper().readValue(json, Ticker.class);
// Подходящие поля: 
// public class Ticker { public String symbol; public String price; } 
BigDecimal price = new BigDecimal(t.price);

(или массив, если symbol не указан).

Обработка списка:

если не указан параметр symbol, JAckson вернёт List<Ticker>, далее .forEach(t -> …).

Вариант асинхронного выполнения (неблокирующий)

Для того чтобы автоматически преобразовывать JSON-ответ от Binance API в объекты Ticker, подключите библиотеку Jackson по примеру из предыдущей статьи, посвящённой основам парсинга и созданию парсинга Яндекс Карт.

После подключения зависимостей переходим непосредственно к коду

CompletableFuture<List<Ticker>> f = client.sendAsync(req, BodyHandlers.ofString())
        .thenApply(HttpResponse::body)
        .thenApply(body -> Arrays.asList(new ObjectMapper().readValue(body, Ticker[].class))); 
f.thenAccept(list -> list.forEach(System.out::println));

Этот код запускает HTTP GET‑запрос асинхронно благодаря sendAsync(...), что сразу возвращает CompletableFuture<HttpResponse<String>> ещё до завершения HTTP‑операции — основной поток не блокируется, и программа продолжает выполняться.

Сначала sendAsync инициирует запрос и сразу же возвращает будущее, не дожидаясь окончания I/O. 

Через .thenApply(HttpResponse::body) мы. новообразованное CompletableFuture<HttpResponse<String>> преобразуем в CompletableFuture<String> — автоматически извлекаем строковое тело HTTP‑ответа (JSON‑строку). Это происходит, когда тело готово, верхние части ответа уже получены (статус/заголовки), и BodyHandlers.ofString() упаковала тело в String объект.

Следующим .thenApply(...) мы преобразуем JSON‑строку в массив Ticker[], используя Jackson ObjectMapper.readValue(body, Ticker[].class), затем оборачиваем в List<Ticker> — получаем CompletableFuture<List<Ticker>>. Такой подход построен на принципах реактивного программирования: каждая ступень цепочки запускается автоматически, когда предыдущая завершается.

Работа с результатом производится через .thenAccept(list -> ...): как только CompletableFuture<List<Ticker>> завершится успешно, JVM выполнит переданный Consumer<List<Ticker>>, в котором мы просто выводим каждую пару (symbol + price) через println.

Если в процессе произойдёт исключение (например, невалидный JSON, I/O ошибка, timeout), оно будет проброшено внутрь CompletableFuture, и его можно обработать через .exceptionally(...) на следующем этапе цепочки.

Итоговый код должен выглядеть следующим образом:

package org.example;

import java.net.URI;
import java.net.http.*;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.*;
import java.util.concurrent.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;

class Ticker {
    private String symbol;
    private String price;

    public String getSymbol() {
        return symbol;
    }

    public void setSymbol(String symbol) {
        this.symbol = symbol;
    }

    public String getPrice() {
        return price;
    }

    public void setPrice(String price) {
        this.price = price;
    }
}

public class GetRequestExample {

    public static void main(String[] args) {
        HttpClient client = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.binance.com/api/v3/ticker/price"))
                .header("Accept", "application/json")
                .GET()
                .build();

        CompletableFuture<List<Ticker>> future = client.sendAsync(request, BodyHandlers.ofString())
                .thenApply(HttpResponse::body)
                .thenApply(json -> {
                    try {
                        return Arrays.asList(new ObjectMapper()
                                .readValue(json, Ticker[].class));
                    } catch (IOException e) {
                        throw new CompletionException(e);
                    }
                });

        future.thenAccept(tickers -> tickers.forEach(t ->
                System.out.println(t.getSymbol() + " → " + t.getPrice())
        )).exceptionally(err -> {
            System.err.println("Ошибка: " + err.getCause());
            return null;
        });


        future.join();
    }
}

Обход блокировок запросов

Binance может блокировать запросы при парсинге данных, особенно если они отправляются слишком часто или без соблюдения правил API. Вот основные причины блокировки и способы их избежать:

Во-первых, Binance устанавливает строгие лимиты на количество запросов. Например, при превышении лимита запросов система может временно заблокировать IP-адрес с рекомендацией использовать WebSocket для получения данных в реальном времени вместо частых HTTP-запросов.

Во-вторых, ошибки могут возникать, если запросы отправляются с некорректными параметрами, например, с неправильной временной меткой или подписью. Это также может привести к временной блокировке.

Кроме того, Binance активно борется с автоматизированным парсингом, особенно если он имитирует поведение пользователя без API-ключа. Например, попытки парсинга через прямое обращение к веб-страницам могут блокироваться, в то время как официальный API (api.binance.com) остаётся доступным только для авторизованных запросов.

Для того, чтобы избежать блокировки со стороны сервера, мы будем использовать WebSocket. Он устанавливает постоянное соединение с сервером, что обеспечивает один потоковый канал, через который сервер шлёт данные, вместо постоянных HTTP-запросов.

Для начала работы с WebSocket подключаем библиотеку WebSocket в зависимостях pom.xml

<dependency>
    <groupId>org.java-websocket</groupId>
    <artifactId>Java-WebSocket</artifactId>
    <version>1.5.3</version>
</dependency>

Далее переходим к созданию URI для подключения

URI uri = new URI("wss://stream.binance.com:9443/ws");

В структуре нашего URI теперь мы используем протокол "wss://...". Он является протоколом WebSocket Secure (аналог HTTPS для WS), необходимым для создания канала связи между клиентом и сервером.

Также мы используем специальный эндпоинт Binance для WebSocket-стримов — stream.binance.com:9443

Важно: Binance использует именно порт 9443 для защищённого соединения.

/ws — базовый путь для подключения.

На этом этапе вы также можете столкнуться с блокировкой подключений из определенных регионов или при нарушении условий использования. Для обхода этого следует использовать следующие URI:

URI uri = new URI("wss://stream.binance.us:9443/ws"); // для США
URI uri = new URI("wss://stream.binance.me:9443/ws"); // для Европы

После этого непосредственно переходим к созданию WebSocketClient

WebSocketClient ws = new WebSocketClient(uri){


}

Создаём анонимный класс, наследующий WebSocketClient. В нём нам нужно переопределить 4 ключевых метода для обработки событий соединения:

Метод onOpen — при успешном подключении

@Override public void onOpen(ServerHandshake h) {
    System.out.println("WS Open");
    subscribeTicker("btcusdt");
}

Он срабатывает при установке соединения. Здесь ServerHandshake — содержит данные, необходимые для соединения между сервером и клиентом (заголовки и т. д.). subscribeTicker("btcusdt") — пример метода для подписки на тикеры (реализация ниже). Это способ получать данные в реальном времени без REST-запросов, которые могут блокироваться сервером.

Метод onMessage — обработка входных данных

@Override public void onMessage(String msg) {
    handleMessage(msg);
}

Он вызывается при получении нового сообщения от сервера. В нём мы задаём принимаемый параметр msg — строку в формате JSON. И прописываем пользовательский метод для парсинга handleMessage(msg).

Метод onClose — при разрыве соединения

@Override public void onClose(int code, String reason, boolean remote) {
    System.err.println("Closed: "+code+" reason="+reason);
    scheduleReconnect();
}

В нём установленными параметрами являются code — 2хх код закрытия, reason — текстовое пояснение 2хх кода, remote — true, если разрыв инициирован сервером и scheduleReconnect() — метод для переподключения, он реализуется отдельно.

Метод onError — обработка ошибок

@Override public void onError(Exception ex) {
    ex.printStackTrace();
}

Он вызывается при ошибках соединения. Exception может быть SocketTimeoutException, SSLException и др.

После этого реализуем подписку на нужные потоки (метод subscribeTicker)

private static String subscribeJson(String symbol) {
    ObjectNode n = mapper.createObjectNode();
    n.put("method", "SUBSCRIBE");
    n.putArray("params").add(symbol.toLowerCase() + "@miniTicker");
    n.put("id", 1);
    n.put("returnRateLimits", true);
    return n.toString();
}

Метод subscribeTicker принимает параметр symbol (например "BTCUSDT"). После чего он создает JSON-сообщение для подписки на WebSocket-канал и отправляет сообщение через установленное соединение ws.

После чего формируется JSON-запрос на сервер с указанием метода SUBSCRIBE и указанием массива подписанных каналов %s@miniTicker с идентификатором запросов.

Следующим шагом реализуем обработку сообщений

Здесь мы будем обрабатывать массив JSON и выводить данные формата:

  • тип события (например, "24hrMiniTicker")

  • символ торговой пары (например "BTCUSDT")

  • Проверять наличие цены в поле c (последняя цена) или p (цена сделки)

  • Выводить информацию в формате: BTCUSDT – 50000.00

private void handleMessage(String msg) {
    JsonNode node = mapper.readTree(msg);
    if (node.has("result") || node.has("id")) {

    } else if (node.has("e")) {
        String symbol = node.get("s").asText();
        String price = node.has("c") ? node.get("c").asText() : node.get("p").asText();
        System.out.println(symbol + " → " + price);
    } else if (node.has("rateLimits")) {
        rateTracker.update(node.get("rateLimits"));
    }
}

e – представляет собой тип события, s – представляет символ торговой пары

Поле rateLimits возвращается автоматически по умолчанию — можно отслеживать текущее использование лимитов.

Обход контроля управляющих сообщений и лимита

Любые команды: PING/PONG, SUBSCRIBE/UNSUBSCRIBE считаются одним входящим сообщением на сервер.

Если вы отправляете более 5 таких команд в секунду — сервер отключит соединение и может банить IP. Для обхода блокировки реализуем отложенную очередь:

Первым делом инициируем процесс переподключения. Для этого вызываем backoff.next() для получения следующего значения задержки и передаём это значение в scheduleReconnectDelay()

private static void scheduleReconnect() {
    scheduleReconnectDelay(backoff.next());
}

После этого реализуем метод для попытки переподключения через указанное время. Он будет выводить сообщение о времени переподключения, после создавать однопоточный планировщик ScheduledExecutorService и планировать выполнение через delaySec секунд. Логика переподключения включает в себя сброс счётчика попыток backoff.reset() и вызова повторного подключения ws.reconnect().

private static void scheduleReconnectDelay(int delaySec) {
    System.err.println("Reconnect in " + delaySec + "s");
    ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
    exec.schedule(() -> {
        backoff.reset();
        ws.reconnect();
    }, delaySec, TimeUnit.SECONDS);
}

Далее реализуем реконнект при ошибках

WebSocket может автоматически закрываться через 24 часа или же получить ошибку со стороны сервера. Для автоматизации реконектинга реализуем следующий класс RateTracker.

Для этого первым делом задаём поля нашему классу.

private int current = 0, limit = Integer.MAX_VALUE;

current — текущее количество использованных "единиц веса" запросов

limit — максимально допустимый лимит, по умолчанию Integer.MAX_VALUE

Далее создадим метод update(JsonNode arr)

В нём будут обновляться текущие значения лимитов на основе данных от Binance.

Логика работы данного метода будет заключаться в следующем:

  1. Перебираем все элементы массива arr (полученные от Binance)

  2. Ищем элементы, в которых будем возвращать значения: REQUEST_WEIGHT (ограничение на вес запросов), MINUTE (лимит в минуту)

  3. Для найденных элементов обновляем current значением count (текущее использование), обновляем поле limit значением limit (максимальный лимит веса), выводим информацию в консоль

Код должен выглядеть следующим образом:

void update(JsonNode arr) {
        arr.forEach(elem -> {
            if ("REQUEST_WEIGHT".equals(elem.get("rateLimitType").asText()) &&
                    "MINUTE".equals(elem.get("interval").asText())) {
                current = elem.get("count").asInt();
                limit = elem.get("limit").asInt();
                System.out.println("Used weight/min: " + current + "/" + limit);
            }
        });
    }
}

После этого, перед закрытием класса, реализуем ещё один метод, который будет проверять, не превысит ли указанный "вес" запроса текущий лимит.

boolean wouldExceed(int cost) {
    return current + cost > limit;
}

Фактически программа завершена, однако, для лучшей работы, реализуем ещё один класс ExponentialBackoff

В этом классе реализуем алгоритм экспоненциальной задержки для постепенного увеличения интервалов между повторными попытками подключения. Таким образом, мы предотвратим возможную перегрузку сервера.

Класс будет работать по логике:

Создадим поле attempt, в котором будут приниматься счётчик неудачных попыток подключения и в котором значения будут инициализироваться нулём при создании объекта.

private int attempt = 0;

Далее реализуем метод next(), который постепенно будет увеличивать счётчик подключений. В нём мы будем вычислять задержку по формуле экспоненциального роста. Для того, чтобы не прервать работу соединения, ограничим максимальную задержку 60 секундами и вернём время задержки в секундах.

int next() {
    attempt++;
    return Math.min(60, (int)Math.pow(2, attempt));
}

После этого реализуем метод reset(), который будет обнулять счётчик попыток и вызываться после успешного подключения:

void reset() { attempt = 0; }

Итоговый код с более развёрнутым классом создания соединения должен выглядеть следующим образом:

package org.example;

import java.net.URI;
import java.util.concurrent.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;

public class Binance {
    private static final ObjectMapper mapper = new ObjectMapper();
    private static final ScheduledExecutorService controlScheduler = Executors.newSingleThreadScheduledExecutor();
    private static final BlockingQueue<String> controlQueue = new LinkedBlockingQueue<>();
    private static WebSocketClient ws;
    private static RateTracker rateTracker = new RateTracker();
    private static ExponentialBackoff backoff = new ExponentialBackoff();

    public static void main(String[] args) throws Exception {
        URI uri = new URI("wss://stream.binance.us:9443/ws");
        ws = new WebSocketClient(uri) {
            @Override public void onOpen(ServerHandshake h) {
                System.out.println("WS Open");
                enqueue(subscribeJson("BTCUSDT"));
            }
            @Override public void onMessage(String msg) {
                handleMessage(msg);
            }
            @Override public void onClose(int code, String reason, boolean remote) {
                System.err.println("Closed: " + code + " reason=" + reason);
                scheduleReconnect();
            }
            @Override public void onError(Exception ex) {
                ex.printStackTrace();
            }
        };
        ws.connect();
        controlScheduler.scheduleAtFixedRate(() -> {
            String cmd = controlQueue.poll();
            if (cmd != null) ws.send(cmd);
        }, 0, 200, TimeUnit.MILLISECONDS);
    }

    private static String subscribeJson(String symbol) {
        ObjectNode n = mapper.createObjectNode();
        n.put("method", "SUBSCRIBE");
        n.putArray("params").add(symbol.toLowerCase() + "@miniTicker");
        n.put("id", 1);
        n.put("returnRateLimits", true);
        return n.toString();
    }

    private static void enqueue(String json) {
        controlQueue.offer(json);
    }

    private static void handleMessage(String msg) {
        try {
            JsonNode n = mapper.readTree(msg);
            if (n.has("rateLimits")) {
                rateTracker.update(n.get("rateLimits"));
            } else if (n.has("e")) {
                String symbol = n.get("s").asText();
                String price = n.has("c") ? n.get("c").asText() : n.get("p").asText();
                System.out.println(symbol + " → " + price);
            } else if (n.has("status") && n.get("status").asInt() == 429) {
                int ra = n.has("retryAfter") ? n.get("retryAfter").asInt() : 1;
                System.err.println("Got 429, retrying after " + ra + "s");
                pauseAndReconnect(ra);
            } else if (n.has("status") && n.get("status").asInt() == 418) {
                long ts = n.get("retryAfter").asLong();
                System.err.println("IP banned until " + ts);
                pauseAndReconnect((int)((ts - System.currentTimeMillis())/1000));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void pauseAndReconnect(int seconds) {
        controlQueue.clear();
        scheduleReconnectDelay(seconds);
    }

    private static void scheduleReconnect() {
        scheduleReconnectDelay(backoff.next());
    }

    private static void scheduleReconnectDelay(int delaySec) {
        System.err.println("Reconnect in " + delaySec + "s");
        ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
        exec.schedule(() -> {
            backoff.reset();
            ws.reconnect();
        }, delaySec, TimeUnit.SECONDS);
    }

    static class RateTracker {
        private int current = 0, limit = Integer.MAX_VALUE;
        void update(JsonNode arr) {
            arr.forEach(elem -> {
                if ("REQUEST_WEIGHT".equals(elem.get("rateLimitType").asText()) &&
                        "MINUTE".equals(elem.get("interval").asText())) {
                    current = elem.get("count").asInt();
                    limit = elem.get("limit").asInt();
                    System.out.println("Used weight/min: " + current + "/" + limit);
                }
            });
        }
        boolean wouldExceed(int cost) {
            return current + cost > limit;
        }
    }

    static class ExponentialBackoff {
        private int attempt = 0;
        int next() {
            attempt++;
            return Math.min(60, (int)Math.pow(2, attempt));
        }
        void reset() { attempt = 0; }
    }
}

Развернём проект на сервере

Создадим новый проект в облаке Amvera.

Это нам даст ротируемые IP и возможность создания несколько инстансов/проектов что, поможет избежать бана за количество запросов. Мы сможем обновлять код через git push, получим встроенный мониторинг с логированием и почти не будем думать о настройке сервера.

Для этого предварительно зарегистрируемся по ссылке. Стартового баланса хватит на эксперименты и первые недели работы проекта.

Находясь на главной странице, нажимаем «Создать».

Вводим название проекта и выбираем тариф «Начальный Плюс». Далле можно уменьшить тариф, но для запуска лучше повысить производительность.

Нажимаем «Далее» и переходим к загрузке данных проекта.
Нажимаем «Далее» и переходим к загрузке данных проекта.

Существует два варианта загрузить проект: командами git или через интерфейс сервиса. Мы воспользуемся Git, это упрощает доставку обновлений. Но можно использовать и загрузку/удаление файлов через личный кабинет. Тем более можно комбинировать способы.

Перед следующими шагами нам требуется собрать проект в нашем локальном репозитории, чтобы получить JAR файл, который потребуется нам далее.

Для этого пропишем:

Mvn package

Учтите, что для этого требуется ранее предустановленный Maven.

Нужный нам JAR появится в корне папки /target. Его потребуется вписать далее в конфигурационный файл amvera.yaml

Нам предлагается склонировать репозиторий созданного проекта. Но так как приложение уже создано, мы пойдем иным путем.

1. Вызовем терминал в IDE, где открыто приложение, или откроем папку проекта в терминале.

2. Инициализируем локальный гит-репозиторий командой.

git init

3. Добавим удаленный репозиторий нашего проекта (url вашего репозитория будет отличаться. Во избежание синтаксических ошибок скопируйте ссылку на втором шаге создания проекта).

git remote add amvera https://git.amvera.ru/имя_пользователя/имя_проекта

4. Добавим файлы и сделаем первый коммит

git add .

git commit -m "init"

5. Запушим наш код в репозиторий проекта

git push amvera master

6. Нажимаем «Далее» и наблюдаем интерфейс создания конфигурационного файла.

Среди всех полей нам требуется вписать в jarName имя нашего jar-файла, в нашем случае это PARSEBINANCE-1.0-SNAPSHOT.jar , но в вашем случае это будут совершенно другие названия.

Пример:

Далее начнётся сборка проекта. После запуск самого приложения.

Следить за ходом сборки и запуска можно в соответствующих логах.

Получение данных

Когда проект успешно соберется и запустится мы сможем наблюдать за ходом работы в логах приложения.

В нашем случае мы разработали простой пример парсера, у нас данные приходят в логи приложения (аналог консоли).

Заключение

В процессе создания мы реализовали минималистичный Java-клиент, который принимает на вход символ(ы) ("BTCUSDT", "ETHUSDT"…), делает HTTP-запрос, возвращает текущую цену; понимание ограничений Binance (rate‑limit, BPA) и как правильнее всего построить приложение, не нарушая API. И шаблон, который легко расширить — добавить WebSocket‑стрим цен, интеграцию с базой, график на JavaFX или экспорт в CSV.

Код проекта вы можете найти по ссылке на GitHub.

Комментарии (2)


  1. MarkovM
    24.08.2025 12:35

    А лимит запросов к API Binance без авторизации и авторизацией сильно отличается?


  1. Taumer
    24.08.2025 12:35

    Для начала работы с WebSocket подключаем библиотеку WebSocket в зависимостях pom.xml

    А зачем? Этот же самый стандартный HttpClient умеет работать с веб-сокетами, причем в том же самом асинхронном стиле.
    https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.html