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

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/users")
public class MyRestController {
    private final UserRepository userRepository;
    private final CustomerRepository customerRepository;
    public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) {
        this.userRepository = userRepository;
        this.customerRepository = customerRepository;
    }
    @GetMapping("/{userId}")
    public Mono<User> getUser(@PathVariable Long userId) {
        return this.userRepository.findById(userId);
    }
    @GetMapping("/{userId}/customers")
    public Flux<Customer> getUserCustomers(@PathVariable Long userId) {
        return this.userRepository.findById(userId).flatMapMany(this.customerRepository::findByUser);
    }
    @DeleteMapping("/{userId}")
    public Mono<Void> deleteUser(@PathVariable Long userId) {
        return this.userRepository.deleteById(userId);
    }
}
Kotlin

import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@RestController
@RequestMapping("/users")
class MyRestController(private val userRepository: UserRepository, private val customerRepository: CustomerRepository) {
    @GetMapping("/{userId}")
    fun getUser(@PathVariable userId: Long): Mono<User?> {
        return userRepository.findById(userId)
    }
    @GetMapping("/{userId}/customers")
    fun getUserCustomers(@PathVariable userId: Long): Flux<Customer> {
        return userRepository.findById(userId).flatMapMany { user: User? ->
            customerRepository.findByUser(user)
        }
    }
    @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

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration(proxyBeanMethods = false)
public class MyRoutingConfiguration {
    private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);
    @Bean
    public RouterFunction<ServerResponse> monoRouterFunction(MyUserHandler userHandler) {
        return route()
                .GET("/{user}", ACCEPT_JSON, userHandler::getUser)
                .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
                .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
                .build();
    }
}
Kotlin

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.server.RequestPredicates.DELETE
import org.springframework.web.reactive.function.server.RequestPredicates.GET
import org.springframework.web.reactive.function.server.RequestPredicates.accept
import org.springframework.web.reactive.function.server.RouterFunction
import 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}").and(ACCEPT_JSON), userHandler::getUser).andRoute(
            GET("/{user}/customers").and(ACCEPT_JSON), userHandler::getUserCustomers).andRoute(
            DELETE("/{user}").and(ACCEPT_JSON), userHandler::deleteUser)
    }
    companion object {
        private val ACCEPT_JSON = accept(MediaType.APPLICATION_JSON)
    }
}
Java

import reactor.core.publisher.Mono;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
@Component
public class MyUserHandler {
    public Mono<ServerResponse> getUser(ServerRequest request) {
        ...
    }
    public Mono<ServerResponse> getUserCustomers(ServerRequest request) {
        ...
    }
    public Mono<ServerResponse> deleteUser(ServerRequest request) {
        ...
    }
}
Kotlin

import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono
@Component
class MyUserHandler {
    fun getUser(request: ServerRequest?): Mono<ServerResponse> {
        return ServerResponse.ok().build()
    }
    fun getUserCustomers(request: ServerRequest?): Mono<ServerResponse> {
        return ServerResponse.ok().build()
    }
    fun deleteUser(request: 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 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

import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.ServerSentEventHttpMessageReader;
@Configuration(proxyBeanMethods = false)
public class MyCodecsConfiguration {
    @Bean
    public CodecCustomizer myCodecCustomizer() {
        return (configurer) -> {
            configurer.registerDefaults(false);
            configurer.customCodecs().register(new ServerSentEventHttpMessageReader());
            // ...
        };
    }
}
Kotlin

import org.springframework.boot.web.codec.CodecCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.http.codec.CodecConfigurer
import org.springframework.http.codec.ServerSentEventHttpMessageReader
class MyCodecsConfiguration {
    @Bean
    fun myCodecCustomizer(): CodecCustomizer {
        return CodecCustomizer { configurer: 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

import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.web.WebProperties.Resources;
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder;
@Component
public class MyErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
    public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, Resources resources,
            ApplicationContext applicationContext) {
        super(errorAttributes, resources, applicationContext);
    }
    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml);
    }
    private boolean acceptsXml(ServerRequest request) {
        return request.headers().accept().contains(MediaType.APPLICATION_XML);
    }
    public Mono<ServerResponse> handleErrorAsXml(ServerRequest request) {
        BodyBuilder builder = ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR);
        // ... llamadas adicionales a la herramienta de construcción return builder.build();
        return builder.build();
    }
}
Kotlin

import org.springframework.boot.autoconfigure.web.WebProperties
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler
import org.springframework.boot.web.reactive.error.ErrorAttributes
import org.springframework.context.ApplicationContext
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.RouterFunctions
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono
@Component
class MyErrorWebExceptionHandler(errorAttributes: ErrorAttributes?, resources: WebProperties.Resources?,
    applicationContext: ApplicationContext?) : AbstractErrorWebExceptionHandler(errorAttributes, resources, applicationContext) {
    override fun getRoutingFunction(errorAttributes: ErrorAttributes): RouterFunction<ServerResponse> {
        return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml)
    }
    private fun acceptsXml(request: ServerRequest): Boolean {
        return request.headers().accept().contains(MediaType.APPLICATION_XML)
    }
    fun handleErrorAsXml(request: ServerRequest?): Mono<ServerResponse> {
        val builder = ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
        // ... llamadas adicionales a la herramienta de construcción return builder.build();
        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

import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.reactive.result.view.Rendering;
import org.springframework.web.server.ServerWebExchange;
@Controller
public class MyExceptionHandlingController {
    @GetMapping("/profile")
    public Rendering userProfile() {
        // ...
        throw new IllegalStateException();
    }
    @ExceptionHandler(IllegalStateException.class)
    public Rendering handleIllegalState(ServerWebExchange exchange, IllegalStateException exc) {
        exchange.getAttributes().putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc);
        return Rendering.view("errorView").modelAttribute("message", exc.getMessage()).build();
    }
}
Kotlin

import org.springframework.boot.web.reactive.error.ErrorAttributes
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.reactive.result.view.Rendering
import org.springframework.web.server.ServerWebExchange
@Controller
class MyExceptionHandlingController {
    @GetMapping("/profile")
    fun userProfile(): Rendering {
        // ...
        throw IllegalStateException()
    }
    @ExceptionHandler(IllegalStateException::class)
    fun handleIllegalState(exchange: ServerWebExchange, exc: IllegalStateException): Rendering {
        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/
     |   + <source code>
     +- resources/
         +- public/
             +- error/
             |   +- 404.html
             +- <other public assets>

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

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- templates/
             +- error/
             |   +- 5xx.mustache
             +- <other templates>

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