Spring Web MVC содержит WebMvc.fn, легкую функциональную модель программирования, в которой функции используются для маршрутизации и обработки запросов, а контракты разработаны для обеспечения неизменяемости. Она является альтернативой модели программирования на основе аннотаций, но в остальном работает на том же DispatcherServlet.

Краткое описание

В WebMvc.fn HTTP-запрос обрабатывается с помощью HandlerFunction: функции, которая принимает ServerRequest и возвращает ServerResponse. И запрос, и объект-ответ имеют неизменяемые контракты, которые обеспечивают доступ к HTTP-запросу и ответу, удобный для JDK 8. HandlerFunction – это эквивалент тела метода, помеченного аннотацией @RequestMapping, в модели программирования на основе аннотаций.

Входящие запросы направляются в функцию-обработчик с помощью RouterFunction: функции, которая принимает ServerRequest и возвращает необязательную HandlerFunction (т.е. Optional<HandlerFunction>). Если функция-маршрутизатор совпадает, возвращается функция-обработчик, в противном случае – пустой Optional. RouterFunction – это эквивалент аннотации @RequestMapping, но с тем существенным отличием, что функции-маршрутизаторы передают не только данные, но и логику работы.

RouterFunctions.route() предоставляет средство сборки маршрутизаторов, которое облегчает создание маршрутизаторов, как показано в следующем примере:

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople)
    .POST("/person", handler::createPerson)
    .build();
public class PersonHandler {
    // ...
    public ServerResponse listPeople(ServerRequest request) {
        // ...
    }
    public ServerResponse createPerson(ServerRequest request) {
        // ...
    }
    public ServerResponse getPerson(ServerRequest request) {
        // ...
    }
}
Kotlin
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = router {
    accept(APPLICATION_JSON).nest {
        GET("/person/{id}", handler::getPerson)
        GET("/person", handler::listPeople)
    }
    POST("/person", handler::createPerson)
}
class PersonHandler(private val repository: PersonRepository) {
    // ...
    fun listPeople(request: ServerRequest): ServerResponse {
        // ...
    }
    fun createPerson(request: ServerRequest): ServerResponse {
        // ...
    }
    fun getPerson(request: ServerRequest): ServerResponse {
        // ...
    }
}
  1. Create router using the router DSL.

Если зарегистрировать RouterFunction как бин, например, открыв его в классе с аннотацией @Configuration, он будет автоматически обнаружен сервлетом.

HandlerFunction

ServerRequest и ServerResponse – это неизменяемые интерфейсы, которые предоставляют доступ к HTTP-запросу и ответу, включая заголовки, тело, метод и код состояния, удобоваримый для JDK 8.

ServerRequest

ServerRequest предоставляет доступ к HTTP-методу, URI-идентификатору, заголовкам и параметрам запроса, а доступ к телу предоставляется через методы body.

В следующем примере тело запроса извлекается в String:

Java
String string = request.body(String.class);
Kotlin
val string = request.body<String>()

В следующем примере тело извлекается в List<Person>, где объекты Person декодируются из сериализованной формы, такой как формат JSON или XML:

Java
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
Kotlin
val people = request.body<Person>()

В следующем примере показано, как получить доступ к параметрам:

Java
MultiValueMap<String, String> params = request.params();
Kotlin
val map = request.params()

ServerResponse

ServerResponse предоставляет доступ к HTTP-ответу, и, поскольку он неизменяем, можно использовать метод build для его создания. Можно использовать средство сборки для настройки статуса ответа, добавления заголовков ответа или передачи тела ответа. В следующем примере создается ответ 200 (OK) с содержимым формата JSON:

Java
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
Kotlin
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)

В следующем примере показано, как создать ответ 201 (CREATED) с заголовком Location и без тела:

Java
URI location = ...
ServerResponse.created(location).build();
Kotlin
val location: URI = ...
ServerResponse.created(location).build()

Также можно использовать в качестве тела асинхронный результат в виде CompletableFuture, Publisher или любого другого типа, поддерживаемого ReactiveAdapterRegistry. Например:

Java
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
Kotlin
val person = webClient.get().retrieve().awaitBody<Person>()
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)

Если не только тело, но также статус или заголовки основаны на асинхронном типе, можно использовать статический метод async для ServerResponse, который принимает CompletableFuture<ServerResponse>, Publisher<ServerResponse>, или любой другой асинхронный тип, поддерживаемый ReactiveAdapterRegistry. Например:

