Функциональные подходы к разработке в Spring становятся все более популярными благодаря своей гибкости и лаконичности. В новой статье от эксперта сообщества Spring АйО, Михаила Поливахи, рассматривается, как можно эффективно определять HTTP-эндпоинты с использованием Spring MVC/WebFlux, применяя функциональный стиль программирования на языке Kotlin. Аналогичный подход можно реализовать и на Java, хотя использование Kotlin позволяет существенно упростить код.


1. Введение

В этом руководстве мы рассмотрим функциональный способ определения наших эндпоинтов в spring-webmvc и spring-webflux. Так как функциональные эндпоинты наиболее полезны и кратки при работе с Kotlin DSL, мы представим наши примеры на языке Kotlin.

Однако, как мы увидим позже, использование Kotlin не является обязательным, в общем, аналогичный функциональный подход возможен и на Java.

2. Зависимости

Начнем с зависимостей, которые нам понадобятся для работы. Самые последние версии можно найти на Maven Central. В любом случае, нам нужна стандартная библиотека Kotlin, это должно быть очевидно. Теперь, если мы хотим работать с spring-webmvc, нам понадобится spring-boot-starter-web:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.3.4</version>
</dependency>

Мы используем стартер здесь вместо одиночной зависимости spring-webmvc, чтобы получить возможности spring-boot через транзитивный spring-boot-starter. Также, если мы решим работать с spring-webflux, нам понадобятся spring-boot-starter-webflux и несколько других зависимостей для работы с Kotlin-корутинами:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>3.3.4</version>
</dependency>
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-core</artifactId>
    <version>1.9.0</version>
</dependency>
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-reactor</artifactId>
    <version>1.9.0</version>
</dependency>

Теперь, имея на руках зависимости, мы наконец можем начать. Но сначала нам нужно понять, как вообще работают функциональные определения бинов в Spring.

3.1. Функциональная регистрация бинов

Существует множество способов создания бинов в spring-framework. Самые известные примеры — это объявления бинов в XML, конфигурации на Java и настройка через аннотации. Но есть и другой способ регистрировать бины в spring — функциональный. Давайте посмотрим на пример:

@SpringBootApplication
open class Application

fun main(vararg args: String) {
    runApplication(*args) {
        addInitializers(
            beans {
                bean(
                  name = "functionallyDeclaredBean",
                  scope = BeanDefinitionDsl.Scope.SINGLETON,
                  isLazyInit = false,
                  isPrimary = false,
                  function = {
                      BigDecimal(1.0)
                  }
                )
            }
        )
    }
}

Здесь мы запускаем Spring Boot приложение через функцию runApplication() и указываем в качестве источника конфигурации класс Application. Эта функция фактически принимает два параметра — аргументы процесса Java (args) и extension функцию с SpringApplication в качестве receiver’а, так как последний аргумент runApplication является функцией, ее (функцию) можно передавать вне круглых скобок. Давайте кратко объясним, как работает приведенный выше код.

3.2. DSL-функции в Kotlin

Поскольку последний параметр имеет тип функции, мы можем вынести его за скобки, что мы и сделали в нашем примере. То, что runApplication() имеет параметр функции с конкретным receiver’ом, здесь очень важно. Благодаря этому receiver типу (в нашем случае SpringApplication является также receiver типом), мы можем использовать функцию addInitializers() внутри тела функции. Также extension функции с receiver’ом на самом деле являются ключевой особенностью Kotlin, которая позволяет не только так кратко регистрировать бины, как мы видели выше, но и использовать DSL-билдеры в Kotlin в целом.

Благодаря этим двум особенностям в Kotlin —extension функциям с receiver’ом и передаче функций за пределы скобок — возможен такой DSL, как выше. Теперь давайте продолжим изучать пример.

Итак, если упростить — в приведенном выше коде есть цепочка вложенных лямбда-функций с различными receiver’ами. Эти лямбды создают набор BeanDefinition, в нашем случае только один BeanDefinition для создания бина типа BigDecimal, которое будет зарегистрировано через ApplicationContextInizilizer.

4. Функциональные эндпоинты с MVC

Но приведенный выше код регистрирует обычный бин в контексте и не имеет никакого отношения к spring-webmvc или spring-webflux. Чтобы зарегистрировать эндпоинт, который будет обрабатывать HTTP-запросы, нам нужно вызвать другую лямбду внутри функции bean — router:

beans {
    bean {
        router {
            GET("/endpoint/{country}") { it : ServlerRequest ->
                ServerResponse.ok().body(
                  mapOf(
                    "name" to it.param("name"),
                    "age" to it.headers().header("X-age")[0],
                    "country" to it.pathVariable("country")
                  )
                )
            }
        }
    }
}

Давайте рассмотрим этот пример более подробно. Хотя лямбды с одним параметром могут обращаться к параметру через it, мы явно указываем параметр функции router вместе с его типом для демонстрационных целей. Параметр имеет тип ServerRequest, который представляет собой абстракцию над HTTP-запросом клиента. Мы можем получить любую информацию из запроса для его обработки, как в примере выше — получая параметр запроса, заголовок запроса или переменную пути.

Этот подход очень похож на создание RestController с одним методом, аннотированным @GetMapping:

