«1. Введение

Весной 5 был представлен WebFlux, новый фреймворк, который позволяет нам создавать веб-приложения с использованием модели реактивного программирования.

В этом уроке мы увидим, как мы можем применить эту модель программирования к функциональным контроллерам в Spring MVC.

2. Настройка Maven

Мы будем использовать Spring Boot для демонстрации новых API.

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

Начиная с Spring 5.2, функциональный подход также будет доступен в среде Spring Web MVC. Как и в случае с модулем WebFlux, RouterFunctions и RouterFunction являются основными абстракциями этого API.

Итак, давайте начнем с импорта зависимости spring-boot-starter-web:

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

3. RouterFunction vs @Controller

В функциональной сфере веб-сервис называется маршрутом, а традиционный концепция @Controller и @RequestMapping заменена RouterFunction.

Чтобы создать наш первый сервис, давайте возьмем сервис на основе аннотаций и посмотрим, как его можно преобразовать в его функциональный эквивалент.

Мы будем использовать пример сервиса, который возвращает все продукты в каталоге продуктов:

@RestController
public class ProductController {

    @RequestMapping("/product")
    public List<Product> productListing() {
        return ps.findAll();
    }
}

Теперь давайте посмотрим на его функциональный эквивалент:

@Bean
public RouterFunction<ServerResponse> productListing(ProductService ps) {
    return route().GET("/product", req -> ok().body(ps.findAll()))
      .build();
}

3.1. Определение маршрута

Следует отметить, что при функциональном подходе метод productListing() возвращает RouterFunction вместо тела ответа. Это определение маршрута, а не выполнение запроса.

RouterFunction включает в себя путь, заголовки запроса, функцию-обработчик, которая будет использоваться для генерации тела ответа и заголовков ответа. Он может содержать один или группу веб-сервисов.

Мы рассмотрим группы веб-сервисов более подробно, когда будем рассматривать вложенные маршруты.

В этом примере мы использовали метод static route() в RouterFunctions для создания RouterFunction. С помощью этого метода можно предоставить все атрибуты запросов и ответов для маршрута.

3.2. Предикаты запроса

В нашем примере мы используем метод GET() для route(), чтобы указать, что это запрос GET с путем, предоставленным в виде строки.

Мы также можем использовать RequestPredicate, когда хотим указать более подробную информацию о запросе.

Например, путь в предыдущем примере также можно указать с помощью RequestPredicate как:

RequestPredicates.path("/product")

Здесь мы использовали статическую утилиту RequestPredicates для создания объекта RequestPredicate.

3.3. Ответ

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

В нашем примере мы используем ok() для добавления HTTP-статуса 200 в заголовки ответа, а затем используем body() для указания тела ответа.

Кроме того, ServerResponse поддерживает построение ответа из пользовательских типов данных с помощью EntityResponse. Мы также можем использовать ModelAndView Spring MVC через RenderingResponse.

3.4. Регистрация маршрута

Далее, давайте зарегистрируем этот маршрут с помощью аннотации @Bean, чтобы добавить его в контекст приложения:

@SpringBootApplication
public class SpringBootMvcFnApplication {

    @Bean
    RouterFunction<ServerResponse> productListing(ProductController pc, ProductService ps) {
        return pc.productListing(ps);
    }
}

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

4. Вложенные маршруты

Довольно часто в приложении имеется множество веб-сервисов, которые также делятся на логические группы на основе функций или объектов. Например, мы можем захотеть, чтобы все услуги, связанные с продуктом, для начала /product.

Давайте добавим еще один путь к существующему пути /product, чтобы найти продукт по его имени:

public RouterFunction<ServerResponse> productSearch(ProductService ps) {
    return route().nest(RequestPredicates.path("/product"), builder -> {
        builder.GET("/name/{name}", req -> ok().body(ps.findByName(req.pathVariable("name"))));
    }).build();
}

При традиционном подходе мы бы добились этого, передав путь @Controller. Однако функциональным эквивалентом для группировки веб-сервисов является метод nest() в route().

