Введение

В среду, 9 декабря 2020 года, состоится официальный релиз JetBrains Space. Space - это среда для команд, цель которой - обеспечить единое окно для управления версиями, ведения блогов, чатов, проверки кода и т. Д.

В Инстил мы были первыми последователями Space. Наша обучающая команда представляла на KotlinConf in Copenhagen и присутствовала на мероприятии, посвященном запуску. После этого мы использовали его с большим эффектом, сначала для внутреннего управления кодовыми базами для новых курсов, а затем в рамках нашего перехода на виртуальное обучение во время пандемии коронавируса.

У Space есть собственный HTTP API, который можно использовать для выполнения операций CRUD с различными ресурсами, которые вы найдете в экземпляре. К ним относятся профили, проекты, публикации, чаты и календари. Чтобы использовать эти конечные точки, вы обычно создаете и регистрируете свое собственное приложение, но для начала работы администратор пространства может использовать встроенную площадку HTTP API Playground.

Вот пример использования API Playground для получения профилей всех зарегистрированных пользователей в экземпляре.

Задача, которую необходимо выполнить

Чтобы отпраздновать официальный запуск Space, позвольте мне показать вам, как создать и зарегистрировать приложение, которое может получать доступ к тем же конечным точкам извне. Следуя теме современных технологий, мы будем использовать язык Kotlin и Reactive Spring (через Project Reactor и WebFlux).

Завершенный код доступен в этом репозитории BitBucket Git, но здесь я проведу вас через настройку и реализацию с нуля.

Начинаем

Первый этап - создание свежего проекта с Spring Initializr. Мы выбираем Kotlin в качестве языка и Gradle в качестве инструмента сборки.

В окне зависимостей мы выбираем Spring Reactive Web, чтобы получить WebClient. Мы будем использовать это для доступа к Space API. Но прежде чем мы сможем это сделать, нам нужно будет войти в систему через OAuth, поэтому мы также включаем OAuth2 Client.

Когда мы нажимаем кнопку Создать, браузер загружает ZIP-файл, содержащий предварительно настроенный файл сборки и некоторый образец кода. Мы можем распаковать этот архив и открыть полученную папку в IntelliJ.

На скриншоте ниже я изменил стандартный код, созданный Initializr, чтобы создать базовое консольное приложение. Класс SpaceApp содержит единственный метод поставщика bean-компонентов, который, в свою очередь, создает и возвращает CommandLineRunner. Spring автоматически обнаружит и запустит это, чтобы получить наш вывод «Hello Spring Boot».

Поскольку мы включили фреймворк WebFlux, Spring автоматически запустит экземпляр сервера Netty. Мы должны явно отключить это в файле конфигурации application.properties (как показано выше).

Регистрация нашего приложения

Теперь у нас есть наше базовое приложение, работающее. Прежде чем мы продолжим, нам нужно зарегистрировать его в Space.

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

Затем нам нужно указать, какой поток OAuth мы хотим использовать для аутентификации. Поскольку нашему приложению не нужно поддерживать вход через третьих лиц, мы можем выбрать поток учетных данных клиента.

Обратите внимание, что Space назначил нашему приложению Client ID и Client Secret. Это то, что нам понадобится в нашем коде Kotlin / Spring для доступа к HTTP API. Обычно они должны быть защищены, но для целей демонстрации мы поместим их прямо в файл свойств.

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

Добавление регистрационных данных в приложение

Вернувшись в IntelliJ, нам нужно добавить только что обнаруженные настройки в файл application.properties. В приведенном ниже листинге я заменил фактическое название компании на megacorp и ввел фиктивные значения для идентификатора клиента (1234) и секретного (5678) .

space.base.url = https: //megacorp.jetbrains.space
space.api.url = $ {space.base.url} / api / http

spring.security.oauth2 .client.registration.HELLO.authorization-grant-type = client_credentials
spring.security.oauth2.client.registration.HELLO.client-id = 1234
spring.security.oauth2.client.registration.HELLO .client-secret = 5678
spring.security.oauth2.client.registration.HELLO.scope = **
spring.security.oauth2.client.provider.HELLO.token-uri = $ {пробел. base.url} / oauth / токен

Обратите внимание, что имена свойств определяются модулем Spring OAuth2. Но предпоследняя часть имени каждого свойства - это идентификатор, который мы можем использовать в нашем коде, чтобы получить группу связанных настроек. В этом случае идентификатор - HELLO.

Завершение кода

Заключительный этап процесса - тот, который мы все любим. Кодировка :-)