Java
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
  .map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);

События, посылаемые сервером, могут быть переданы с помощью статического метода sse для ServerResponse. Средство сборки, предоставляемое этим методом, позволяет отправлять строки или другие объекты в формате JSON. Например:

Java
public RouterFunction<ServerResponse> sse() {
    return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
                // Сохраняем объект sseBuilder где-нибудь...
            }));
}
// В каком-то другом потоке, отправляющем строку
sseBuilder.send("Hello world");
// Или объекте, который будет преобразован в JSON
Person person = ...
sseBuilder.send(person);
// Настраиваем событие с помощью других методов
sseBuilder.id("42")
        .event("sse event")
        .data(person);
// и в какой-то момент будет готово
sseBuilder.complete();
Kotlin
fun sse(): RouterFunction<ServerResponse> = router {
    GET("/sse") { request -> ServerResponse.sse { sseBuilder ->
        // Save the sseBuilder object somewhere..
    }
}
// В каком-то другом потоке, отправляющем строку
sseBuilder.send("Hello world")
// Или объекте, который будет преобразован в JSON
val person = ...
sseBuilder.send(person)
// Настраиваем событие с помощью других методов
sseBuilder.id("42")
        .event("sse event")
        .data(person)
// и в какой-то момент будет готово
sseBuilder.complete()

Классы обработчиков

Мы можем написать функцию-обработчик в виде лямбда-выражения, как показано в следующем примере:

Java
HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().body("Hello World");
Kotlin
val helloWorld: (ServerRequest) -> ServerResponse =
  { ServerResponse.ok().body("Hello World") }

Это удобно, но в приложении нам требуются несколько функций, а несколько встроенных лямбда-выражений могут все запутать. Поэтому полезно будет сгруппировать связанные функции-обработчики в класс обработчика, который играет такую же роль, как и аннотация @Controller в приложении, основанном на аннотациях. Например, следующий класс представляет реактивное хранилище Person:

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
    private final PersonRepository repository;
    public PersonHandler(PersonRepository repository) {
        this.repository = repository;
    }
    public ServerResponse listPeople(ServerRequest request) {
        List<Person> people = repository.allPeople();
        return ok().contentType(APPLICATION_JSON).body(people);
    }
    public ServerResponse createPerson(ServerRequest request) throws Exception {
        Person person = request.body(Person.class);
        repository.savePerson(person);
        return ok().build();
    }
    public ServerResponse getPerson(ServerRequest request) {
        int personId = Integer.parseInt(request.pathVariable("id"));
        Person person = repository.getPerson(personId);
        if (person != null) {
            return ok().contentType(APPLICATION_JSON).body(person);
        }
        else {
            return ServerResponse.notFound().build();
        }
    }
}
  1. listPeople – это функция-обработчик, которая возвращает все объекты Person, найденные в репозитории, в формате JSON.
  2. createPerson – это функция-обработчик, которая сохраняет новый объект Person, содержащийся в теле запроса.
  3. getPerson – это функция-обработчик, которая возвращает одного человека, идентифицированного переменной пути id. Мы извлекаем этот объект Person из хранилища и создаем ответ в формате JSON, если он будет найден. Если он не будет найден, мы возвращаем ответ 404 Not Found.
Kotlin
class PersonHandler(private val repository: PersonRepository) {
    fun listPeople(request: ServerRequest): ServerResponse {
        val people: List<Person> = repository.allPeople()
        return ok().contentType(APPLICATION_JSON).body(people);
    }
    fun createPerson(request: ServerRequest): ServerResponse {
        val person = request.body<Person>()
        repository.savePerson(person)
        return ok().build()
    }
    fun getPerson(request: ServerRequest): ServerResponse {
        val personId = request.pathVariable("id").toInt()
        return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) }
                ?: ServerResponse.notFound().build()
    }
}
  1. listPeople – это функция-обработчик, которая возвращает все объекты Person, найденные в репозитории, в формате JSON.
  2. createPerson – это функция-обработчик, которая сохраняет новый объект Person, содержащийся в теле запроса.
  3. getPerson – это функция-обработчик, которая возвращает одного человека, идентифицированного переменной пути id. Мы извлекаем этот объект Person из хранилища и создаем ответ в формате JSON, если он будет найден. Если он не будет найден, мы возвращаем ответ 404 Not Found.

