В этой статье мы поговорим о новой концепции в готовящемся к выходу Spring Framework 5 которая называется функциональный веб-фреймворк и посмотрим, как она может помочь при разработке легковесных приложений и микросервисов.


Вы, возможно, удивлены видеть Spring и микрофреймворк в одном предложении. Но все верно, Spring вполне может стать вашим следующим Java микрофреймворком. Чтобы избежать недоразумений, давайте определим, что им имеем в виду под микро:


  • Лаконичный — минимум бойлерплейта, минимум настройки
  • Простой — без магии
  • Простой в деплойменте — один артефакт для деплоймента
  • Простой в запуске — без дополнительных зависимостей
  • Легковесный — минимальное использование памяти / CPU
  • Неблокирующий — для написания конкуррентных неблокирующих приложений

Несмотря на то, что некоторые из этих пунктов актуальны при использовании Spring Boot, он сам по себе добавляет дополнительную магию поверх самого Spring Framework. Даже такие базовые аннотации, как @Controller не совсем прямолинейны, что уж говорить про авто-конфигурации и сканирование компонентов. В общем-то, для крупномасштабных приложений, просто незаменимо то, что Spring берет на себя заботу о DI, роутинге, конфигурации и т.п. Однако, в мире микросервисов, где приложения это просто шестеренки в одной большой машине, вся мощь Spring Boot может быть немного лишней.


Для решения этой проблемы, команда Spring представила новую фичу, которая называется функциональный веб-фреймворк — и именно о ней мы и будем говорить. В целом, это часть большего под-проекта Spring WebFlux, который раньше назывался Spring Reactive Web.


Для начала, давайте вернемся к основам и посмотрим, что такое веб-приложение и какие компоненты мы ожидаем иметь в нем. Несомненно, есть базовая вещь — веб-сервер. Чтобы избежать ручной обработки запросов и вызова методов приложения, нам пригодится роутер. И, наконец, нам нужен обработчик — кусок кода, который принимает запрос и отдает ответ. По сути, это все, что нужно! И именно эти компоненты предоставляет функциональный веб-фреймворк Spring, убирая всю магию и фокусируясь на фундаментальном минимуме. Отмечу, что это вовсе не значит, что Spring резко меняет направление и уходит от Spring MVC, функциональный веб просто дает еще одну возможность создавать приложения на Spring.


Обработчик


Давайте рассмотрим пример. Для начала, пойдем на Spring Initializr и создадим новый проект используя Spring Boot 2.0 и Reactive Web как единственную зависимость. Теперь мы можем написать наш первый обработчик — функцию которая принимает запрос и отдает ответ.


HandlerFunction hello = new HandlerFunction() {
    @Override
    public Mono handle(ServerRequest request) {
        return ServerResponse.ok().body(fromObject("Hello"));
    }
};

Итак, наш обработчик это просто реализация интерфейса HandlerFunction который принимает параметр request (типа ServerRequest) и возвращает объект типа ServerResponse с текстом "Hello". Spring так же предоставляет удобные билдеры чтобы создать ответ от сервера. В нашем случае, мы используем ok() которые автоматически возвращают HTTP код ответа 200. Чтобы вернуть ответ, нам потребуется еще один хелпер — fromObject, чтобы сформировать ответ из предоставленного объекта.


Мы так же можем сделать код немного более лаконичным и использовать лямбды из Java 8 и т.к. HandlerFunction это интерфейс одного метода (single abstract method interface, SAM), мы можем записать нашу функцию как:


HandlerFunction hello = request -> ServerResponse.ok().body(fromObject("Hello"));

Роутер


Теперь, когда у нас есть хендлер, пора определить роутер. Например, мы хотим вызвать наш обработчик когда URL "/" был вызван с помощью HTTP метода GET. Чтобы этого добиться, определим объект типа RouterFunction который мапит функцию-обработчик, на маршрут:


RouterFunction router = route(GET("/"), hello);

route и GET это статические методы из классов RequestPredicates и RouterFunctions, они позволяют создать так называемую RouterFunction. Такая функция принимает запрос, проверяет, соответствует ли он все предикатам (URL, метод, content type, etc) и вызывает нужную функцию-обработчик. В данном случае, предикат это http метод GET и URL '/', а функция обработчик это hello, которая определена выше.


