1. Обзор
Обычно при работе с HTTP-вызовами в наших веб-приложениях нам нужен способ сбора каких-либо показателей о запросах и ответах. Обычно это делается для отслеживания размера и частоты HTTP-вызовов, которые делает наше приложение.
OkHttp — это эффективный клиент HTTP и HTTP/2 для приложений Android и Java. В предыдущем уроке мы рассмотрели основы работы с OkHttp.
В этом руководстве мы узнаем все о том, как мы можем собирать эти типы метрик с помощью событий.
2. События
Как следует из названия, события предоставляют нам мощный механизм для записи метрик приложения, относящихся ко всему жизненному циклу HTTP-вызова.
Чтобы подписаться на все интересующие нас события, нам нужно определить EventListener
и переопределить методы для событий, которые мы хотим захватить.
Это особенно полезно, если, например, мы хотим отслеживать только неудачные и успешные вызовы. В этом случае мы просто переопределяем определенные методы, соответствующие этим событиям в нашем классе прослушивателя событий. Мы увидим это более подробно позже.
Использование событий в наших приложениях имеет по крайней мере несколько преимуществ:
- Мы можем использовать события для отслеживания размера и частоты HTTP-вызовов, которые делает наше приложение.
- Это может помочь нам быстро определить, где у нас может быть узкое место в нашем приложении.
Наконец, мы также можем использовать события, чтобы определить, есть ли у нас основная проблема с нашей сетью.
3. Зависимости
Конечно, нам нужно добавить стандартную зависимость okhttp
в наш pom.xml
:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.1</version>
</dependency>
Нам также понадобится еще одна зависимость специально для наших тестов. Добавим артефакт mockwebserver OkHttp
:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>4.9.1</version>
<scope>test</scope>
</dependency>
Теперь, когда у нас настроены все необходимые зависимости, мы можем приступить к написанию нашего первого прослушивателя событий.
4. Методы и порядок событий
Но прежде чем мы начнем определять наш собственный прослушиватель событий, мы сделаем шаг назад и кратко рассмотрим, какие методы обработки событий нам доступны, а также порядок, в котором мы можем ожидать прибытия событий . Это поможет нам позже, когда мы погрузимся в некоторые реальные примеры.
Предположим, мы имеем дело с успешным HTTP-вызовом без перенаправлений или повторных попыток. Тогда мы можем ожидать этот типичный поток вызовов методов.
4.1. callStart()
Этот метод является нашей точкой входа, и мы будем вызывать его, как только поставим вызов в очередь или наш клиент выполнит его.
4.2. proxySelectStart()
и proxySelectEnd()
Первый метод вызывается до выбора прокси, а также после выбора прокси, включая списки прокси в том порядке, в котором они будут предприняты. Этот список, конечно, может быть пустым, если прокси не настроен.
4.3. dnsStart()
и dnsEnd()
Эти методы вызываются непосредственно перед поиском DNS и сразу после разрешения DNS.
4.4. connectStart()
и connectEnd()
Эти методы вызываются до установления и закрытия соединения через сокет.
4.5. SecureConnectStart()
и SecureConnectEnd()
Если наш вызов использует HTTPS, то в промежутках между connectStart
и connectEnd
у нас будут эти варианты безопасного подключения.
4.6. ConnectionAcquired()
и connectionReleased()
Вызывается после установления или разрыва соединения.
4.7. requestHeadersStart()
и requestHeadersEnd()
Эти методы будут вызываться непосредственно до и после отправки заголовков запроса.
4.8. requestBodyStart()
и requestBodyEnd()
Как следует из названия, вызывается перед отправкой тела запроса. Конечно, это применимо только к запросам, содержащим тело.
4.9. ответЗаголовкиСтарт()
и ответЗаголовкиКонец()
Эти методы вызываются, когда заголовки ответов впервые возвращаются с сервера и сразу после их получения.
4.10. ответBodyStart()
и ответBodyEnd()
Аналогичным образом вызывается при первом возврате тела ответа с сервера и сразу после его получения.
В дополнение к этим методам у нас также есть три дополнительных метода, которые мы можем использовать для регистрации сбоев:
4.11. callFailed()
, responseFailed()
и requestFailed()
Если наш вызов постоянно терпит неудачу, запрос имеет ошибку записи или ответ имеет ошибку чтения.
5. Определение простого прослушивателя событий
Давайте начнем с определения нашего собственного четного слушателя. Чтобы все было действительно просто, наш прослушиватель событий будет регистрировать начало и конец вызова вместе с некоторой информацией заголовка запроса и ответа :
public class SimpleLogEventsListener extends EventListener {
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleLogEventsListener.class);
@Override
public void callStart(Call call) {
LOGGER.info("callStart at {}", LocalDateTime.now());
}
@Override
public void requestHeadersEnd(Call call, Request request) {
LOGGER.info("requestHeadersEnd at {} with headers {}", LocalDateTime.now(), request.headers());
}
@Override
public void responseHeadersEnd(Call call, Response response) {
LOGGER.info("responseHeadersEnd at {} with headers {}", LocalDateTime.now(), response.headers());
}
@Override
public void callEnd(Call call) {
LOGGER.info("callEnd at {}", LocalDateTime.now());
}
}
Как мы видим, для создания нашего слушателя все, что нам нужно сделать, это расширить класс EventListener
. Затем мы можем пойти дальше и переопределить методы для нужных нам событий.
В нашем простом слушателе мы регистрируем время начала и окончания вызова вместе с заголовками запроса и ответа, когда они приходят.
5.1. Соединяем это вместе
Чтобы действительно использовать этот прослушиватель, все, что нам нужно сделать, это вызвать метод eventListener
при создании нашего экземпляра OkHttpClient
, и он должен просто работать:
OkHttpClient client = new OkHttpClient.Builder()
.eventListener(new SimpleLogEventsListener())
.build();
В следующем разделе мы рассмотрим, как мы можем протестировать наш новый слушатель.
5.2. Тестирование прослушивателя событий
Теперь мы определили наш первый прослушиватель событий; давайте продолжим и напишем наш первый интеграционный тест:
@Rule
public MockWebServer server = new MockWebServer();
@Test
public void givenSimpleEventLogger_whenRequestSent_thenCallsLogged() throws IOException {
server.enqueue(new MockResponse().setBody("Hello ForEach Readers!"));
OkHttpClient client = new OkHttpClient.Builder()
.eventListener(new SimpleLogEventsListener())
.build();
Request request = new Request.Builder()
.url(server.url("/"))
.build();
try (Response response = client.newCall(request).execute()) {
assertEquals("Response code should be: ", 200, response.code());
assertEquals("Body should be: ", "Hello ForEach Readers!", response.body().string());
}
}
Прежде всего, мы используем правило JUnit OkHttp MockWebServer
.
Это легкий веб-сервер с поддержкой сценариев для тестирования HTTP-клиентов, который мы собираемся использовать для тестирования наших прослушивателей событий . Используя это правило, мы создадим чистый экземпляр сервера для каждого интеграционного теста.
Имея это в виду, давайте теперь пройдемся по ключевым частям нашего теста:
- Прежде всего, мы настраиваем фиктивный ответ, который содержит простое сообщение в теле.
- Затем мы создаем наш
OkHttpClient
и настраиваем нашSimpleLogEventsListener.
- Наконец, мы отправляем запрос и проверяем полученный код ответа и тело с помощью утверждений.
5.3. Запуск теста
Когда мы запустим наш тест, мы увидим наши зарегистрированные события:
callStart at 2021-05-04T17:51:33.024
...
requestHeadersEnd at 2021-05-04T17:51:33.046 with headers User-Agent: A ForEach Reader
Host: localhost:51748
Connection: Keep-Alive
Accept-Encoding: gzip
...
responseHeadersEnd at 2021-05-04T17:51:33.053 with headers Content-Length: 23
callEnd at 2021-05-04T17:51:33.055
6. Собираем все вместе
Теперь давайте представим, что мы хотим использовать наш простой пример ведения журнала и записывать прошедшее время для каждого из шагов в нашей цепочке вызовов:
public class EventTimer extends EventListener {
private long start;
private void logTimedEvent(String name) {
long now = System.nanoTime();
if (name.equals("callStart")) {
start = now;
}
long elapsedNanos = now - start;
System.out.printf("%.3f %s%n", elapsedNanos / 1000000000d, name);
}
@Override
public void callStart(Call call) {
logTimedEvent("callStart");
}
// More event listener methods
}
Это очень похоже на наш первый пример, но на этот раз мы записываем время, прошедшее с момента начала нашего вызова для каждого события. Как правило, это может быть довольно интересно для обнаружения сетевой задержки .
Давайте посмотрим, запустим ли мы это на реальном сайте, таком как наш собственный https://www.foreach.com/ :
0.000 callStart
0.012 proxySelectStart
0.012 proxySelectEnd
0.012 dnsStart
0.175 dnsEnd
0.183 connectStart
0.248 secureConnectStart
0.608 secureConnectEnd
0.608 connectEnd
0.609 connectionAcquired
0.612 requestHeadersStart
0.613 requestHeadersEnd
0.706 responseHeadersStart
0.707 responseHeadersEnd
0.765 responseBodyStart
0.765 responseBodyEnd
0.765 connectionReleased
0.765 callEnd
Поскольку этот вызов проходит через HTTPS, мы также увидим события secureConnectStart
и secureConnectStart
.
7. Мониторинг неудачных вызовов
До сих пор мы фокусировались на успешных HTTP-запросах, но мы также можем фиксировать неудачные события:
@Test (expected = SocketTimeoutException.class)
public void givenConnectionError_whenRequestSent_thenFailedCallsLogged() throws IOException {
OkHttpClient client = new OkHttpClient.Builder()
.eventListener(new EventTimer())
.build();
Request request = new Request.Builder()
.url(server.url("/"))
.build();
client.newCall(request).execute();
}
В этом примере мы сознательно избегали настройки нашего фиктивного веб-сервера, что означает, конечно, что мы увидим катастрофический сбой в виде исключения SocketTimeoutException
.
Давайте посмотрим на результат, когда мы запустим наш тест сейчас:
0.000 callStart
...
10.008 responseFailed
10.009 connectionReleased
10.009 callFailed
Как и ожидалось, мы увидим начало нашего вызова, а затем через 10 секунд произойдет тайм-аут соединения, и, следовательно, мы увидим зарегистрированные события responseFailed
и callFailed
.
8. Несколько слов о параллелизме
До сих пор мы предполагали, что у нас нет нескольких одновременных вызовов . Если мы хотим приспособиться к этому сценарию, нам нужно использовать метод eventListenerFactory
при настройке нашего OkHttpClient
.
Мы можем использовать фабрику для создания нового экземпляра EventListener
для каждого HTTP-вызова. Когда мы используем этот подход, в нашем слушателе можно сохранить состояние, специфичное для вызова.
9. Заключение
В этой статье мы узнали все о том, как перехватывать события с помощью OkHttp. Во-первых, мы начали с объяснения того, что такое событие, и понимания того, какие события нам доступны и в каком порядке они поступают при обработке HTTP-вызова.
Затем мы рассмотрели, как мы можем определить простой регистратор событий для захвата частей наших HTTP-вызовов и как написать интеграционный тест.
Как всегда, полный исходный код статьи доступен на GitHub .