Валидация

Функциональная конечная точка может использовать средства валидации Spring для применения валидации к телу запроса. Например, дана пользовательская реализация Validator из Spring для Person:

Java
public class PersonHandler {
    private final Validator validator = new PersonValidator();
    // ...
    public ServerResponse createPerson(ServerRequest request) {
        Person person = request.body(Person.class);
        validate(person);
        repository.savePerson(person);
        return ok().build();
    }
    private void validate(Person person) {
        Errors errors = new BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw new ServerWebInputException(errors.toString());
        }
    }
}
  1. Создаем экземпляр Validator.
  2. Применяем валидацию.
  3. Генерируется исключение для ответа 400.
Kotlin
class PersonHandler(private val repository: PersonRepository) {
    private val validator = PersonValidator()
    // ...
    fun createPerson(request: ServerRequest): ServerResponse {
        val person = request.body<Person>()
        validate(person)
        repository.savePerson(person)
        return ok().build()
    }
    private fun validate(person: Person) {
        val errors: Errors = BeanPropertyBindingResult(person, "person")
        validator.validate(person, errors)
        if (errors.hasErrors()) {
            throw ServerWebInputException(errors.toString())
        }
    }
}
  1. Создаем экземпляр Validator.
  2. Применяем валидацию.
  3. Генерируется исключение для ответа 400.

Обработчики также могут использовать стандартный API валидации бинов (JSR-303), создавая и внедряя глобальный экземпляр валидатора на основе LocalValidatorFactoryBean.

RouterFunction

Функции-маршрутизаторы используются для маршрутизации запросов к соответствующим HandlerFunction. Как правило, функции-маршрутизаторы пишутся не самостоятельно, а для их создания используется метод для вспомогательного класса RouterFunctions. RouterFunctions.route() (без параметров) предоставляет удобное средство сборки для создания функции-маршрутизатора, в то время как RouterFunctions.route(RequestPredicate, HandlerFunction) предлагает прямой способ создания маршрутизатора.

Как правильно, рекомендуется использовать средство сборки route(), поскольку оно обеспечивает удобные сокращения для типичных сценариев отображения, не требуя импортирования статических элементов, которые трудно обнаружить. Например, средство сборки функций-маршрутизаторов предлагает метод GET(String, HandlerFunction) для создания Map для GET-запросов; и POST(String, HandlerFunction) – для POST-запросов.

Помимо отображения на основе HTTP-методов, средство сборки маршрутов предлагает способ введения дополнительных предикатов при отображении на запросы. Для каждого HTTP-метода существует перегруженный вариант, который принимает RequestPredicate в качестве параметра, через который могут быть выражены дополнительные ограничения.

Предикаты

Вы можете написать свой собственный RequestPredicate, но вспомогательный класс RequestPredicates предусматривает часто используемые реализации, основанные на пути запроса, HTTP-методе, типе содержимого и так далее. В следующем примере используется предикат запроса для создания ограничения на основе заголовка Accept:

Java
RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
        request -> ServerResponse.ok().body("Hello World")).build();
Kotlin
import org.springframework.web.servlet.function.router
val route = router {
    GET("/hello-world", accept(TEXT_PLAIN)) {
        ServerResponse.ok().body("Hello World")
    }
}

Можно скомпоновать несколько предикатов запроса вместе при помощи:

  • RequestPredicate.and(RequestPredicate) – оба должны совпадать.

  • RequestPredicate.or(RequestPredicate) – любой из них может совпадать.

Многие предикаты из RequestPredicates являются составными. Например, RequestPredicates.GET(String) состоит из RequestPredicates.method(HttpMethod) и RequestPredicates.path(String). В приведенном примере также используются два предиката запроса, так как средство сборки использует RequestPredicates.GET на внутреннем уровне и комбинирует его с предикатом accept.

Маршруты

Функции-маршрутизаторы вычисляются упорядоченно: если первый маршрут не совпадает, то вычисляется второй, и так далее. Поэтому имеет смысл объявлять более конкретные маршруты перед обобщенными. Это также важно при регистрации функций-маршрутизаторов в качестве бинов Spring, как будет описано позже. Обратите внимание, что такая логика работы отличается от модели программирования на основе аннотаций, где "наиболее конкретный" метод контроллера выбирается автоматически.