Веб-сервер


А сейчас пришло время собрать все вместе в единое приложение. Мы используем легковесный и простой сервер Reactive Netty. Чтобы интегрировать наш роутер с веб-сервером, необходимо превратить его в HttpHandler. После этого можно запустить сервер:


HttpServer
    .create("localhost", 8080)
    .newHandler(new ReactorHttpHandlerAdapter(httpHandler))
    .block();

ReactorHttpHandlerAdapter это класс предоставленный Netty, который принимает HttpHandler, остальной код, думаю, не требует пояснений. Мы создаем новые веб-сервер привязанный к хосту localhost и на порту 8080 и предоставляем httpHandler созданный из нашего роутера.


И это все, приложение готово! И его полный код:


public static void main(String[] args)
          throws IOException, LifecycleException, InterruptedException {

    HandlerFunction hello = request -> ServerResponse.ok().body(fromObject("Hello"));

    RouterFunction router = route(GET("/"), hello);

    HttpHandler httpHandler = RouterFunctions.toHttpHandler(router);

    HttpServer
            .create("localhost", 8080)
            .newHandler(new ReactorHttpHandlerAdapter(httpHandler))
            .block();

    Thread.currentThread().join();
}

Последняя строчка нужна только чтобы держать JVM процесс живым, т.к. сам HttpServer его не блокирует. Вы возможно сразу обратите внимание, что приложение стартует мгновенно — там нет ни сканирования компонентов, ни авто-конфигурации.


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


Чтобы запаковать приложение для деплоймента, мы можем воспользоваться преимуществами Maven плагина Spring и просто вызвать


./mvnw package


Эта команда создаст так называемый fat JAR со всеми зависимостями, включенными в JAR. Это файл может быть задеплоен и запущен не имея ничего, кроме установленной JRE


java -jar target/functional-web-0.0.1-SNAPSHOT.jar


Так же, если мы проверим использование памяти приложением, то увидим, что оно держится примерно в районе 32 Мб — 22 Мб использовано на metaspace (классы) и около 10 Мб занято непосредственно в куче. Разумеется, наше приложение ничего и не делает — но тем не менее, это просто показатель, что фреймворк и рантайм сами по себе требуют минимум системных ресурсов.


Поддержка JSON


В нашем примере, мы возвращали строку, но вернуть JSON ответ так же просто. Давайте расширим наше приложение новым endpoint-ом, который вернет JSON. Наша модель будет очень простой — всего одно строковое поле под названием name. Чтобы избежать ненужного boilerplate кода, мы воспользуемся фичей из проекта Lombok, аннотацией @Data. Наличие этой аннотации автоматически создаст геттеры, сеттеры, методы equals и hashCode, так что нам не придется релизовывать их вручную.


@Data
class Hello {
    private final String name;
}

Теперь, нам нужно расширить наш роутер чтобы вернуть JSON ответ при обращении к URL /json. Это можно сделать вызвав andRoute(...) метод на существующем роуте. Также, давайте вынесем код роутер в отдельную функцию, чтобы отделить его от кода приложения и позволить использовать позже в тестах.


