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 ListproductListing() { return ps.findAll(); } }
Теперь давайте рассмотрим его функциональный эквивалент:
@Bean public RouterFunctionproductListing(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 RouterFunctionproductListing(ProductController pc, ProductService ps) { return pc.productListing(ps); } }
Теперь давайте рассмотрим некоторые распространенные случаи использования, с которыми мы сталкиваемся при разработке веб-сервисов с использованием функционального подхода.
4. Вложенные маршруты
Довольно часто в приложении есть несколько веб-служб, а также они разделены на логические группы на основе функции или сущности. Например, мы можем захотеть, чтобы все услуги, связанные с продуктом, начинались с /продукта .
Давайте добавим еще один путь к существующему пути /product , чтобы найти продукт по его имени:
public RouterFunctionproductSearch(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 RouterFunctionproductSearch(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 RouterFunctionadminFunctions(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 RouterFunctionadminFunctions(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 RouterFunctionallApplicationRoutes(ProductController pc, ProductService ps) { return route()... .before(req -> { LOG.info("Found a route which matches " + req.uri() .getPath()); return req; }) .build(); }
Аналогично, мы можем добавить простой оператор журнала после обработки запроса с помощью метода after() на route() :
@Bean RouterFunctionallApplicationRoutes(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 .