При использовании средства сборки функций-маршрутизаторов все определенные маршруты компонуются в одну RouterFunction, которая возвращается из build(). Существуют и другие способы компоновки нескольких функций маршрутизатора в одну:

  • add(RouterFunction) к средству сборки RouterFunctions.route()

  • RouterFunction.and(RouterFunction)

  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) - сокращение для RouterFunction.and() с вложенными RouterFunctions.route().

В следующем примере показана компоновка из четырех маршрутов:

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople)
    .POST("/person", handler::createPerson)
    .add(otherRoute)
    .build();
  1. GET /person/{id} с заголовком Accept, который соответствует формату JSON, маршрутизируется в PersonHandler.getPerson
  2. GET /person с заголовком Accept, который соответствует формату JSON, маршрутизируется в PersonHandler.listPeople
  3. POST /person без дополнительных предикатов отображается на PersonHandler.createPerson, и
  4. otherRoute – это функция-маршрутизатор, которая создается в другом месте и добавляется к построенному маршруту.
Kotlin
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute = router {  }
val route = router {
    GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
    GET("/person", accept(APPLICATION_JSON), handler::listPeople)
    POST("/person", handler::createPerson)
}.and(otherRoute)
  1. GET /person/{id} с заголовком Accept, который соответствует формату JSON, маршрутизируется в PersonHandler.getPerson
  2. GET /person с заголовком Accept, который соответствует формату JSON, маршрутизируется в PersonHandler.listPeople
  3. POST /person без дополнительных предикатов отображается на PersonHandler.createPerson, и
  4. otherRoute – это функция-маршрутизатор, которая создается в другом месте и добавляется к построенному маршруту.

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

Обычно группа функций-маршрутизаторов имеет общий предикат, например, общий путь. В приведенном выше примере общим предикатом будет предикат пути, который соответствует /person, используемый тремя маршрутами. При использовании аннотаций можно устранить это дублирование, используя аннотацию @RequestMapping на уровне типа, которая отображается на /person. В WebMvc.fn предикаты пути могут совместно использоваться с помощью метода path в средстве сборки функций-маршрутизаторов. Например, последние несколько строк приведенного выше примера можно дополнить следующим образом, используя вложенные маршруты:

Java
RouterFunction<ServerResponse> route = route()
    .path("/person", builder -> builder
        .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        .GET(accept(APPLICATION_JSON), handler::listPeople)
        .POST(handler::createPerson))
    .build();
  1. Обратите внимание, что второй параметр path – это получатель, который принимает средство сборки маршрутизатора.
Kotlin
import org.springframework.web.servlet.function.router
val route = router {
    "/person".nest {
        GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        GET(accept(APPLICATION_JSON), handler::listPeople)
        POST(handler::createPerson)
    }
}

Хотя вложение на основе пути является наиболее распространенным, можно вложить предикат любого типа, используя метод nest в средстве сборки. Вышеприведенный вариант все еще содержит некоторое дублирование в виде общего предиката Accept-header. Мы продолжить дополнять код, используя метод nest вместе с accept:

Java
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler::listPeople))
        .POST(handler::createPerson))
    .build();
Kotlin
import org.springframework.web.servlet.function.router
val route = router {
    "/person".nest {
        accept(APPLICATION_JSON).nest {
            GET("/{id}", handler::getPerson)
            GET("", handler::listPeople)
            POST(handler::createPerson)
        }
    }
}

Запуск сервера

Обычно функции маршрутизатора в конфигурации на основе DispatcherHandler запускаются через конфигурацию MVC, которая задействует конфигурацию Spring для объявления компонентов, необходимых для обработки запросов. Конфигурация MVC на Java объявляет следующие компоненты инфраструктуры для поддержки функциональных конечных точек:

  • RouterFunctionMapping: Обнаруживает один или несколько бинов RouterFunction<?> в конфигурации Spring, упорядочивает их, объединяет с помощью RouterFunction.andOther и маршрутизирует запросы к результирующей составной функции RouterFunction.

  • HandlerFunctionAdapter: Простой адаптер, позволяющий DispatcherHandler вызывать HandlerFunction, которая была отображена на запрос.

