Перейти к основному содержимому

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

· 6 мин. чтения

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 .