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

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

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

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

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

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

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

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

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

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

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

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

7. POST-запрос

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

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

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

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

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

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

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

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

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

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

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

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

Во-первых, вот метод загрузки:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

«

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

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

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

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

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

11. Тайм-ауты

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

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