Перейти к основному содержимому

Руководство по OkHttp

· 9 мин. чтения

1. Введение

В этом руководстве мы рассмотрим основы отправки различных типов HTTP-запросов, а также получения и интерпретации HTTP-ответов. Затем мы узнаем, как настроить клиент с помощью OkHttp .

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

2. Обзор OkHttp

OkHttp — это эффективный клиент HTTP и HTTP/2 для приложений Android и Java.

Он поставляется с расширенными функциями, такими как пул соединений (если HTTP/2 недоступен), прозрачное сжатие GZIP и кэширование ответов, чтобы полностью избежать сети для повторяющихся запросов.

Он также может восстанавливаться после распространенных проблем с подключением; при сбое подключения, если служба имеет несколько IP-адресов, она может повторить запрос на альтернативные адреса.

На высоком уровне клиент предназначен как для блокировки синхронных вызовов, так и для неблокирующих асинхронных вызовов.

OkHttp поддерживает Android 2.3 и выше. Для Java минимальное требование — 1.7.

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

3. Зависимость от Maven

Во-первых, мы добавим библиотеку в качестве зависимости в pom.xml :

<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.1</version>
</dependency>

Чтобы увидеть последнюю зависимость этой библиотеки, посетите страницу на Maven Central .

4. Синхронный GET с OkHttp

Чтобы отправить синхронный запрос GET, нам нужно создать объект Request на основе URL -адреса и сделать вызов . После его выполнения мы получим обратно экземпляр Response :

@Test
public void whenGetRequest_thenCorrect() throws IOException {
Request request = new Request.Builder()
.url(BASE_URL + "/date")
.build();

Call call = client.newCall(request);
Response response = call.execute();

assertThat(response.code(), equalTo(200));
}

5. Асинхронный GET с OkHttp

Чтобы сделать асинхронный GET, нам нужно поставить Call в очередь . Обратный вызов позволяет нам прочитать ответ, когда он доступен для чтения. Это происходит после того, как заголовки ответа готовы.

Чтение тела ответа может по-прежнему блокироваться. OkHttp в настоящее время не предлагает никаких асинхронных API для получения тела ответа по частям:

@Test
public void whenAsynchronousGetRequest_thenCorrect() {
Request request = new Request.Builder()
.url(BASE_URL + "/date")
.build();

Call call = client.newCall(request);
call.enqueue(new Callback() {
public void onResponse(Call call, Response response)
throws IOException {
// ...
}

public void onFailure(Call call, IOException e) {
fail();
}
});
}

6. GET с параметрами запроса

Наконец, чтобы добавить параметры запроса в наш запрос GET, мы можем воспользоваться HttpUrl.Builder .

После того, как мы создадим URL-адрес, мы можем передать его нашему объекту запроса :

@Test
public void whenGetRequestWithQueryParameter_thenCorrect()
throws IOException {

HttpUrl.Builder urlBuilder
= HttpUrl.parse(BASE_URL + "/ex/bars").newBuilder();
urlBuilder.addQueryParameter("id", "1");

String url = urlBuilder.build().toString();

Request request = new Request.Builder()
.url(url)
.build();
Call call = client.newCall(request);
Response response = call.execute();

assertThat(response.code(), equalTo(200));
}

7. POST-запрос

Теперь давайте посмотрим на простой POST-запрос, в котором мы создаем RequestBody для отправки параметров «имя пользователя» и «пароль» :

@Test
public void whenSendPostRequest_thenCorrect()
throws IOException {
RequestBody formBody = new FormBody.Builder()
.add("username", "test")
.add("password", "test")
.build();

Request request = new Request.Builder()
.url(BASE_URL + "/users")
.post(formBody)
.build();

Call call = client.newCall(request);
Response response = call.execute();

assertThat(response.code(), equalTo(200));
}

В нашей статье «Краткое руководство по почтовым запросам с OkHttp » есть больше примеров POST-запросов с OkHttp.

8. Загрузка файлов

8.1. Загрузить файл

В этом примере мы покажем, как загрузить файл . Мы загрузим файл « test.ext» с помощью MultipartBody.Builder :

@Test
public void whenUploadFile_thenCorrect() throws IOException {
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", "file.txt",
RequestBody.create(MediaType.parse("application/octet-stream"),
new File("src/test/resources/test.txt")))
.build();