static RouterFunction getRouter() {
    HandlerFunction hello = request -> ok().body(fromObject("Hello"));

    return
        route(
            GET("/"), hello)
        .andRoute(
            GET("/json"), req ->
                ok()
                .contentType(APPLICATION_JSON)
                .body(fromObject(new Hello("world")));
}

После перезапуска, приложение вернет { "name": "world" } при обращении к URL /json при запросе контента с типом application/json.


Контекст приложения


Вы, возможно, заметили, что мы не определили контекст приложения — он нам просто не нужен! Несмотря на то, что мы можем объявить RouterFunction как бин (bean) в контексте Spring WebFlux приложения, и он точно так же будет обрабатывать запросы на определенные URL, роутер можно запустить просто поверх Netty Server чтобы создавать простые и легковесные JSON сервисы.


Тестирование


Для тестирования реактивных приложений, Spring предоставляет новый клиент под названием WebTestClient (подобно MockMvc). Его можно создать для существующего контекста приложения, но так же можно определить его и для RouterFunction.


public class FunctionalWebApplicationTests {

    private final WebTestClient webTestClient =
            WebTestClient
                .bindToRouterFunction(
                    FunctionalWebApplication.getRouter())
                .build();

    @Test
    public void indexPage_WhenRequested_SaysHello() {
        webTestClient.get().uri("/").exchange()
                .expectStatus().is2xxSuccessful()
                .expectBody(String.class)
                .isEqualTo("Hello");
    }

    @Test
    public void jsonPage_WhenRequested_SaysHello() {
        webTestClient.get().uri("/json").exchange()
                .expectStatus().is2xxSuccessful()
                .expectHeader().contentType(APPLICATION_JSON)
                .expectBody(Hello.class)
                .isEqualTo(new Hello("world"));
    }
}

WebTestClient включает ряд assert-ов, которые можно применить к полученному ответу, чтобы провалидировать HTTP код, содержимое ответа, тип ответа и т.п.


В заключение


Spring 5 представляет новую парадигму для разработки маленьких и легковесных microservice-style веб-приложений. Такие приложения могут работать без контекста приложений, автоконфигурации и в целом использовать подход микрофреймворков, когда роутер и функции-обработчики и веб-сервер опеределены явно в теле приложения.


Код


Доступен на GitHub


Ссылки



От переводчика


Я так же являюсь и автором оригинальной статьи, так что вопросы можно задавать в комментариях.

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


  1. vlanko
    10.09.2017 22:53
    +2

    Сюда б еще сравнения легковесности с другими.


  1. Hixon10
    10.09.2017 23:10
    +1

    Будет интересно, как в конечном итоге 5-й спринг заинтегрируется с 9-й джавой в разрезе java.util.concurrent.Flow. Много статей пивотал пишет про это, но пока не было релизов — не ясно.


    1. alek_sys Автор
      11.09.2017 12:42

      Spring использует Project Reactor для реактивности, а Реактор реализует стандартные API Reactive Streams из Java 9 (именно java.util.concurrent.Flow). Т.е. Java 9 предоставляет API, Реактор реализацию, Спринг использует Реактор и таким образом интегрируется с Java 9.


  1. ExplosiveZ
    11.09.2017 00:49
    -4

    NodeJS какой-то, не представляю, во что это может превратиться в большом проекте.

    Я вот например начал знакомство со спринг и закончил почти сразу же. Какие-то конфигураторы для конфигураторов и ко всему этому фреймворк для запуска. Жуть какая-то, spring превратился в огромный комбайн и переплюнул java ee.
    Сейчас например остановился на JSF. Для сайтов уровня hello world — самое то. Что-то крупнее hello world еще не пробовал писать. Может в этом случае я выиграл бы от такого комбайна как spring?


    1. AndreyRubankov
      11.09.2017 09:38

      На самом деле, это именно то, что нужно.
      Бекенд в чистом видел – это API сервер, который ничего не должен знать про фронтенд. Сейчас все больше и больше мобильных клиентов, которым нужно отдавать только данные. Веб-сайты постепенно отходят на задний план.

      Да серверный ренден еще остается, но для этого будут нужны «тяжеловесные» фреймворки. Для остальних же, нужны крайне легковесные, которым хватало бы каких-нибудь дешевых t1.micro.

      Я сам не сильно люблю Spring, но Spring WebFlux – это совсем другое дело, если все так, как выглядит, то это замечательный инструмент (пока они не превратят его в Spring WebFlux Boot).


      1. alek_sys Автор
        11.09.2017 12:35

        Думаю, не превратят. Он и планируется как легковесная микрофреймворковая штука для желающих. Хотя точно так же он может работать и в рамках обычного Spring приложения, просто @Controller может вернуть не ответ, а Flux / Mono в качестве ответа — и все, реактивный контроллер готов. И даже можно объявить бин с типом RouterFunction и привязать роутер к существующему приложению, а @Controller аннотации не использовать вообще. Более того, чтобы оценить масштаб — по-умолчанию, Spring WebFlux даже не тащит зависимость на Spring MVC, они прям вообще разные.


        1. AndreyRubankov
          11.09.2017 12:49

          Думаю, не превратят.
          Это же Spring, не стоит их недооценивать :-)

          Кстати, попробовал потыкать webflux из примера на ApacheBenchmark – он вообще не отвечает на запросы. Ну а по консоли видно, что это сейчас на стадии альфы или в лучшем случае беты.


          1. IoannGolovko
            11.09.2017 14:42

            Webflux вполне вменяемо функционирует. На запросы отвечает, фильтры работают, конвертеры работают. Можно линк на пример?


            1. AndreyRubankov
              11.09.2017 14:53

              Я брал код из статьи: github.com/alek-sys/spring-functional-microframework

              и пробовал выполнить:
              ab -k -c 10 -n 1000 «localhost:8080/»
              ab -k -c 1 -n 1 «localhost:8080/»

              Оба зависают.
              А через браузер отлично отдает ответ.


              1. alek_sys Автор
                11.09.2017 15:46

                Интересно, ab действительно не работает, а wrk вполне себе.


                wrk -t 10 -c 1000 -d 10s http://localhost:8080/


          1. alek_sys Автор
            11.09.2017 14:44
            +1

            Команда Spring это одна из самых прагматичных и профессиональных, которые я встречал, так что не стоит их недооценивать ;)


            Текущий статус Release Candidate, релиз будет до конца этого года.


          1. vlanko
            11.09.2017 15:30

            та же фигня. Ни стрингом, ни ЖСОНом


    1. alek_sys Автор
      11.09.2017 12:39
      +2

      Собственно, в этом и замысел — если приложение простое и экспортирует только пару эндпоинтов, то можно его написать супер просто, без магии вообще, но оставаться в рамках одного фреймворка.


      Комбайн как Spring рулит когда в приложении много разных аспектов, типа интеграции, облачных штук, безопасности. В общем случае (не в крайних) их проще и быстрее сделать на Спринге, чем писать самому. Если приложение просто отдает html / json по данным из базы — его теперь можно сделать очень просто (хотя и раньше было просто, если уж откровенно).


  1. pmcode
    11.09.2017 08:27

    Что-то даже в официальных доках спринга очень мало информации. Где бы как почитать как в этот реактив прикручивается DI и декларативные транзакции?


    1. alek_sys Автор
      11.09.2017 12:32

      Например, здесь https://docs.spring.io/spring/docs/5.0.0.RC3/spring-framework-reference/web.html#web-reactive


      Если же нужно просто взять Spring (Boot) как есть с его DI и транзакциями, и прикрутить роутер, все делается просто регистрацией бина в конфигурации:


      @Bean
      public RounterFunction routerFunction() {
        return route(...)
      }


      1. pmcode
        11.09.2017 13:36

        Т.е. это старый-добрый спринг, просто они вместо слоя контроллеров реактив прикрутили? Другими словами spring-web-flux — это замена spring-webmvc а все остальное как и было?


        1. alek_sys Автор
          11.09.2017 14:50

          Есть несколько вариантов использования реактивного веба в Spring:


          • Без Application context, DI, конфигураций и т.п. — как описано в статье
          • Используя @Controller аннотации и возвращая Flux<T> или Mono<T> из методов контроллера
          • Не используя контроллеры, определить бин типа RouterFunction с явно заданной таблицей машрутов и хендлерами.

          Да, все верно, это старый-добрый Спринг, WebFlux это новая парадигма для веб-разработки, даже не замена MVC, а дополнение. А в качестве приятного бонуса, теперь на Spring можно писать microframework-style приложения a la Sparkframework, Ninja etc. без контекста приложений, DI и автоконфигураций.


  1. i_visseri
    11.09.2017 12:29

    Выглядит пока все очень классно, просто и удобочитаемо. Захотелось попробовать. Автору спасибо за статью)


  1. kanarisru
    11.09.2017 12:29
    -2

    Раньше порталы писались без всяких фреймворков.
    Все говорят о каком то облегчении жизни, типа так быстрее.
    Два месяца и 1 человек — готовый портал без фреймворка с нуля.
    Неужели сейчас делается еще быстрее?


    1. alix_ginger
      11.09.2017 13:25
      +2

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


  1. Scf
    11.09.2017 16:59

    Хорошая попытка, Спринг, но нет, это плохая попытка. Получается, для того, чтобы вывести JSON вместо строки, нужно указать content-type:


    .contentType(APPLICATION_JSON)

    А дальше начинается счастье и проклятие спринга — спринговая магия. Какой сериализатор мы используем? Как его настроить? А если я хочу выдавать не json с content-type application/json? А если я хочу свой сериализатор вкрутить? И вообще зачем веб-серверу зависеть от спринга?


    21 век на дворе, сейчас всем нужны:


    • http server, где спрингом бы и не пахло
    • сериализатор json, где спрингом бы и не пахло
    • возможность скомпоновать 1 и 2, вне зависмости от релиализации.


    1. esin
      11.09.2017 17:14

      Думаю, тут магии пока нет. Следующая строка (как я предполагаю) возвращает сериализованный ответ:

      .body(fromObject(new Hello("world")))

      И там уже (как я очень надеюсь) можно использовать любой сериализатор.


    1. IoannGolovko
      11.09.2017 20:06

      Content-type указывать необязательно, достаточно передать какой-нибудь отличный от базовых типов объект, например ...body(fromObject(myDto)). По факту на выходе автоматом получится json. А вот берет ли на себя webflux корректную расстановку хидеров; это нужно проверить.


    1. Borz
      11.09.2017 20:23

      А если я хочу свой сериализатор вкрутить?

      берите и вкручивайте и даже свой ContentType можете в виде строки передать — там никакой магии — сплошная документация


    1. sshikov
      11.09.2017 20:38

      http server, где спрингом бы и не пахло

      Вообще говоря, это неоднозначное желание. Т.е. я могу вас понять, и представить такие запросы. Но я также могу представить свои запросы, где скажем сервлеты (ну или в более общем виде обработчики http запросов) это spring бины, и я хочу, чтобы сервер знал про спринговые контексты, и умел подключать оттуда обработчиков. И вообще говоря — не только спринговые.


      Иными словами — как вы в общем виде компоновать-то 1 и 2 собираетесь, если у вас нет контейнера, где лежит сериализатор, http сервер, и что-нибудь еще?


      1. AndreyRubankov
        11.09.2017 22:33

        До DI с этим без особых усилий жили. Да, кода было больше, но код был явный.

        Что мешает написать:

        HttpServer
         .withCodecRegistry(
           new CodecRegistry()
             .bind(APPLICATION_JSON, new JsonCodec( ... ))
             .bind(APPLICATION_XML, new XmlCodec( ... ))
         )
         .withRouteRegistry(
           new RouteRegistry()
              .bind(GET("/"), (r) -> Response.ok());
         )
         ...
         .run(8080);
        Или что-то подобное…
        Более того, чтобы файл не был на тысячи строк, можно этот конфиг разбить на несколько: RouteConfig, ErrorHandlerConfig, CodecConfig, etc.

        Для проектов, у которых меньше 50-100 эндпоинтов это будет вполне приемлемый подход.
        Это не будет слишком избыточным кодом или страшным бойлерплейтом, это будет вполне удобный и красивый конфиг.


        1. Scf
          11.09.2017 22:49

          Лямбды и чистые функции позвляют значительно улучшать конфиги. В этом плане инновационные языки — Scala и Javascript. Вкратце, идея в том, что любой сетевой обработчик, фильтр и собственно сервер — это функция Request -> Response. Т.е. приложение можно собирать функциональной композицией индивидуальных обработчиков, фильтров и роутеров.


          1. sshikov
            12.09.2017 19:56

            Ну да. И что вы хотели этим сказать, что вам DI не нужно?


            1. Scf
              12.09.2017 20:17

              DI нужен, не нужна конфигурация через данные. Конфигурация кодом — лучше.


              1. sshikov
                12.09.2017 20:29

                Конфигурация кодом как правило меняется только при перекомпиляции. Это не плохо и не хорошо — но бывает что нужно и в рантайме. Ну т.е. функциональная композиция — это хорошо, но иногда хочется собрать без компиляции.


              1. alek_sys Автор
                12.09.2017 22:24

                Конфигурация DI кодом уже есть в Spring 5 и называется Programmatic Bean Registration и даже есть удобный Kotlin DSL для него.


        1. sshikov
          12.09.2017 19:55

          Это нормально, но я не могу сказать, что это однозначно лучше. Или хуже. Во-первых, кроме явного кода вы выбрали инициализацию статическую, где все определяется при компиляции. Где-то это хорошо, где-то не факт. Это не лучше и не хуже — это разные потребности.


          Скажем прямо — у меня и так не видно, чтобы скажем http server где-то зависел от спринга. От вот этих самых сериализаторов и обработчиков — да, зависит. Они возможно даже инжектятся спрингом — только сервер про спринг (который DI) не знает.


          1. AndreyRubankov
            12.09.2017 21:22

            Во-первых, кроме явного кода вы выбрали инициализацию статическую, где все определяется при компиляции.
            Никто не мешает использовать конфиги – поменял конфиг и рестартовал и погнали.

            Спринг крутой, он позволяет быстро написать то, что будет работать.
            Но вот я, к примеру, всегда хотел выкинуть из спринга все ненужное мне в конкретном проекте. У меня API сервер, который умеет только JSON и ничего больше. Но спринг такой классный, что подсовывает мне дополнительно кучу кода, который нужен для поддержки xml, html, а еще загружает классы для AOP и кучу своих связок для других вещей, которые нафик не нужны. А ведь RAM то ограничен.

            Но это уже offtop с моей стороны пошел.


            1. sshikov
              12.09.2017 21:38

              Знаете, у меня в прошлом проекте были "аналитики", которые тоже говорили: "А чо, поменял конфиги, рестартовал и погнали". А то что на входе непрерывный поток данных (финансовых), который можно потерять пока рестартуем — это им в голову не приходило. Всякие проекты бывают, попросту говоря.


              Что же до "загружает всякие классы для AOP" — ну так вообще говоря, это не является обязательным.


              Единственное что меня лично в этом вопросе расстраивает — это тот факт, что не всегда очевидно, зачем конкретно тут вот появился AOP (или что-то еще). Хотя если подумать, обычно ответ недалеко — включил аннотацию @Cacheable для кеширования результатов метода — так оно что, святым духом что-ли будет вызовы метода перехватывать? Нет же, будет либо Proxy, либо AOP, со всеми вытекающими. Ну т.е., нужно иногда понимать, как оно там внутри, что за неонки.


              1. AndreyRubankov
                12.09.2017 23:18

                Знаете, у меня в прошлом проекте были «аналитики», которые тоже говорили: «А чо, поменял конфиги, рестартовал и погнали». А то что на входе непрерывный поток данных (финансовых), который можно потерять пока рестартуем — это им в голову не приходило. Всякие проекты бывают, попросту говоря.
                Инструмент выбирается под проект. Я не говорил, что Spring – это абсолютное зло, он хорош для много. Я лишь сказал, что для большинства задач, с которыми я сталкивался в своей карьере он излишне «жирный».

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


            1. alek_sys Автор
              12.09.2017 22:33

              Только в битве за RAM не забывайте, что вообще весь Spring сам по себе, занимает не так уж и много памяти — до 32Мб. Есть интересное исследование на эту тему от Дейва Сайера.


  1. pilot911
    12.09.2017 22:25

    У нас в проекте используется Spring для REST API. Интересно мнение профи — будут ли они переписывать REST на Spring WebFlux?


    1. alek_sys Автор
      12.09.2017 22:27

      Если "они" это Spring, а не команда вашего проекта — то нет, не будут. Более того, REST и WebFlux они ортогональны, т.е. REST не надо "переписывать" на WebFlux, REST можно запускать поверх WebFlux, но не обязательно. Все старые парадигмы остаются.


  1. Shpital
    12.09.2017 22:25

    Пока для микросервисов использую Dropwizard и что-то пока не вижу особых стимулов для переезда на модную реактивщину. Кто нибудь сравнивал производительность?


    1. alek_sys Автор
      12.09.2017 22:29

      Реактивные подходы они скорее не для производительности (она теоретически может быть даже хуже для отдельных запросов), а для конкурентности — т.е. обработать больше клиентов одновременно.


  1. stasmarkin
    12.09.2017 22:25

    Я так понимаю, что текущий интерфейс хэндлера не предполагает асинхронных задач. Тогда что неблокирующего в этом вебсервере?


    1. alek_sys Автор
      12.09.2017 22:26

      Предполагает. Это не очень видно, но он возвращает не ответ типа T, а Mono<T> или Flux<T> — реактивный тип из Project Reactor.