Меня осенило, когда я был на местном хакатоне. Участникам было предложено получить некоторые результаты из общедоступного API. И вдруг люди вокруг меня начали спорить, какой HTTP-клиент использовать. Одни выбрали RestTemplate, другие - HTTP-клиент Apache, третьи пробовали что-то вроде JSoup.

Оказывается, большинство опытных Java-разработчиков не знали, что в стандартной библиотеке Java уже есть встроенный HTTP-клиент. Он просто называется URL

Чтобы улучшить эту ситуацию, давайте возьмем простую задачу, например, синтаксический анализ ответа от GitHub API, без внешнего HTTP-клиента (хотя мы все равно будем использовать некоторую библиотеку для синтаксического анализа JSON).

Мы начнем с класса данных, поскольку он поможет нам понять, что мы хотим получить:

data class Repo(val name: String,
                val url: String,
                val topics: List<String>,
                val updatedAt: LocalDateTime)

Поскольку мы будем использовать Kotlin, вполне естественно, что полученный URL будет принадлежать JetBrains:

Https://api.github.com/orgs/jetbrains/repos

Наш исходный код будет выглядеть примерно так:

Поскольку GitHub API разбит на страницы, мы выбираем наши репозитории страницу за страницей, пока не останется больше страниц.

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

Теперь мы добавим код для печати этих результатов в порядке убывания, отсортированных по полю updatedAt:

allRepos.sortedByDescending { 
    it.updatedAt 
}.forEach {
    println(it)
}

Теперь мы готовы к налаживанию контактов.

Чтобы получить содержимое с удаленного URL-адреса, мы можем просто использовать:

URL(url).openStream().use { 
    it // InputStream
}

Вызов use автоматически закроет поток, когда мы выйдем из блока.

Распространенная ошибка, связанная с use в потоках, - это попытка вернуть считыватель из блока use следующим образом:

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

Теперь предположим, что у нас есть этот входной поток, представляющий JSON, как мы можем его проанализировать?

Один из вариантов - использовать Джексона:

compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.6'

Обычный вариант - использовать метод ObjectMapper().readValue().

Но поскольку ответ этого конкретного API - это массив JSON, его анализ становится немного громоздким:

val repos: List<Repo> = ObjectMapper().readValue(it,
        (object : TypeReference<List<Repo>>() {}) )

Коротко, но некрасиво.

Оказывается, не работает:

InvalidDefinitionException: Cannot construct instance of `Repo` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

Один из вариантов исправить это - добавить еще одну зависимость:

compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+"

И используйте новую функцию jacksonObjectMapper():

val repos: List<Repo> = jacksonObjectMapper().readValue(it)

Но это создаст больше проблем, которые он решит.

Что, если бы существовал другой способ синтаксического анализа JSON, который дал бы нам немного больше контроля?

К счастью, он существует в виде readTree()

val result = ObjectMapper().readTree(it).map { node -> // JsonNode
    // Our parsing code comes here
}

Теперь мы можем перебрать каждый объект в массиве JSON, возвращаемом API, и начать их синтаксический анализ.

Получить некоторые свойства очень просто:

Но поскольку topics являются вложенным массивом JSON, который также является необязательным, мы используем безопасный вызов и сопоставляем значения:

Наконец, мы возвращаем наш только что созданный класс данных:

Repo(name,
     htmlUrl,
     topics=topics,
     updatedAt=LocalDateTime.parse(updatedAt))

Это работает? Конечно же нет! Получаем следующее исключение:

DateTimeParseException: Text '2018-08-09T04:43:06Z' could not be parsed, unparsed text found at index 19

Похоже, что GitHub использует формат, отличный от того, который LocalDateTime использует по умолчанию (это ISO_LOCAL_DATE_TIME).

Давайте исправим это, используя правильный формат даты и времени для GitHub:

updatedAt=LocalDateTime.parse(updatedAt, DateTimeFormatter.ISO_OFFSET_DATE_TIME)

Это намного лучше. Но есть еще одна проблема. Наши topics всегда возвращаются пустыми:

Repo(name=Chocolatey, url=https://github.com/JetBrains/Chocolatey, topics=[], updatedAt=2017-03-29T02:41:51)

После беглого просмотра документации GitHub API вы можете заметить, что чтобы получить темы, вам необходимо установить определенный заголовок:

Accept: application/vnd.github.mercy-preview+json

Но как это сделать с URL? Вам нужно забыть об этом и вернуться к обычным HTTP-клиентам?

Конечно, я привел вас сюда не только для того, чтобы сказать, что «извините, но наша принцесса в другом замке». Для этого нам просто нужно использовать другой API более низкого уровня, который называется openConnection().

Конечно, нельзя просто использовать openConnection ():

URL(url).openConnection().use { ... } // Won't work

Вам нужно будет получить от него входной поток:

URL(url).openConnection().
        getInputStream().use { ... }

Это то, что openStream() метод сделал для нас раньше.

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

openConnection().setRequestProperty("Accept", "application/vnd.github.mercy-preview+json")

Но проблема в том, что setRequestProperty() не владеет свободно. Вы не можете красиво связать его и называть так:

К счастью, в Котлине у нас есть apply(), чтобы решить эту проблему:

И теперь наш topics проанализирован, как и ожидалось:

Repo(name=Chocolatey, url=https://github.com/JetBrains/Chocolatey, topics=[choco, chocolatey, chocolatey-packages, jetbrains], updatedAt=2017–03–29T02:41:51)

Окончательный код выглядит так:

Это можно было бы сократить

Если вы не хотите согласовывать себя с потоками и заголовками, а также (очень) уверены, что ваши ответы поместятся в памяти, Kotlin также предлагает более простой способ. : readText() метод:

Резюме

Итак, эта статья демонстрирует, что не обязательно вводить другую зависимость для простых случаев использования HTTP в Kotlin или Java.

Означает ли это, что вам следует прекратить использование клиентов HTTP, которые вы используете сейчас?

Возможно нет. Клиентские библиотеки HTTP имеют множество замечательных функций, таких как отслеживание перенаправлений, кеширование ответов, обеспечение безопасности и многое другое.

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