Рубрики
Без рубрики

Функциональные контроллеры в Spring MVC

Контроллеры Spring MVC теперь могут быть написаны в новом функциональном стиле. Исследуйте этот новый шаблон, привнося преимущества функционального программирования в блокирующий код.

Автор оригинала: baeldung.

1. введение

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

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

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

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

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

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

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


    org.springframework.boot
    spring-boot-starter-web

3. Функция маршрутизатора против @Контроллера

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

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

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

@RestController
public class ProductController {

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

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

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

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

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

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

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

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

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

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

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

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

RequestPredicates.path("/product")

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

3.3. Ответ

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

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

Кроме того, Server Response поддерживает построение ответа из пользовательских типов данных с использованием Entity Response. Мы также можем использовать ответ Spring MVC ModelAndView via Rendering.

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

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

@SpringBootApplication
public class SpringBootMvcFnApplication {

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

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

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

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

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

public RouterFunction 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().

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

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

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

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

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

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

public RouterFunction 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 class и ожидает ответа Сервера от функциональной реализации.

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

6. Фильтры

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

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

public RouterFunction 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();
}

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

Мы можем сделать это, добавив фильтр() способ на маршруте():

public RouterFunction 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 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 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 .