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

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

1. Вступление

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

Кроме того, мы рассмотрим более сложные сценарии использования клиента с настраиваемыми заголовками, тайм-аутами, кешированием ответов и т. Д.

2. OkHttp Обзор

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

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

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

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

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

After this brief overview, let’s see some usage examples.

3. Maven Dependency

Давайте сначала добавим библиотеку как зависимость вpom.xml:


    com.squareup.okhttp3
    okhttp
    3.4.2

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

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

Чтобы отправить синхронный запрос GET, нам нужно построить объектRequest на основеURL и создатьCall. После его выполнения мы возвращаем экземпляр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. Callback позволяет нам прочитать ответ, когда он доступен для чтения. Это происходит после того, как заголовки ответа готовы.

Чтение тела ответа может все еще блокировать. 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-адреса мы можем передать его нашему объектуRequest:

@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. Пример отправки запроса

7.1. Базовый пост

В этом простом примере мы создаемRequestBody для отправки двух параметров - «имя пользователя» и «пароль» - с запросом POST:

@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));
}

7.2. POST с авторизацией

В этом примере мы увидим, как выполнить POST с учетными данными базовой аутентификации, плюс мы отправимString в качестве тела запроса:

@Test
public void whenSendPostRequestWithAuthorization_thenCorrect()
  throws IOException {
    String postBody = "test post";

    Request request = new Request.Builder()
      .url(URL_SECURED_BY_BASIC_AUTHENTICATION)
      .addHeader("Authorization", Credentials.basic("test", "test"))
      .post(RequestBody.create(
        MediaType.parse("text/x-markdown; charset=utf-8"), postBody))
      .build();

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

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

7.3. POST с JSON

В этом примере мы отправим POST-запрос сJSON какRequestBody:

@Test
public void whenPostJson_then:Correct() throws IOException {
    String json = "{\"id\":1,\"name\":\"John\"}";

    RequestBody body = RequestBody.create(
      MediaType.parse("application/json; charset=utf-8"), json);

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

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

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

7.4. Многокомпонентный запрос POST

В этом примере мы отправим составной запрос POST. Нам нужно построить нашRequestBody какMultipartBody для публикацииFile, имени пользователя и пароля:

@Test
public void whenSendMultipartRequest_thenCorrect()
  throws IOException {
    RequestBody requestBody = new MultipartBody.Builder()
      .setType(MultipartBody.FORM)
      .addFormDataPart("username", "test")
      .addFormDataPart("password", "test")
      .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/multipart")
      .post(requestBody)
      .build();

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

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

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

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

В этом примере мы увидим, как загрузитьFile. Загрузим файл «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. Получить прогресс загрузки файла

Наконец, давайте посмотрим, как узнать о ходе загрузкиFile. Мы расширим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, который является расширенной версией пересылкиSink:

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. Установка заголовка запроса

Чтобы установить любой пользовательский заголовок наRequest, мы можем использовать простой вызов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 project.