Здесь мы начинаем с указания пути, по которому мы хотим сгруппировать новый маршрут, то есть /product. Затем мы используем объект построителя, чтобы добавить маршрут, как в предыдущих примерах.

«Метод nest() обеспечивает слияние маршрутов, добавленных в объект построителя, с основной функцией RouterFunction.

5. Обработка ошибок

Другим распространенным вариантом использования является собственный механизм обработки ошибок. Мы можем использовать метод onError() в route(), чтобы определить собственный обработчик исключений.

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

Давайте добавим обработчик исключений к маршруту поиска продукта, который мы создали ранее, для обработки пользовательского исключения, выдаваемого, когда продукт не найден:

public RouterFunction<ServerResponse> productSearch(ProductService ps) {
    return route()...
      .onError(ProductService.ItemNotFoundException.class,
         (e, req) -> EntityResponse.fromObject(new Error(e.getMessage()))
           .status(HttpStatus.NOT_FOUND)
           .build())
      .build();
}

Метод onError() принимает объект класса Exception и ожидает ответа ServerResponse от функциональная реализация.

Мы использовали EntityResponse, который является подтипом ServerResponse, для создания здесь объекта ответа из пользовательского типа данных Error. Затем мы добавляем статус и используем EntityResponse.build(), который возвращает объект ServerResponse.

6. Фильтры

Обычный способ реализации аутентификации, а также управления сквозными задачами, такими как ведение журналов и аудит, — это использование фильтров. Фильтры используются для принятия решения о продолжении или прекращении обработки запроса.

Давайте рассмотрим пример, когда нам нужен новый маршрут, добавляющий продукт в каталог:

public RouterFunction<ServerResponse> adminFunctions(ProductService ps) {
    return route().POST("/product", req -> ok().body(ps.save(req.body(Product.class))))
      .onError(IllegalArgumentException.class, 
         (e, req) -> EntityResponse.fromObject(new Error(e.getMessage()))
           .status(HttpStatus.BAD_REQUEST)
           .build())
        .build();
}

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

Мы можем сделать это, добавив метод filter() в route():

public RouterFunction<ServerResponse> adminFunctions(ProductService ps) {
   return route().POST("/product", req -> ok().body(ps.save(req.body(Product.class))))
     .filter((req, next) -> authenticate(req) ? next.handle(req) : 
       status(HttpStatus.UNAUTHORIZED).build())
     ....;
}

Здесь, поскольку метод filter() предоставляет запрос, а также следующий обработчик, мы используем его для выполнения простого аутентификация, которая позволяет сохранить продукт в случае успеха или возвращает клиенту НЕАВТОРИЗОВАННУЮ ошибку в случае неудачи.

7. Сквозные проблемы

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

Давайте регистрировать оператор каждый раз, когда приложение находит соответствие для входящего запроса. Мы сделаем это, используя метод before() в route():

@Bean
RouterFunction<ServerResponse> allApplicationRoutes(ProductController pc, ProductService ps) {
    return route()...
      .before(req -> {
          LOG.info("Found a route which matches " + req.uri()
            .getPath());
          return req;
      })
      .build();
}

Точно так же мы можем добавить простой оператор журнала после обработки запроса, используя метод after() в route(): ~~ ~

@Bean
RouterFunction<ServerResponse> allApplicationRoutes(ProductController pc, ProductService ps) {
    return route()...
      .after((req, res) -> {
          if (res.statusCode() == HttpStatus.OK) {
              LOG.info("Finished processing request " + req.uri()
                  .getPath());
          } else {
              LOG.info("There was an error while processing request" + req.uri());
          }
          return res;
      })          
      .build();
    }

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

В этом руководстве мы начали с краткого введения в функциональный подход к определению контроллеров. Затем мы сравнили аннотации Spring MVC с их функциональными эквивалентами.

Далее мы реализовали простой веб-сервис, возвращающий список продуктов с функциональным контроллером.

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

Как всегда, пример кода можно найти на GitHub.