Предыдущие компоненты позволяют функциональным конечным точкам "вписаться" в жизненный цикл обработки запросов DispatcherServlet, а также (потенциально) работать бок о бок с аннотированными контроллерами, если таковые объявлены. Это также способ активации функциональных конечных точек в пусковой системе Spring Boot Web.

В следующем примере показана конфигурация WebFlux Java:

Java
@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public RouterFunction<?> routerFunctionA() {
        // ...
    }
    @Bean
    public RouterFunction<?> routerFunctionB() {
        // ...
    }
    // ...
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // конфигурируем преобразование сообщений...
    }
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // конфигурируем CORS...
    }
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // конфигурируем разрешение представления для HTML-визуализации...
    }
}
Kotlin
@Configuration
@EnableMvc
class WebConfig : WebMvcConfigurer {
    @Bean
    fun routerFunctionA(): RouterFunction<*> {
        // ...
    }
    @Bean
    fun routerFunctionB(): RouterFunction<*> {
        // ...
    }
    // ...
    override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
        // конфигурируем преобразование сообщений...
    }
    override fun addCorsMappings(registry: CorsRegistry) {
        // конфигурируем CORS...
    }
    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // конфигурируем разрешение представления для HTML-визуализации...
    }
}

Функции обработчика фильтрации

Можно фильтровать функции-обработчики с помощью методов before, after или filter в средстве сборки функций маршрутизации. С помощью аннотаций можно добиться аналогичной функциональности, используя @ControllerAdvice, ServletFilter или и то, и другое. Фильтр будет применен ко всем маршрутам, построенным средством сборки. Это означает, что фильтры, определенные во вложенных маршрутах, не применяются к маршрутам "верхнего уровня". Например, рассмотрим следующий пример:

Java
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler::listPeople)
            .before(request -> ServerRequest.from(request)
                .header("X-RequestHeader", "Value")
                .build()))
        .POST(handler::createPerson))
    .after((request, response) -> logResponse(response))
    .build();
  1. Фильтр before, добавляющий кастомный заголовок запроса, применяется только к двум маршрутам GET.
  2. Фильтр after, регистрирующий ответ, применяется ко всем маршрутам, включая вложенные.
Kotlin
import org.springframework.web.servlet.function.router
val route = router {
    "/person".nest {
        GET("/{id}", handler::getPerson)
        GET(handler::listPeople)
        before {
            ServerRequest.from(it)
                    .header("X-RequestHeader", "Value").build()
        }
    }
    POST(handler::createPerson)
    after { _, response ->
        logResponse(response)
    }
}
  1. Фильтр before, добавляющий кастомный заголовок запроса, применяется только к двум маршрутам GET.
  2. Фильтр after, регистрирующий ответ, применяется ко всем маршрутам, включая вложенные.

Метод filter в средстве сборки маршрутизатора принимает HandlerFilterFunction: функцию, которая принимает ServerRequest и HandlerFunction и возвращает ServerResponse. Параметр функции-обработчика представляет собой следующий элемент в цепочке. Обычно это обработчик, к которому маршрутизируется запрос, но это может быть и другой фильтр, если применяется несколько.

Теперь можно добавить простой фильтр безопасности к нашему маршруту, предположив, что у нас есть SecurityManager, который может определить, допустим ли определенный путь. В следующем примере показано, как это сделать:

Java
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler::listPeople))
        .POST(handler::createPerson))
    .filter((request, next) -> {
        if (securityManager.allowAccessTo(request.path())) {
            return next.handle(request);
        }
        else {
            return ServerResponse.status(UNAUTHORIZED).build();
        }
    })
    .build();
Kotlin
import org.springframework.web.servlet.function.router
val securityManager: SecurityManager = ...
val route = router {
    ("/person" and accept(APPLICATION_JSON)).nest {
        GET("/{id}", handler::getPerson)
        GET("", handler::listPeople)
        POST(handler::createPerson)
        filter { request, next ->
            if (securityManager.allowAccessTo(request.path())) {
                next(request)
            }
            else {
                status(UNAUTHORIZED).build();
            }
        }
    }
}

В предыдущем примере продемонстрировано, что вызов next.handle(ServerRequest) является необязательным. Мы позволяем выполняться функции-обработчику только тогда, когда доступ разрешен.

Помимо использования метода filter в средстве сборки функций-маршрутизаторов, можно применить фильтр к существующей функции-маршрутизатору через RouterFunction.filter(HandlerFilterFunction).

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