воскресенье, 2 февраля 2014 г.

REST под Android. Часть 3: библиотеки Square

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

OkHttp: продвинутый HTTP-клиент

Как известно, под Android имеется два стандартных HTTP-клиента: Apache HTTP Client и HttpURLConnection. Первый оптимален на старых версиях Android (Eclair and Froyo), второй - на более поздних версиях.

Библиотека OkHttp - это альтернативный HTTP-клиент, основанный на исходных кодах HttpURLConnection, и реализующий множество дополнительных полезных функций. В частности, в OkHttp:

  • добавлена поддержка протоколов HTTP 2 (draft), SPDY 3 (draft);
  • реализовано автоматическое восстановление соединения, при возникновении распространенных сетевых проблем (например, проблем c прокси-сервером и TLS рукопожатием);
  • реализован пул соединений, обеспечивающий повторное использование HTTP и SPDY соединений, за счет чего увеличивается пропускная способность и снижается время ожидания.
Важнейший плюс OkHttp - библиотека устраняет проблемы реализации HttpURLConnection на старых версиях Android. Например, прозрачная поддержка GZip реализована в HttpURLConnection только в Gingerbread (Android 2.3), кэш ответов - в Ice Cream Sandwich (Android 4.0). OkHttp предоставляет весь этот функционал, начиная с Android 2.2.

Retrofit: безопасный REST-клиент

В общем случае, при выполнении запроса к REST-серверу, требуется выполнить ряд операций:
  • сформировать URL;
  • задать HTTP-заголовки;
  • выбрать тип HTTP-запроса;
  • сформировать тело HTTP-запроса, т.е. преобразовать Java объект в JSON;
  • выполнить запрос, воспользовавшись HTTP-клиентом;
  • распарсить результаты запроса - преобразовать полученный JSON в Java объект.
Куча кода на каждый запрос? Библиотека Retrofit позволяет описать все перечисленные операции с помощью аннотаций - компактно и без ошибок. Рассмотрим пример. Пусть у нас есть запрос из BooksSet REST API:
GET /books/1234
Этот запрос возвращает JSON вида
{
    "id": "1236",
    "title": "Yellow mist",
    "author": "Alexander Volkov",
    "released": "1970"
}
С помощью Retrofit мы можем описать этот запрос следующим образом:
import retrofit.http.GET;
import retrofit.http.Path;
import retrofit.http.Query;

public class Book {
  public int id;
  public String title;
  public String author;
  public int released;
}

public interface IBookSetRestAPI {

  @GET("/v1/books/{book_id}")
  Book getBook(@Path("book_id") int book_id);

}
Выполнить запрос к серверу можно так:
RestAdapter restAdapter = new RestAdapter.Builder()
  .setServer("http://samplebookssetrestapi.apiary.io")
  .build();
IBookSetRestAPI rest_api = restAdapter.create(IBookSetRestAPI.class);
Book book = rest_api.getBook(136);

Каждый запрос к REST-серверу описывается отдельной функцией в интерфейсе IBookSetRestAPI. Тип HTTP запроса задается с помощью аннотаций @GET, @POST, @PUT, @DELETE, @HEAD, @PATCH, параметры URI-шаблона задаются через параметры функции и описываются аннотацией @Path.

В случае, когда в URL присутствуют переменные, они задаются через аннотацию @Query, например запросы:

GET /v1/books?limit=10&offset=20
GET /v1/books?limit=10
описываются следующим образом:
public interface IBookSetRestAPI {

  @GET("/v1/books/{book_id}")
  Book getBook(@Path("book_id") int book_id);

  @GET("/v1/books")
  ListBooks getBooks(@Query("limit") int limit);

  @GET("/v1/books")
  ListBooks getBooks(@Query("limit") int limit, @Query("offset") int offset);
}

Разумеется, есть и другие аннотации - для задания заголовков HTTP-запросов, для формирования составных запросов и т.д.

Несмотря на то, что аннотации Retrofit по структуре похожи на аннотации JAX-RS, эти наборы аннотаций не совместимы. Причина: аннотации JAX-RS ориентированы на серверную часть, а аннотации Retrofit - на клиентскую.

Парсинг результатов

Функция Book getBook(int book_id) возвращает объект, распарсивание JSON производится автоматически. По умолчанию, для распарсивания используется GSON, т.е. в проект необходимо добавить gson-2.2.4.jar. Библиотека поддерживает кастомизацию конвертации объектов, причем конвертеры XML и Protobuf доступны вместе с библиотекой.

Можно получать результаты в "сыром", нераспарсенном виде. Достаточно определить интерфейс IBookSetRestAPI следующим способом:

import retrofit.client.Response;
import retrofit.http.GET;
import retrofit.http.Path;
import retrofit.http.Query;

public interface IBookSetRestAPI {

  @GET("/v1/books/{book_id}")
  Response getBook(@Path("book_id") int book_id);

  @GET("/v1/books")
  Response getBooks(@Query("limit") int limit);

  @GET("/v1/books")
  Response getBooks(@Query("limit") int limit, @Query("offset") int offset);
}
Объект Response дает прямой доступ к содержимому ответа сервера - можно парсить его вручную.

OkHttp и Retrofit

Retrofit выбирает HTTP-клиента следующим образом:
  • Если OkHttp задействована в проекте, то используется OkHttpClient;
  • В противном случае: если приложение работает под Android 2.2 или ниже, используется HttpClient, иначе - HttpURLConnection.

GZip и Retrofit

