Spring Boot simplifica el desarrollo de aplicaciones web reactivas al proporcionar configuración automática para Spring Webflux.

Spring WebFlux Framework

Spring WebFlux es un nuevo marco web reactivo introducido en Spring Framework 5.0. A diferencia de Spring MVC, no requiere una API de servlet, es completamente asincrónico y sin bloqueo, e implementa Reactive Streams especificación a través de Project Reactor.

Spring WebFlux viene en dos versiones: basado en modelos funcionales y basado en anotaciones. El modelo basado en anotaciones es bastante parecido al modelo Spring MVC, como se muestra en el siguiente ejemplo:

Java
importar reactor.core.publisher.Flux; importar reactor.core.publisher.Mono; importar org.springframework.web.bind.annotation.DeleteMapping; importar org.springframework.web.bind.annotation.GetMapping; importar org.springframework.web.bind.annotation.PathVariable; importar org.springframework.web.bind.annotation.RequestMapping; importar org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/users") clase pública MyRestController { UserRepository final privado userRepository; CustomerRepository final privado customerRepository; public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) { this.userRepository = userRepository; this.customerRepository = customerRepository; } @GetMapping("/{userId}") public Mono<Usuario> getUser(@PathVariable Long userId) { return this.userRepository.findById(userId); } @GetMapping("/{userId}/clientes") public Flux<Cliente> getUserCustomers(@PathVariable Long userId) { return this.userRepository.findById(userId).flatMapMany(this.customerRepository::findByUser); } @DeleteMapping("/{userId}") public Mono<Void> eliminarUsuario(@PathVariable Long ID de usuario) { return this.userRepository.deleteById(userId); } } 
Kotlin
importar org.springframework.web.bind.annotation.DeleteMapping importar org.springframework.web.bind.annotation.GetMapping importar org.springframework.web.bind.annotation.PathVariable importar org.springframework.web.bind.annotation.RequestMapping importar org.springframework.web.bind.annotation.RestController importar reactor.core.publisher.Flux importar reactor.core.publisher.Mono @RestController @RequestMapping("/users ") class MyRestController(valor privado userRepository: UserRepository, valor privado customerRepository: CustomerRepository) { @GetMapping("/{userId}") fun getUser(@PathVariable userId: Long): Mono<Usuario?> { return userRepository.findById(userId) } @GetMapping("/{userId}/customers") fun getUserCustomers(@PathVariable userId: Long): Flux<Cliente> { return userRepository.findById(userId).flatMapMany { usuario: ¿Usuario? -> customerRepository.findByUser(usuario) } } @DeleteMapping("/{userId}") fun deleteUser(@PathVariable userId: Long): Mono<Void> { return userRepository.deleteById(userId) } } 

"WebFlux.fn", una variante basada en el modelo funcional, desacopla el enrutamiento configuración del procesamiento de solicitudes real, como se muestra en el siguiente ejemplo:

Java
importar org. springframework.context .annotation.Bean; importar org.springframework.context.annotation.Configuration; importar org.springframework.http.MediaType; importar org.springframework.web.reactive.function.server.RequestPredicate; importar org.springframework.web.reactive.function.server.RouterFunction; importar org.springframework.web.reactive.function.server.ServerResponse; importar org.springframework.web.reactive.function.server.RequestPredicates.accept estático; importar estática org.springframework.web.reactive.function.server.RouterFunctions.route; @Configuration(proxyBeanMethods = false) clase pública MyRoutingConfiguration { RequestPredicate final estático privado ACCEPT_JSON = aceptar(MediaType.APPLICATION_JSON); @Bean public RouterFunction<ServerResponse> monoRouterFunction(MyUserHandler userHandler) { ruta de retorno() .GET("/{usuario}", ACCEPT_JSON, userHandler::getUser) .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers) .DELETE(" /{usuario}", ACCEPT_JSON, userHandler::deleteUser) .build(); } } 
Kotlin
importar org.springframework.context.annotation.Bean importar org.springframework.context.annotation.Configuración importar org.springframework.http.MediaType importar org.springframework.web.reactive.function.server.RequestPredicates.DELETE importar org.springframework .web.reactive.function.server.RequestPredicates.GET importar org.springframework.web.reactive.function.server.RequestPredicates.accept importar org.springframework.web.reactive.function.server.RouterFunction importar org.springframework.web.reactive .function.server.RouterFunctions import org.springframework.web.reactive.function.server.ServerResponse @Configuration(proxyBeanMethods = false) class MyRoutingConfiguration { @Bean fun monoRouterFunction(userHandler : MyUserHandler): RouterFunction<ServerResponse> { return RouterFunctions.route( GET("/{user}").y(ACCEPT_JSON), userHandler::getUser).andRoute( GET("/{user}/customers").y(ACCEPT_JSON), userHandler::getUserCustomers ).andRoute( DELETE("/{user}").and(ACCEPT_JSON), userHandler::deleteUser) } objeto complementario { valor privado ACCEPT_JSON = aceptar(MediaType.APPLICATION_JSON) } } 
Java
importar reactor.core.publisher.Mono; importar org.springframework.stereotype.Component; importar org.springframework.web.reactive.function.server.ServerRequest; importar org.springframework.web.reactive.function.server.ServerResponse; @Component public class MyUserHandler { public Mono<ServerResponse> getUser(solicitud ServerRequest) { ... } public Mono<ServerResponse> getUserCustomers(solicitud ServerRequest) { ... } public Mono<ServerResponse> eliminarUsuario(solicitud ServerRequest) { ... } } 
Kotlin
importar org.springframework.stereotype.Component importar org.springframework.web.reactive.function.server.ServerRequest importar org.springframework.web.reactive.function.server.ServerResponse importar reactor. core.publisher.Mono @Component class MyUserHandler { fun getUser(solicitud: ServerRequest?): Mono<ServerResponse> { return ServerResponse.ok().build() } fun getUserCustomers(solicitud: ServerRequest?): Mono<ServerResponse> { return ServerResponse.ok().build() } divertido eliminarUsuario(solicitud: ServerRequest?): Mono<ServerResponse> { return ServerResponse.ok().build() } } 
Para Para modular la definición del enrutador, puede definir tantos beans RouterFunction como desee. Los beans se pueden ordenar si desea utilizar el orden de ejecución.

