Spring Web MVC contiene WebMvc.fn, un modelo de programación funcional liviano en el que se utilizan funciones para enrutar y procesar solicitudes, y los contratos están diseñados para ser inmutables. Es una alternativa al modelo de programación basado en anotaciones, pero por lo demás se ejecuta en el mismo DispatcherServlet.
Breve descripción
En WebMvc.fn, una solicitud HTTP se maneja mediante un
HandlerFunction
: una función que acepta ServerRequest
y devuelve
ServerResponse
. Tanto el objeto de
solicitud como el de respuesta tienen contratos inmutables que proporcionan acceso compatible con JDK 8 a la
solicitud y respuesta HTTP. HandlerFunction
es el equivalente al cuerpo de un método marcado con la
anotación @RequestMapping
, en un modelo de programación basado en anotaciones.
Las solicitudes entrantes se enrutan a una función de controlador usando RouterFunction
: una
función que acepta una ServerRequest
y devuelve una HandlerFunction
opcional (es decir,
Optional<HandlerFunction>
). Si la función del enrutador coincide, se devuelve la función del
controlador; de lo contrario, se devuelve un Opcional vacío. RouterFunction
es el equivalente a la
anotación @RequestMapping
, pero con la diferencia significativa de que las funciones del enrutador
transfieren no solo datos, sino también lógica operativa.
RouterFunctions.route()
proporciona
un generador de enrutadores que facilita la creación de enrutadores, como se muestra en el siguiente ejemplo:
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) {
// ...
}
}
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 {
// ...
}
}
- Crear enrutador usando el enrutador DSL.
Si está registrado RouterFunction
como un bean, por ejemplo, al abrirlo en una clase con la
anotación @Configuration
, el servlet lo descubrirá automáticamente.
HandlerFunction
ServerRequest
y ServerResponse
son interfaces inmutables que brindan acceso a la solicitud
y respuesta HTTP, incluidos encabezados, cuerpo, método y compatible con JDK 8. código de estado.
ServerRequest
ServerRequest
proporciona acceso al método HTTP, URI, encabezados y parámetros de
la solicitud, y el acceso al cuerpo se proporciona a través de los métodos body
.
El siguiente
ejemplo extrae el cuerpo de la solicitud en una String
:
String string = request.body (String.class);
val string = request.body<String>()
El siguiente ejemplo recupera el cuerpo en una List<Person>
, donde Los objetos Person
se decodifican desde el formato serializado, como el formato JSON o XML:
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
val people = request.body<Person>()
El siguiente ejemplo muestra cómo acceder a los parámetros:
MultiValueMap<String, String> params = request.params();
val map = request.params()
ServerResponse
ServerResponse
proporciona acceso a la respuesta HTTP y, dado que es
inmutable, puede usar la compilación método para su creación. Puede utilizar la herramienta de compilación para
configurar el estado de la respuesta, agregar encabezados de respuesta o pasar el cuerpo de la respuesta. El
siguiente ejemplo genera una respuesta 200 (OK) con contenido con formato JSON:
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON ).body(person);
val person: Person = ...
ServerResponse.ok( ).contentType(MediaType.APPLICATION_JSON).body(person)
El siguiente ejemplo muestra cómo crear una respuesta 201 (CREADA) con el encabezado Location
y
sin cuerpo:
URI location = ...
ServerResponse.created(location).build();
val location: URI = ...
ServerResponse.created(location).build()
También puedes usar un resultado asincrónico como cuerpo en forma de CompletableFuture
,
Publisher
o cualquier otro tipo compatible con
ReactiveAdapterRegistry
. Por ejemplo:
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
val person = webClient.get().retrieve().awaitBody<Person>()
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
Si no sólo el cuerpo, sino también el estado o los encabezados se basan en un tipo asincrónico, puede utilizar
el método estático async
en ServerResponse
, que toma CompletableFuture<ServerResponse>
, Publisher<ServerResponse>
o cualquier otro tipo asincrónico admitido por ReactiveAdapterRegistry
.
Por ejemplo:
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
.map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);
Los eventos enviados por el servidor se
pueden enviar utilizando el método estático sse
para ServerResponse
. La función de
compilación proporcionada por este método le permite enviar cadenas u otros objetos en formato JSON. Por ejemplo:
public RouterFunction<ServerResponse> sse() {
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
// Guarde el objeto sseBuilder en algún lugar...
}));
}
// En algún otro hilo enviando la cadena
sseBuilder.send("Hello world");
// O un objeto que se convertirá a JSON
Person person = ...
sseBuilder.send(person);
// Configura el evento usando otros métodos
sseBuilder.id("42")
.event("sse event")
.data(person);
// y en algún momento estará listo
sseBuilder.complete();
fun sse( ): RouterFunction<ServerResponse> = router {
GET("/sse") { request -> ServerResponse.sse { sseBuilder ->
// Guarde el objeto sseBuilder en algún lugar...
}
}
// En algún otro hilo enviando la cadena
sseBuilder.send("Hello world")
// En algún otro hilo enviando la cadena
val person = ...
sseBuilder.send (person)
// Configura el evento usando otros métodos
sseBuilder.id("42")
.event("sse event")
.data(person)
// y en algún momento estará listo
sseBuilder.complete())
Clases de controlador
Podemos escribir una función de controlador como una expresión lambda, como se muestra en el siguiente ejemplo:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body("Hello World");
val helloWorld: (ServerRequest ) -> ServerResponse =
{ ServerResponse.ok().body("Hello World") }
Esto es conveniente, pero en la aplicación necesitamos varias funciones y Varias expresiones Lambda integradas
pueden resultar confusas. Por lo tanto, resulta útil agrupar funciones de controlador relacionadas en una clase de
controlador, que desempeña el mismo papel que la anotación @Controller
en una aplicación basada en
anotaciones. Por ejemplo, la siguiente clase representa una tienda reactiva Person
:
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();
}
}
}
listPeople
es una función de controlador que devuelve todos los objetosPerson
encontrados en el repositorio en formato JSON.createPerson
es una función de controlador que guarda un nuevo código de objetoPerson
. contenida en el cuerpo de la solicitud.getPerson
es una función de controlador que devuelve una única persona identificada por la variable de rutaid
. Recuperamos este objetoPerson
del almacenamiento y generamos una respuesta JSON si lo encontramos. Si no se encuentra, devolvemos una respuesta 404 No encontrado.
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()
}
}
listPeople
es una función de controlador que devuelve todos los objetosPerson
que se encuentran en el repositorio, en formato JSON.createPerson
es una función de controlador que guarda un nuevo objetoPerson
contenido en el cuerpo de la solicitud.getPerson
es una función de controlador que devuelve una única persona identificada por la variable de rutaid
. Recuperamos este objetoPersona
del almacenamiento y generamos una respuesta JSON si lo encontramos. Si no se encuentra, devolvemos una respuesta 404 No encontrado.
Validación
El punto final de la función puede usar los validadores de Spring para aplicar la
validación al cuerpo de la solicitud. Por ejemplo, aquí hay una implementación Spring personalizada de Validator
para Person
:
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());
}
}
}
- Crear una instancia de
Validator
. - Aplicar validación.
- Lanzar una excepción para la respuesta 400.
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())
}
}
}
- Crear una instancia de
Validator
. - Aplicar validación.
- Lanzar una excepción para la respuesta 400.
Los controladores también pueden usar la API de validación de beans estándar (JSR-303) creando e inyectando un
validador
global. instancia basada en un LocalValidatorFactoryBean
.
RouterFunction
Las funciones de enrutador se utilizan para enrutar solicitudes a la correspondiente HandlerFunction
. Como regla general, las funciones del enrutador no se escriben de forma independiente, sino que se utiliza
un método para la clase auxiliar RouterFunctions
para crearlas. RouterFunctions.route()
(sin parámetros) proporciona un generador conveniente para crear una función de enrutador, mientras que RouterFunctions.route(RequestPredicate,
HandlerFunction)
proporciona una forma directa de crear un enrutador .
De hecho, se recomienda
utilizar el constructor route()
porque proporciona accesos directos convenientes para escenarios de
visualización comunes sin requerir la importación de elementos estáticos que son difíciles. detectar. Por ejemplo,
el generador de funciones del enrutador ofrece un método GET(String, HandlerFunction)
para crear un
mapa para solicitudes GET; y POST(String, HandlerFunction)
- para solicitudes POST.
Además del
mapeo basado en métodos HTTP, el generador de rutas ofrece una manera de introducir predicados adicionales al mapear
solicitudes. Para cada método HTTP existe una sobrecarga que toma RequestPredicate
como parámetro a
través del cual se pueden expresar restricciones adicionales.
Predicados
Puedes escribir tus propios
métodos nativos RequestPredicate
, pero una clase auxiliar RequestPredicates
proporciona
implementaciones comúnmente utilizadas basadas en la ruta de solicitud, el método HTTP, el tipo de contenido, etc.
El siguiente ejemplo utiliza un predicado de solicitud para crear una restricción basada en el encabezado Accept
:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().body("Hello World")).build();
import org.springframework.web.servlet.function.router
val route = router {
GET("/hello-world", accept(TEXT_PLAIN)) {
ServerResponse.ok().body("Hello World")
}
}
Puedes encadenar múltiples predicados de solicitud juntos usando:
RequestPredicate.and(RequestPredicate)
- ambos deben coincidir.RequestPredicate.or(RequestPredicate)
– cualquiera de ellos puede coincidir.
Muchos predicados de RequestPredicates
son compuestos. Por ejemplo, RequestPredicates.GET(String)
consta de RequestPredicates.method(HttpMethod)
y RequestPredicates.path(String)
. El
ejemplo anterior también usa dos predicados de solicitud porque el constructor usa
RequestPredicates.GET
internamente y lo combina con el predicado accept
.
Rutas
Las funciones del enrutador se evalúan de manera ordenada: si la primera ruta no coincide, se evalúa la segunda, y así sucesivamente. Por tanto, tiene sentido declarar rutas más específicas antes que las genéricas. Esto también es importante al registrar funciones de enrutador como Spring beans, como se explica más adelante. Tenga en cuenta que esta lógica operativa difiere del modelo de programación basado en anotaciones, donde el método de controlador "más específico" se selecciona automáticamente.
Cuando se utiliza el generador de funciones del enrutador, todas las rutas definidas se ensamblan en un único
RouterFunction
, que se devuelve desde build()
. Hay otras formas de combinar varias
funciones de enrutador en una:
add(RouterFunction)
a la herramienta de compilaciónRouterFunctions.route()
RouterFunction.and(RouterFunction)
RouterFunction.andRoute( RequestPredicate, HandlerFunction)
: abreviatura deRouterFunction.and()
conRouterFunctions.route()
anidado.
El siguiente ejemplo muestra un diseño de cuatro rutas:
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();
GET /person/{id}
con encabezado ElAceptar
que coincide con el formato JSON se dirige aPersonHandler.getPerson
GET /person
con el encabezadoAceptar
que coincide con el formato JSON se enruta aPersonHandler.listPeople
POST /person
sin predicados adicionales se asigna aPersonHandler.createPerson
, yotherRoute
es una función de enrutador que se crea en otro lugar y se agrega a la ruta construida.
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)
GET /person/{id}
con encabezadoAceptar
que coincide con el formato JSON se dirige aPersonHandler.getPerson
GET /person
con el encabezadoAceptar
El que se ajusta al formato JSON se enruta aPersonHandler.listPeople
POST /person
sin predicados adicionales se asigna aPersonHandler.createPerson
, yotherRoute
es una función de enrutador que se crea en otro lugar y se agrega a la ruta construida.
Rutas anidadas
Normalmente, un grupo de funciones de enrutador tiene un predicado común, como una ruta común. En el ejemplo
anterior, el predicado común sería el predicado de ruta que coincide con
/person
, utilizado por las tres rutas. Al utilizar anotaciones, puede eliminar esta duplicación utilizando la anotación
@RequestMapping
en el nivel de tipo, que se asigna a /person
. En WebMvc.fn, los predicados
de ruta se pueden compartir utilizando el método path
en el generador de funciones del enrutador. Por
ejemplo, las últimas líneas del ejemplo anterior podrían expandirse para verse así, usando rutas anidadas:
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();
- Tenga en cuenta que el segundo parámetro
path
es el destino que acepta el constructor del enrutador.
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)
}
}
Aunque el archivo adjunto está activado basado en ruta es el más común, puede anidar un predicado de
cualquier tipo usando el método nest
en la herramienta de compilación. Lo anterior todavía contiene
algunas duplicaciones en la forma del predicado común Accept-header
. Continuaremos expandiendo el
código usando el método nest
junto con accept
:
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();
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)
}
}
}
Inicio del servidor
Normalmente, el enrutador funciona en una configuración basados en DispatcherHandler
se lanzan a través de una configuración MVC, que aprovecha la configuración de Spring para declarar los componentes
necesarios para manejar las solicitudes. La configuración de Java MVC declara los siguientes componentes de
infraestructura para admitir puntos finales funcionales:
-
RouterFunctionMapping
: descubre uno o más beansRouterFunction<?>
en la configuración de Spring, los ordena, los combina usandoRouterFunction.andOther
y enruta las solicitudes a la función compuesta resultanteRouterFunction
. HandlerFunctionAdapter
: un adaptador simple que permite queDispatcherHandler
llame a laHandlerFunction
que se asignó a la solicitud.
Los componentes anteriores
permiten que los puntos finales funcionales encajen en el ciclo de vida de la solicitud
DispatcherServlet
y también (potencialmente) trabajen en paralelo con los controladores anotados, si se
declaran. Esta es también la forma de habilitar puntos finales funcionales en el iniciador web Spring Boot.
El siguiente ejemplo muestra la configuración de WebFlux Java:
@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
} @Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// configurar la conversión de mensajes...
}
@Override
public void addCorsMappings(CorsRegistry registry ) {
// configurar CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configurar la resolución de vista para la representación HTML
}
}}
@Configuration
@EnableMvcclass WebConfig : WebMvcConfigurer {
@Bean
fun routerFunctionA(): RouterFunction<*> {
// ...
}
@Bean
fun routerFunctionB(): RouterFunction<*> {
// ...
}
// ...
override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
// configurar la conversión de mensajes...
}
override fun addCorsMappings(registry: CorsRegistry) {
// configurar CORS...
}
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// configurar la resolución de vista para la representación HTML...
}
}
Funciones del controlador de filtro
Puede filtrar funciones de controlador utilizando los métodos before
,
after
o filter
en el generador de funciones de enrutamiento. Las anotaciones pueden lograr
una funcionalidad similar utilizando @ControllerAdvice
, ServletFilter
o ambos. El filtro
se aplicará a todas las rutas creadas por la herramienta de construcción. Esto significa que los filtros definidos
en rutas anidadas no se aplican a las rutas de "nivel superior". Por ejemplo, considere el siguiente ejemplo:
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();
- La adición del filtro
before
encabezado de solicitud personalizado, se aplica solo a dos rutas GET. - El filtro
after
, que registra la respuesta, se aplica a todas las rutas, incluidas las anidadas.
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)
}
}
- El filtro
before
, que agrega un encabezado de solicitud personalizado, se aplica solo a dos rutas GET. - El filtro
after
, que registra la respuesta, se aplica a todas las rutas, incluidas las anidadas.
El método filter
en la herramienta de construcción del enrutador acepta una
HandlerFilterFunction
:
una función que acepta una ServerRequest
y una HandlerFunction
y devuelve ServerResponse
.
El parámetro de la función del controlador es el siguiente elemento de la cadena. Normalmente, este es el
controlador al que se enruta la solicitud, pero podría ser un filtro diferente si se aplica más de uno.
Ahora podemos agregar un filtro de seguridad simple a nuestra ruta, asumiendo que tenemos un
SecurityManager
, que puede determinar si una determinada ruta es válida. El siguiente ejemplo
muestra cómo hacer esto:
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();
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();
}
}
}
}
El ejemplo anterior demostró que llamar a next.handle(ServerRequest)
es opcional. Permitimos que la
función ejecute -handler solo cuando se permita el acceso.
Además de utilizar el método filter
en el generador de funciones del enrutador, puede aplicar un
filtro a una función de enrutador existente a través de RouterFunction.filter(
HandlerFilterFunction)
.
CorsFilter
especial.
GO TO FULL VERSION