Request request = new Request.Builder()
.url(BASE_URL + "/users/upload")
.post(requestBody)
.build();

Call call = client.newCall(request);
Response response = call.execute();

assertThat(response.code(), equalTo(200));
}

8.2. Получить ход загрузки файла

Затем мы узнаем, как получить ход загрузки файла . Мы расширим RequestBody , чтобы получить представление о процессе загрузки.

Вот метод загрузки:

@Test
public void whenGetUploadFileProgress_thenCorrect()
throws IOException {
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", "file.txt",
RequestBody.create(MediaType.parse("application/octet-stream"),
new File("src/test/resources/test.txt")))
.build();

ProgressRequestWrapper.ProgressListener listener
= (bytesWritten, contentLength) -> {
float percentage = 100f * bytesWritten / contentLength;
assertFalse(Float.compare(percentage, 100) > 0);
};

ProgressRequestWrapper countingBody
= new ProgressRequestWrapper(requestBody, listener);

Request request = new Request.Builder()
.url(BASE_URL + "/users/upload")
.post(countingBody)
.build();

Call call = client.newCall(request);
Response response = call.execute();

assertThat(response.code(), equalTo(200));
}

Теперь вот интерфейс ProgressListener, который позволяет нам наблюдать за ходом загрузки:

public interface ProgressListener {
void onRequestProgress(long bytesWritten, long contentLength);
}

Далее идет ProgressRequestWrapper, который является расширенной версией RequestBody :

public class ProgressRequestWrapper extends RequestBody {

@Override
public void writeTo(BufferedSink sink) throws IOException {
BufferedSink bufferedSink;

countingSink = new CountingSink(sink);
bufferedSink = Okio.buffer(countingSink);

delegate.writeTo(bufferedSink);

bufferedSink.flush();
}
}

Наконец, вот CountingSink, который является расширенной версией ForwardingSink :

protected class CountingSink extends ForwardingSink {

private long bytesWritten = 0;

public CountingSink(Sink delegate) {
super(delegate);
}

@Override
public void write(Buffer source, long byteCount)
throws IOException {
super.write(source, byteCount);

bytesWritten += byteCount;
listener.onRequestProgress(bytesWritten, contentLength());
}
}

Обратите внимание, что:

  • При расширении ForwardingSink до «CountingSink» мы переопределяем метод write() для подсчета записанных (переданных) байтов.
  • При расширении RequestBody до « ProgressRequestWrapper » метод writeTo() переопределялся для использования нашего «ForwardingSink» .

9. Настройка пользовательского заголовка

9.1. Установка заголовка в запросе

Чтобы установить любой пользовательский заголовок в запросе , мы можем использовать простой вызов addHeader :

@Test
public void whenSetHeader_thenCorrect() throws IOException {
Request request = new Request.Builder()
.url(SAMPLE_URL)
.addHeader("Content-Type", "application/json")
.build();

Call call = client.newCall(request);
Response response = call.execute();
response.close();
}

9.2. Установка заголовка по умолчанию

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

Например, если мы хотим установить тип контента «application/json» для каждого запроса, нам нужно установить перехватчик для нашего клиента:

@Test
public void whenSetDefaultHeader_thenCorrect()
throws IOException {

OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(
new DefaultContentTypeInterceptor("application/json"))
.build();

Request request = new Request.Builder()
.url(SAMPLE_URL)
.build();

Call call = client.newCall(request);
Response response = call.execute();
response.close();
}

Вот DefaultContentTypeInterceptor, который является расширенной версией Interceptor :

public class DefaultContentTypeInterceptor implements Interceptor {

public Response intercept(Interceptor.Chain chain)
throws IOException {

Request originalRequest = chain.request();
Request requestWithUserAgent = originalRequest
.newBuilder()
.header("Content-Type", contentType)
.build();

return chain.proceed(requestWithUserAgent);
}
}

Обратите внимание, что перехватчик добавляет заголовок к исходному запросу.

10. Не следуйте перенаправлениям

В этом примере мы увидим, как настроить OkHttpClient , чтобы остановить перенаправления.

По умолчанию, если на запрос GET отвечает HTTP 301 Moved Permanently, перенаправление выполняется автоматически. В некоторых случаях это совершенно нормально, но есть и другие варианты использования, когда это нежелательно.

Чтобы добиться такого поведения, когда мы создаем наш клиент, нам нужно установить для параметра followRedirects значение false .

