«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.