Para comenzar, agregue el módulo spring-boot-starter-webflux a su aplicación.

Agregar los módulos spring-boot-starter-web y spring-boot-starter-webflux a su aplicación Porque Spring Boot configurará automáticamente Spring MVC, no WebFlux. Se eligió esta lógica porque muchos desarrolladores de Spring agregan spring-boot-starter-webflux a sus aplicaciones Spring MVC para usar un WebClient reactivo. Aún puede elegir usted mismo configurando el tipo de aplicación seleccionada en SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE).

Configuración automática de Spring WebFlux

Spring Boot proporciona configuración automática para Spring WebFlux, que funciona muy bien con la mayoría de las aplicaciones.

La configuración automática ofrece las siguientes características además de la configuración predeterminada de Spring:

  • Configuración de códecs para instancias de HttpMessageReader

  • Configuración de códecs para instancias de HttpMessageReader y HttpMessageWriter.

  • Soporte para el manejo de recursos estáticos, incluido el soporte para WebJar.

Si desea conservar las capacidades de Spring Boot WebFlux y agregar configuración WebFlux adicional, puede agregar su propia clase marcada con la anotación @Configuration como WebFluxConfigurer, pero sin @EnableWebFlux anotaciones.

Si necesita un control total sobre Spring WebFlux, puede agregar su propio @Configuration anotado con @EnableWebFlux .

Códecs HTTP a través de HttpMessageReaders y HttpMessageWriters

Spring WebFlux utiliza las interfaces HttpMessageReader y HttpMessageWriter para traducir solicitudes y respuestas HTTP. Estos se configuran usando CodecConfigurer con valores adecuados mirando las bibliotecas disponibles en su classpath.

Spring Boot proporciona propiedades de configuración especializadas para códecs, spring.codec. *. El marco también aplica una mayor personalización a través de instancias CodecCustomizer. Por ejemplo, las claves de configuración spring.jackson.* se aplican al códec de la biblioteca Jackson.

Si necesita agregar o configurar códecs, puede crear un componente personalizado CodecCustomizer, como se muestra en el siguiente ejemplo:

Java
importar organización .springframework.boot.web.codec .CodecCustomizer; importar org.springframework.context.annotation.Bean; importar org.springframework.context.annotation.Configuration; importar org.springframework.http.codec.ServerSentEventHttpMessageReader; @Configuration(proxyBeanMethods = false) clase pública MyCodecsConfiguration { @Bean public CodecCustomizer myCodecCustomizer() { return (configurer) -> {configurer.registerDefaults(false); configurer.customCodecs().register(new ServerSentEventHttpMessageReader()); //... }; } } 
Kotlin
importar org.springframework.boot.web.codec.CodecCustomizer importar org.springframework.context.annotation.Bean importar org.springframework.http.codec.CodecConfigurer importar org.springframework.http.codec.ServerSentEventHttpMessageReader class MyCodecsConfiguration { @Bean fun myCodecCustomizer(): CodecCustomizer { return CodecCustomizer { configurador: CodecConfigurer -> configurer.registerDefaults(false) configurer.customCodecs().register(ServerSentEventHttpMessageReader()) } } } 

Además, puede utilizar serializadores y deserializadores JSON personalizados en Spring Boot.

Contenido estático

De forma predeterminada, Spring Boot procesa contenido estático desde el directorio /static (o /public, o /resources, o /META-INF/resources) en el classpath. El marco utiliza ResourceWebHandler de Spring WebFlux, por lo que puede cambiar esta lógica agregando su propio WebFluxConfigurer y anulando el método addResourceHandlers.

De forma predeterminada, los recursos se asignan a /**, pero puede ajustar esta asignación configurando la propiedad spring.webflux.static-path-pattern. Por ejemplo, mover todos los recursos a /resources/** se puede hacer de la siguiente manera:

Propiedades
spring.webflux.static-path-pattern=/resources/**
Yaml
spring: webflux: static-path-pattern: "/resources/**"

Además, puede configurar las ubicaciones de los recursos estáticos usando spring.web.resources.static-locations. Esto reemplaza los valores predeterminados con una lista de ubicaciones de directorios. Con esta configuración, la detección de la página de inicio predeterminada cambiará a sus ubicaciones personalizadas. Por lo tanto, si hay index.html en cualquiera de sus ubicaciones cuando inicie, se convertirá en la página de inicio de la aplicación.

Además de las ubicaciones de ubicación de recursos estáticas "estándar" mencionado anteriormente, se proporciona un script especial para contenido de Webjars. Cualquier recurso con una ruta en /webjars/** se procesa a partir de archivos jar si están empaquetados en formato Webjars.

Las aplicaciones Spring WebFlux no dependen de la API del servlet, por lo que se pueden implementar como archivos war y no utilizarán el directorio src/main/webapp.

Página de inicio

Spring Boot admite páginas de inicio estáticas y con plantillas. El marco primero busca el archivo index.html en las ubicaciones de contenido estático configuradas. Si no se encuentra dicho patrón, busca el patrón index. Si se encuentra uno de ellos, se usará automáticamente como página de inicio de la aplicación.

Motores de plantillas

Además de los servicios web REST, también puede usar Spring WebFlux para manejar dinámicas. Contenido HTML. Spring WebFlux admite varias tecnologías de plantillas, incluidas Thymeleaf, FreeMarker y Moustache.

Spring Boot proporciona soporte de configuración automática para los siguientes motores de plantillas:

Si uno de estos motores de plantillas se utiliza con la configuración predeterminada, las plantillas se seleccionan automáticamente desde src/main/resources /templates.

Manejo de errores

Spring Boot proporciona un WebExceptionHandler que maneja todos los errores de manera adecuada. Su posición en el orden de procesamiento es inmediatamente anterior a los controladores proporcionados por WebFlux, que se cuentan en último lugar. Para los clientes de la máquina, genera una respuesta JSON con una descripción detallada del error, un código de estado HTTP y un mensaje de excepción. Para los clientes de navegador, existe un controlador de errores de "etiqueta blanca" que representa los mismos datos en formato HTML. También puede proporcionar sus propias plantillas HTML para mostrar errores.

El primer paso para configurar esta función suele ser utilizar el mecanismo existente, excepto reemplazar o agregar contenido al error. Para hacer esto, puede agregar un bean de tipo ErrorAttributes.

Para cambiar la lógica de manejo de errores, puede implementar ErrorWebExceptionHandler y registrar una definición de bean. de este tipo. Debido a que ErrorWebExceptionHandler es de nivel bastante bajo, Spring Boot también proporciona un asistente AbstractErrorWebExceptionHandler que le permite manejar errores de una manera funcional a través de WebFlux, como se muestra en el siguiente ejemplo:

Java
importar reactor.core.publisher.Mono; importar org.springframework.boot.autoconfigure.web.WebProperties.Resources; importar org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler; importar org.springframework.boot.web.reactive.error.ErrorAttributes; importar org.springframework.context.ApplicationContext; importar org.springframework.http.HttpStatus; importar org.springframework.http.MediaType; importar org.springframework.stereotype.Component; importar org.springframework.web.reactive.function.server.RouterFunction; importar org.springframework.web.reactive.function.server.RouterFunctions; importar org.springframework.web.reactive.function.server.ServerRequest; importar org.springframework.web.reactive.function.server.ServerResponse; importar org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder; @Component clase pública MyErrorWebExceptionHandler extiende AbstractErrorWebExceptionHandler { public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, Resources resources, ApplicationContext applicationContext) { super(errorAttributes, resources, applicationContext); } @Override función de enrutador protegida<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml); } booleano privado aceptaXml (solicitud ServerRequest) { return request.headers().accept().contains(MediaType.APPLICATION_XML); } público Mono<ServerResponse> handleErrorAsXml (solicitud ServerRequest) { Constructor BodyBuilder = ServerResponse.status (HttpStatus.INTERNAL_SERVER_ERROR); // ... llamadas adicionales a la herramienta de construcción return builder.build(); } } 
Kotlin
importar org.springframework.boot.autoconfigure.web.WebProperties importar org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler importar org.springframework.boot.web.reactive.error.ErrorAttributes importar org.springframework.context .ApplicationContext importar org.springframework.http.HttpStatus importar org.springframework.http.MediaType importar org.springframework.stereotype.Component importar org.springframework.web.reactive.function.server.RouterFunction importar org.springframework.web.reactive.function .server.RouterFunctions importar org.springframework.web.reactive.function.server.ServerRequest importar org.springframework.web.reactive.function.server.ServerResponse importar reactor.core.publisher.Mono @Component class MyErrorWebExceptionHandler(errorAttributes: ErrorAttributes?, recursos: WebProperties.Resources?, applicationContext: ApplicationContext?): AbstractErrorWebExceptionHandler(errorAttributes, resources, applicationContext) { anular fun getRoutingFunction(errorAttributes: ErrorAttributes): RouterFunction<ServerResponse> { return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml) } diversión privada aceptaXml(solicitud: ServerRequest): booleano { return request.headers().accept().contains(MediaType.APPLICATION_XML) } fun handleErrorAsXml(solicitud : ServerRequest?): Mono<ServerResponse> { val builder = ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR) // ... llamadas adicionales a la herramienta de compilación return builder.build() } } 

Para obtener una imagen más completa, también puede crear una subclase DefaultErrorWebExceptionHandler directamente y anular métodos específicos.

En algunos casos, los errores manejados en el nivel de función del controlador o del controlador no son capturados por el marco de métricas. Las aplicaciones pueden garantizar que dichas excepciones se registren en las métricas de solicitud configurando la excepción manejada como un atributo de solicitud:

Java
importar org.springframework.boot.web.reactive.error.ErrorAttributes; importar org.springframework.stereotype.Controller; importar org.springframework.web.bind.annotation.ExceptionHandler; importar org.springframework.web.bind.annotation.GetMapping; importar org.springframework.web.reactive.result.view.Rendering; importar org.springframework.web.server.ServerWebExchange; @Controller public class MyExceptionHandlingController { @GetMapping("/profile") renderizado público userProfile() { // ... throw new IllegalStateException(); } @ExceptionHandler(IllegalStateException.class) Representación pública handleIllegalState(ServerWebExchange exchange, IllegalStateException exc) { exchange.getAttributes().putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc); return Rendering.view("errorView").modelAttribute("mensaje", exc.getMessage()).build(); } } 
Kotlin
importar org.springframework.boot.web.reactive.error.ErrorAttributes importar org.springframework.stereotype.Controller importar org.springframework.web.bind.annotation.ExceptionHandler importar org.springframework.web.bind.annotation.GetMapping importar org .springframework.web.reactive.result.view.Rendering import org.springframework.web.server.ServerWebExchange @Controller clase MyExceptionHandlingController { @GetMapping("/profile") divertido userProfile(): Representación { // ... throw IllegalStateException() } @ExceptionHandler(IllegalStateException::class) fun handleIllegalState(exchange: ServerWebExchange, exc: IllegalStateException): Representación { exchange.attributes.putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc) return Rendering.view("errorView").modelAttribute("message", exc.message ?: "").build() } } 

Páginas de error personalizadas

Si desea mostrar una página de error HTML personalizada para un código de estado determinado, puede agregar un archivo al directorio /error. Las páginas de error pueden ser HTML estático (es decir, agregadas a cualquiera de los directorios de recursos estáticos) o creadas mediante plantillas. El nombre del archivo debe ser el código de estado exacto o la máscara de serie.

Por ejemplo, para mostrar un 404 con un archivo HTML estático, la estructura del directorio debería verse así:

src/ +- main/ +- java/ | + <código fuente> +- recursos/ +- público/ +- error/ | +- 404.html +- <otros activos públicos>

Para mostrar todos los errores 5xx usando la plantilla, estructura Moustache El directorio debería verse así:

src/ +- main / +-java/ | + <código fuente> +- recursos/ +- plantillas/ +- error/ | +- 5xx.mustache +- <otras plantillas>

Filtros web

Spring WebFlux proporciona una interfaz WebFilter, que se puede implementar para filtrar los intercambios de solicitudes y respuestas HTTP. Los beans WebFilter que se encuentran en el contexto de la aplicación se utilizarán automáticamente para filtrar cada instancia de comunicación.

Si el orden de los filtros es importante, pueden implementar el método Ordered clase o también se pueden marcar con la anotación @Order. La configuración automática de Spring Boot puede configurar filtros web por usted. En este caso, se utilizarán los métodos de pedido que se muestran en la siguiente tabla:

Filtro web Pedido

MetricsWebFilter

Ordenado.HIGHEST_PRECEDENCE + 1

WebFilterChainProxy (Seguridad de primavera)

-100

HttpTraceWebFilter

Ordenado.LOWEST_PRECEDENCE - 10