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:
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);
}
}
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:
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();
}
}
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)
}
}
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) {
...
}
}
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()
}
}
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.
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
yHttpMessageWriter
.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:
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());
// ...
};
}
}
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:
spring.webflux.static-path-pattern=/resources/**
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.
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:
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();
}
}
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:
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();
}
}
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 |
---|---|
|
|
|
|
|
|