Обратите внимание, что ответ вернет код состояния HTTP 301 :

@Test
public void whenSetFollowRedirects_thenNotRedirected()
throws IOException {

OkHttpClient client = new OkHttpClient().newBuilder()
.followRedirects(false)
.build();

Request request = new Request.Builder()
.url("http://t.co/I5YYd9tddw")
.build();

Call call = client.newCall(request);
Response response = call.execute();

assertThat(response.code(), equalTo(301));
}

Если мы включим перенаправление с параметром true (или удалим его полностью), клиент будет следовать перенаправлению, и тест будет провален, так как код возврата будет HTTP 200.

11. Тайм-ауты

Мы можем использовать тайм-ауты для сбоя вызова, когда его одноранговый узел недоступен. Сбои в сети могут быть вызваны проблемами с подключением клиента, проблемами с доступностью сервера или чем-то другим. OkHttp поддерживает тайм-ауты подключения, чтения и записи.

В этом примере мы создали наш клиент с readTimeout в 1 секунду, в то время как URL-адрес обслуживается с задержкой в 2 секунды:

@Test
public void whenSetRequestTimeout_thenFail()
throws IOException {
OkHttpClient client = new OkHttpClient.Builder()
.readTimeout(1, TimeUnit.SECONDS)
.build();

Request request = new Request.Builder()
.url(BASE_URL + "/delay/2")
.build();

Call call = client.newCall(request);
Response response = call.execute();

assertThat(response.code(), equalTo(200));
}

Обратите внимание, что тест завершится ошибкой, так как время ожидания клиента меньше, чем время ответа ресурса.

12. Отмена вызова

Мы можем использовать Call.cancel() для немедленной остановки текущего вызова. Если поток в данный момент пишет запрос или читает ответ, будет выброшено исключение IOException .

Мы используем этот метод для сохранения сети, когда вызов больше не нужен, например, когда наш пользователь уходит из приложения:

@Test(expected = IOException.class)
public void whenCancelRequest_thenCorrect()
throws IOException {
ScheduledExecutorService executor
= Executors.newScheduledThreadPool(1);

Request request = new Request.Builder()
.url(BASE_URL + "/delay/2")
.build();

int seconds = 1;
long startNanos = System.nanoTime();

Call call = client.newCall(request);

executor.schedule(() -> {
logger.debug("Canceling call: "
+ (System.nanoTime() - startNanos) / 1e9f);

call.cancel();

logger.debug("Canceled call: "
+ (System.nanoTime() - startNanos) / 1e9f);

}, seconds, TimeUnit.SECONDS);

logger.debug("Executing call: "
+ (System.nanoTime() - startNanos) / 1e9f);

Response response = call.execute();

logger.debug(Call was expected to fail, but completed: "
+ (System.nanoTime() - startNanos) / 1e9f, response);
}

13. Кэширование ответов

Чтобы создать Cache , нам понадобится каталог кеша, в который мы можем читать и писать, и ограничение на размер кеша.

Клиент будет использовать его для кэширования ответа:

@Test
public void whenSetResponseCache_thenCorrect()
throws IOException {
int cacheSize = 10 * 1024 * 1024;

File cacheDirectory = new File("src/test/resources/cache");
Cache cache = new Cache(cacheDirectory, cacheSize);

OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();

Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();

Response response1 = client.newCall(request).execute();
logResponse(response1);

Response response2 = client.newCall(request).execute();
logResponse(response2);
}

После запуска теста ответ от первого вызова не будет кэшироваться. Вызов метода cacheResponse вернет null , а вызов метода networkResponse вернет ответ из сети.

Папка кеша также будет заполнена файлами кеша.

Выполнение второго вызова произведет противоположный эффект, так как ответ уже будет закэширован. Это означает, что вызов networkResponse вернет null, а вызов cacheResponse вернет ответ из кеша.

Чтобы ответ не использовал кеш, мы можем использовать CacheControl.FORCE_NETWORK . Чтобы он не использовал сеть, мы можем использовать CacheControl.FORCE_CACHE .

Важно отметить, что если мы используем FORCE_CACHE и для ответа требуется сеть, OkHttp вернет ответ 504 Unsatisfiable Request.

14. Заключение

В этой статье мы рассмотрели несколько примеров использования OkHttp в качестве клиента HTTP и HTTP/2.

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