Мы начинаем с создания типов Kotlin для представления данных, которые будет возвращать Space. Краткий синтаксис Kotlin делает его намного короче, чем эквивалентный код на более старых объектно-ориентированных языках.

class Name(var firstName: String,
           var lastName: String) {
    override fun toString() = "$firstName $lastName"
}

class Profile(var id: String,
              var username: String,
              var name: Name) {
    override fun toString() = "$id is $name with username $username"
}

class AllProfilesResponse(var next: String,
                          var totalCount: String,
                          var data: List<Profile>)

Следующая часть самая сложная. Нам нужно создать метод Bean Provider, который будет производить для нас объект WebClient. Обычно это было бы тривиально, но здесь мы должны использовать поддержку OAuth2 в Spring, чтобы гарантировать, что клиент автоматически войдет в нас.

Вот как это делается. Для ясности я поместил этот метод в отдельный класс Configuration:

@Configuration
class SpaceAppConfig {
    @Bean
    fun oauthWebClient(
          clientRegistrations: ReactiveClientRegistrationRepository,
          @Value("\${space.api.url}") baseUrl: String
        ): WebClient {

        val clientService = InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations)
        val manager = AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService)
        val oauth = ServerOAuth2AuthorizedClientExchangeFilterFunction(manager)
        oauth.setDefaultClientRegistrationId("HELLO")

        return WebClient.builder()
                .filter(oauth)
                .baseUrl(baseUrl)
                .build()
    }
}

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

  • Мы используем аннотацию Value для передачи URL-адреса экземпляра Space, определенного в файле application.properties. Тем самым избегая дублирования.
  • Мы используем идентификатор HELLO, чтобы указать, какие настройки мы хотим, чтобы модуль OAuth2 использовал из файла конфигурации. В реальном решении нам может потребоваться аутентификация со многими удаленными службами, поэтому нам нужен способ поддержки произвольных групп свойств.

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

fun retrieveSpaceProfiles(webClient: WebClient) = webClient
        .get()
        .uri("/team-directory/profiles")
        .retrieve()
        .bodyToFlux(AllProfilesResponse::class.java)
        .flatMap { Flux.fromIterable(it.data) }

Как видите, этот код использует реактивные потоки Springs, более известные как Flux. Тип WebClient ориентирован на будущее (без каламбура), когда все API будут реактивными. Это ожидалось, но пока не реализовано. В частности, WebClient предполагает, что сервер захочет отправлять нам элементы постепенно, через такой протокол, как SSE, WebSockets или RSocket.

Поскольку в данном случае это не так, нам необходимо:

  • Отправьте на сервер стандартный GET-запрос
  • Маршалировать данные, возвращаемые в AllProfilesResponse
  • Преобразуйте список профилей внутри этого в Flux

Наконец, нам нужно расширить наш консольный метод следующим образом:

@Bean
fun console(webClient: WebClient) = CommandLineRunner {

    val header = Mono.just("Details of all the profiles:")
    val profiles = retrieveSpaceProfiles(webClient).map { "\t $it" }
    val footer = Mono.just("All done - hit return to exit")

    val values = header
            .concatWith(profiles)
            .concatWith(footer)

    values.subscribe(::println)

    //Because this is a console app we need to keep the
    // main thread alive. Otherwise Spring would exit.
    readLine()
}

Как видите, теперь метод поставщика компонентов принимает WebClient в качестве параметра. Это будет создано и передано через внедрение зависимостей. Нам нужно помнить, что действия в WebClient будут происходить асинхронно с основным потоком. Итак, войдя в мир стримов, лучше продолжать.

Мы создаем единый поток элементов (также известный как Mono) для первого и последнего сообщений, которые хотим отправить, а затем объединяем их с потоком профилей. Затем на объединенный поток можно подписаться. Нам нужно только указать обработчик событий для завершенных значений. В случае нашей простой демонстрации это просто println.

Поскольку это консольное приложение, а Spring ApplicationContext управляется в основном потоке, нам нужно поддерживать этот поток работающим до тех пор, пока не будут отображены все результаты. Очевидно, этого не было бы, если бы этот код выполнялся в контроллере WebFlux, выполняющемся в Netty.

Вот готовый код, работающий с образцом экземпляра Spring:

Выводы

Надеюсь, это руководство показало вам, что войти в экземпляр Space и удаленно управлять им не так уж сложно. Это особенно верно, когда мы используем Kotlin для удаления ненужных шаблонов и упрощения реактивного подключения. В следующем руководстве я покажу, как мы используем API в Instil для настройки пользовательских экземпляров Space для курсов, семинаров и хакатонов.