OkHttp использует Gzip автоматически при условии, что заголовок Accept-Encoding не задан явно. Таким образом, если Accept-Encoding не задан, то:
  • OkHttp автоматически добавляет "Accept-Encoding: gzip" и отправляет запрос серверу;
  • далее, проверяет ответ от сервера: если в заголовках ответа есть "Content-Encoding: gzip", значит данные запакованы;
  • запакованные данные OkHttp автоматически распаковывает. В результате, клиент получает Response с распакованным содержимым; если же используется конвертер, то в функцию fromBody конвертера опять же приходят уже распакованные данные.
Если же по каким-либо причинам transparent gzip необходимо отключить, то следует явно задать заголовок Accept-Encoding. Например:
@Headers("Accept-Encoding: identity")
@GET("/v1/books")
ListBooks getBooks();

@Headers("Accept-Encoding: gzip")
@GET("/v1/books")
ListBooks getBooks2();
Запрос getBooks будет работать без gzip. Запрос getBooks2 будет поддерживать gzip, однако на выходе пользователь получит запакованные данные (как в Response, так и в функции fromBody конвертера), которые необходимо будет распаковывать вручную.

Синхронное и асинхронное выполнение запросов

Retrofit поддерживает оба варианта. Асинхронное выполнение запросов требует подключения библиотеки RxJava.

Зависимости Retrofit

Все зависимости опциональны:
  • OkHttp - если требуется OkHttpClient;
  • GSon - если планируется использовать стандартный GSon конвертер;
  • RxJava - если требуется асинхронное выполнение запросов.

Примеры использования Retrofit

Неплохая коллекция примеров.

MimeCraft: формирование HTTP-запросов

Еще одна библиотека, MimeCraft, предназначена для формирования тела составных HTTP-запросов и данных веб-формы. Все это умеет делать Retrofit, однако синтаксис немного другой. Пример:
  • Retrofit:
    @FormUrlEncoded
    @POST("/user/edit")
    User updateUser(@Field("first_name") String first, @Field("last_name") String last);
    ...
    User u = rest_api.updateUser("First", "Last");
    
  • MimeCraft:
    FormEncoding fe = new FormEncoding.Builder()
        .add("first_name", "First")
        .add("last_name", "Last")
        .build();
    fe.writeBodyTo(outstream);
    

Выводы

Библиотека Retrofit идеально подходит для целей реализации паттернов Virgil Dobjanschi. Причем функционал Retrofit и RoboSpice не пересекается: RoboSpice реализует каркас шаблона A, Retrofit - берет на себя всю низкоуровневую работу по формированию запроса к серверу и обработке результатов. Другими словами, Retrofit предоставляет весь тот функционал, который в RoboSpice требуется реализовать в SpiceRequest, так что библиотеки хорошо дополняют друг друга.

OkHttp - удобная замена для стандартных HTTP-клиентов, с более эффективной реализацией, с поддержкой SPDY и без проблем работающая на старых версиях Android (>= 2.2).

MimeCraft можно использовать для формирования тела составных HTTP-запросов и запросов форм вручную - Retrofit позволяет сделать все то же самое через аннотации.

Следующая часть статьи будет посвящена реализации тестового проекта на базе RoboSpice + Retrofit + OkHttp.


5 комментариев:

  1. Везде пишут, что для того, чтобы Retrofit стал использовать gzip, достаточно добавить соответствующий заголовок (так, как описано у вас в статье, либо же с помощью RequestInterceptor), но нигде не описано, как добиться нормального распаковывания ответа сервера.
    И я не могу понять, что я делаю не так: без этого заголовка все работает, с ним - парсер получает на вход гзипованую строку, и, естественно, ничего не работает.
    В логе это выглядит вот так:

    02-04 10:54:01.692: D/retrofit(8656): Transfer-Encoding: chunked
    02-04 10:54:01.692: D/retrofit(8656): � ������������ =�An�0 E�byoaǎ!���Ċ Dhխ�L�"�+;Ab� �2]��Dz�&
    �f�?�I��<� g ��; [���B�sW� �ƀm'�i
    !`T���?�gH1 �F$�L F�1_�ɟ� �:
    F��q�p/�' �'� �
    9� � �R3-����ŤԂ !��\GuUs)�
    02-04 10:54:01.692: D/retrofit(8656): ����~vv�O�Rr�X��S�����������~L$3��q [�U3
    ���� �հ ����
    02-04 10:54:01.692: D/retrofit(8656): <--- END HTTP (226-byte body)

    А в случае отсутствия заголовка про gzip, вместо этой абракадабры - нормальный текст.

    ОтветитьУдалить
    Ответы
    1. Вы правы, у меня была ошибка в статье. Спасибо, поправил и написал подробно. Пришлось под отладчиком посмотреть, как именно работает OkHttp. Transparent gzip - прозрачная распаковка запакованного ответа, - используется во всех запросах, у которых нет заголовка Accept-Encoding. В этом случае Accept-Encoding:gzip добавляется OkHttp автоматически. Если же Accept-Encoding указан явно, то transparent gzip отключается и OkHttp выдает наружу запакованные данные, как в вашем случае.

      Удалить
    2. Спасибо большое! Я не сообразил подебажить okhttp, и думал, что gzip вообще не используется.

      Удалить
  2. Статья интересная, спасибо. Будете ли вы рассматривать другие библиотеки для работы с сетью? http://android-arsenal.com (Networking)

    ОтветитьУдалить
  3. Спасибо большое за статью. Также мого полезной информации можно найти здесь https://amazingcart.us/

    ОтветитьУдалить