1. Введение
Spring 5 представил WebFlux , новую структуру, которая позволяет нам создавать веб-приложения с использованием модели реактивного программирования.
В этом руководстве мы увидим, как мы можем применить эту модель программирования к функциональным контроллерам в Spring MVC.
2. Настройка Мавена
Мы будем использовать 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
против @ контроллера
В функциональной сфере веб-служба называется маршрутом, а традиционная концепция @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 .