@RestController
class RegularController {
    @GetMapping(path = ["/endpoint/{country}"])
    fun getPerson(
      @RequestParam name: String,
      @RequestHeader(name = "X-age") age: String,
      @PathVariable country: String
    ): Map {
        return mapOf(
          "name" to name,
          "age" to age,
          "country" to country
        )
    }
}

В общем и целом, эти два подхода почти идентичны, например, HTTP-фильтры для spring-security будут работать в обоих случаях одинаково.

5. Истоки функциональных эндпоинтов

Важно понимать, что функция router DSL, приведенная выше, на самом деле является всего лишь удобной абстракцией над RouterFunction API. Этот API существует как для модулей spring-webmvc, так и для spring-webflux. Это означает, что любой код, использующий router function DSL, также может использовать RouterFunction напрямую:

@Bean
open fun configure() {
    RouterFunctions.route()
      .GET("/endpoint/{country}") {
          ServerResponse.ok().body(
            mapOf(
              "name" to it.param("name"),
              "age" to it.headers().header("X-age")[0],
              "country" to it.pathVariable("country")
            )
          )
      }
    .build()
}

Это будет полностью идентично использованию функции router DSL. Обратите внимание, что мы не добавляем никаких инициализаторов в контекст. Это сделано намеренно, чтобы подчеркнуть, что в общем-то не имеет значения, как мы регистрируем наши RouterFunction бины в контексте — через инициализатор контекста или Java Config.

6. Функциональные эндпоинты в Spring WebFlux

Как уже было сказано, spring-webflux имеет функциональный подход к написанию эндпоинтов. Подобно spring-webmvc, мы можем либо использовать RouterFunction API напрямую, либо использовать абстракцию router function DSL. Давайте быстро рассмотрим прямое использование RouterFunction DSL:

@Bean
open fun configure(): RouterFunction {
    return RouterFunctions.route()
      .GET("/users/{id}") {
          ServerResponse
            .ok()
            .body(usersRepository.findUserById(it.pathVariable("id").toLong()))
      }
      .POST("/create") {
          usersRepository.createUsers(it.bodyToMono(User::class.java))
          return@POST ServerResponse
            .ok()
            .build()
      }
      .build()
}

Это довольно похоже на spring-webmvc и должно быть достаточно прямолинейным. Аналог DSL для router function будет выглядеть так:

@Bean
open fun endpoints() = router {
    GET("/users/{id}") {
        ServerResponse
          .ok()
          .body(usersRepository.findUserById(it.pathVariable("id").toLong()))
    }
    POST("/create") {
        usersRepository.createUsers(it.bodyToMono(User::class.java))
        return@POST ServerResponse
          .ok()
          .build()
    }
}

Таким образом, этот подход с router function более лаконичен в Kotlin, просто потому что мы можем использовать его через декларативный DSL-стиль. Но, как теперь ясно, под капотом мы можем делать абсолютно то же самое  на Java.

7. Kotlin-корутины с функциональными эндпоинтами

Наконец, стоит упомянуть, что функциональные эндпоинты Spring также поддерживают корутины Kotlin. Рассмотрим следующий случай:

@Bean
open fun registerForCo() =
    coRouter {
        GET("/users/{id}") {
            val customers : Flow<User> = usersRepository.findUserByIdForCoroutines(
              it.pathVariable("id").toLong()
            )
            ServerResponse.ok().bodyAndAwait(customers)
        }
    }

Здесь мы используем функцию coRouter DSL. Это также абстракция над RouterFunction API, но эта абстракция построена с использованием suspend HandlerFunction. Иными словами, лямбда, которую мы передаем в GET, на самом деле является suspend-функцией, которая, в свою очередь, вызывает метод findUserByIdForCoroutines, который также является suspend-функцией.

Обратите внимание, что возвращаемое значение метода findUserByIdForCoroutines — это Flow. Это важно здесь, так как функция coRouter DSL на самом деле является оберткой над реактивной RouterFunction, а не webmvc. Следовательно, поскольку Flow — это асинхронный холодный поток, который последовательно выдает значения в течение некоторого времени, он в целом сопоставим с Publisher из проекта Reactor. Таким образом, под капотом Spring просто выполняет преобразование Flow в Publisher проекта Reactor, а затем рабочий процесс аналогичен API RouterFunction в WebFlux.

8. Заключение

В этой статье мы обсудили функциональный API для spring-webflux и spring-webmvc. Он основан на RouterFunction API, который сам по себе отличается в зависимости от работы с WebFlux (через DispatcherHandler) и работы с WebMVC (через обычный DispatcherServlet). RouterFunction можно использовать напрямую, и с этим нет проблем, но при работе с Kotlin есть более лаконичный и элегантный способ работать с API RouterFunction — через функции router DSL. Это всего лишь абстракция над RouterFunction, возможная благодаря extension-функциям Kotlin с receiver’ом и top level функциям. Также есть возможность работать с корутинами и Kotlin DSL. DSL для корутин построен поверх RouterFunction WebFlux, поэтому для работы с ним нам нужен WebFlux.

Как всегда, исходный код для примеров MVC доступен в этом модуле, а исходный код для WebFlux доступен здесь.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Ждем всех, присоединяйтесь

Комментарии (0)