«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-запрос, нам нужно создать объект запроса на основе 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, нам нужно поставить вызов в очередь. Обратный вызов позволяет нам прочитать ответ, когда он доступен для чтения. Это происходит после того, как заголовки ответа готовы.

Чтение тела ответа может по-прежнему блокироваться. 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. Строитель.

После создания 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. 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.

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

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

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

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

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

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

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

Кроме того, папка кеша будет заполнена файлами кеша.

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

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

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

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

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

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

«