-
Notifications
You must be signed in to change notification settings - Fork 7
[ru] GitHub клиент
На практике Restler-у не так уж и важно, какая технология на самом деле используется на стороне сервера, главное чтобы эндпоинты можно было описать в терминах (на данный момент только) Spring MVC Controller, а структуры - в терминах Java/Kotlin/Scala/Groovy классов.
В этом туториале мы разработаем небольшое приложение на Kotlin, которое на вход получает имя пользователя GitHub и выдаёт список языков, используемый в нефоркнутых репозиториях.
Полная версия исходного кода досупна здесь
Давайте рассмотрим класс, который мы будем использовать для разбора ответа от GitHub:
1. class UserRepo @JsonCreator constructor(
2. val name: String,
3. val fork: Boolean,
4. val language: String?)
В объекты этого класса мы будем разбирать ответы от https://api.github.com/users/{userName}/repos
. Большую часть полей ответа мы игнорируем и выбираем только 3 из них:
-
name
- имя репозитория, потребуется для отладочного вывода -
fork
- флаг является ли репозиторий форком. Нас интересуют только не форкнутые репозитории -
language
- будем использовать для ранней фильтрации репозиториев, которые не содержат кода на известных языках
Теперь давайте рассмотрим класс GitHub который будет выступать в роли дескриптора сервиса.
1. @RestController
2. open class GitHub {
3.
4. @RequestMapping("/users/{userName}/repos")
5. open fun userRepos(@PathVariable userName: String): List<UserRepo> = emptyList()
6.
7. @RequestMapping("/repos/{userName}/{repo}/languages")
8. open fun repoLanguages(@PathVariable userName: String, @PathVariable repo: String): DeferredResult<Map<String, Int>> =
9. DeferredResult<Map<String, Int>>()
10. }
Данный класс содержит два метода, которые описывают эндпоинты для получения списка репозиториев пользователя и списка языков репозитория.
Аннотация @RequestMapping
используется для того чтобы задать шаблон эндпоинта, а переменные указываются в фигурных скобках. Для каждой переменной шаблона, необходимо определить параметр метода с таким же именем, который обязательно должен быть проаннотирован @PathVariable
. Вы так же можете ввести дополнительные параметры проаннотированные @RequestParam
для передачи параметров запроса, но в нашем примере это не требуется.
Обратите внимание на тип результата метода repoLanguages
- DeferredResult
. Этот тип имеет особую поддержку в Restler и методы возвращающие этот тип обрабатываются в отдельном пуле потоков. Эта фича Restler позволит нам выполнить запросы языков репозиториев параллельно без написания ни единой строчки дополнительного кода.
В случае разработки на Java описать сервис лучше воспользовавшись интерфейсом, а не классом, но Kotlin пока что не поддерживает запись имён параметров интерфейсов в байткод. Это ограничение можно обойти дублируя имена параметров в аннотациях @PathVariable
, но т.к. имена будут меняться с большой вероятностью, избавление от дублирования в этом месте стоит небольшого шума в виде фейковых тел методов.
Теперь давайте рассмотрим собственно код решающий нашу задачу.
1. fun main(args: Array<String>) {
2. val springMvcSupport = SpringMvcSupport().
3. addJacksonModule(ParanamerModule())
4. val github = Restler("https://api.github.com/", springMvcSupport).
5. build().
6. produceClient(GitHub::class.java)
7. val userName = if (args.size() > 0) args[0] else "aleksey-zhidkov"
8. val userRepos = github.userRepos(userName)
9.
10. val userLngs = userRepos.
11. filter { it.language != null && !it.fork }.
12. map { Pair(it.name, github.repoLanguages(userName, it.name)) }.
13. flatMap {
14. val (name, res) = it
15. while (!res.hasResult()) Thread.sleep(100)
16. println("$name: ${res.getResult()}")
17. (res.getResult() as Map<String, Int>).keySet() }.
18. toSet()
19. println("User languages: $userLngs")
20. }
Рассмотрим этот код по частям. В строках 2-5 создаётся билдер сервиса, добавляется Jackson модуль Paranamer (необходим для того чтобы не дублировать имена параметров конструктора в аннотациях @JsonParameter
), строится сервис и создаётся клиент на базе класса GitHub
.
В строках 7-8 проверяется было ли передано имя пользователя и если нет то в качестве дефолтного имени пользователя используется "aleksey-zhidkov"
. Затем для полученного имени пользователя получается список его репозиториев на GitHub.
В строках 10-18 содержится основное мясо нашей программы, давайте рассмотрим каждую строчку в отдельности:
-
val userLngs = userRepos.
- объявляется переменная, в которой будет содержаться список языков программирования используемых пользователем. -
filter { it.language != null && !it.fork }.
- отфильтровываем репозитории для которых не определён язык и которые являются форками -
map { Pair(it.name, github.repoLanguages(userName, it.name)) }.
- каждый репозиторий превращаем в пару <Имя, Список Языков>. БлагодаряDeferredResult
вызовrepoLanguages
становится асинхронным. -
flatMap {
- превращаем список списков языков программирования в плоский список -
val (name, res) = it
- для удобства разбиваем пару на локальные переменный -
while (!res.hasResult()) Thread.sleep(100)
- грязно, но просто ждём выполнения асинхронного вызова -
println("$name: ${res.getResult()}")
- для отладки выводим языки каждого отдельного репозитория -
(res.getResult() as Map<String, Int>).keySet() }.
- GitHub по запросу языков возвращает мапу язык -> количество байтов и мы из этой мапы берём только ключи. -
toSet()
- простой способ избавиться от дубликатов. -
println("User languages: $userLngs")
- выводим на экран результат работы
В этом туториале мы рассмотрели как Restler позволяет написать программу всего в 34 строки кода которая, может асинхронно получать данные от API GitHub без единой строчки бойлерплейт связанного с выполнением HTTP-запросов или параллельным программированием.