響應式 Web 應用程式
Spring Boot 透過為 Spring Webflux 提供自動配置來簡化響應式 Web 應用程式的開發。
“Spring WebFlux Framework”
Spring WebFlux 是 Spring Framework 5.0 中引入的新響應式 Web 框架。與 Spring MVC 不同,它不需要 servlet API,是完全非同步和非阻塞的,並透過 Reactor 專案實現了 Reactive Streams 規範。
Spring WebFlux 有兩種風格:函式式和基於註解的。基於註解的風格與 Spring MVC 模型非常相似,如下例所示
-
Java
-
Kotlin
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 是 Spring Framework 的一部分,詳細資訊可在其參考文件中找到。
“WebFlux.fn”,即函式式變體,將路由配置與實際的請求處理分離開來,如下例所示
-
Java
-
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.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)
}
}
-
Java
-
Kotlin
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> {
...
}
fun getUserCustomers(request: ServerRequest?): Mono<ServerResponse> {
...
}
fun deleteUser(request: ServerRequest?): Mono<ServerResponse> {
...
}
}
“WebFlux.fn” 是 Spring Framework 的一部分,詳細資訊可在其參考文件中找到。
你可以根據需要定義任意數量的 RouterFunction Bean,以便模組化路由的定義。如果需要應用優先順序,可以對 Bean 進行排序。 |
要開始使用,請將 spring-boot-starter-webflux
模組新增到你的應用程式中。
在你的應用程式中同時新增 spring-boot-starter-web 和 spring-boot-starter-webflux 模組會導致 Spring Boot 自動配置 Spring MVC,而不是 WebFlux。之所以選擇這種行為,是因為許多 Spring 開發者將 spring-boot-starter-webflux 新增到其 Spring MVC 應用程式中以使用響應式 WebClient 。你仍然可以透過將選擇的應用程式型別設定為 SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE) 來強制執行你的選擇。 |
Spring WebFlux 自動配置
Spring Boot 為 Spring WebFlux 提供了自動配置,這適用於大多數應用程式。
自動配置在 Spring 的預設設定之上添加了以下特性
-
配置
HttpMessageReader
和HttpMessageWriter
例項的編解碼器(本文件後面會描述)。 -
支援提供靜態資源,包括對 WebJars 的支援(本文件後面會描述)。
如果你想保留 Spring Boot WebFlux 的特性並新增額外的 WebFlux 配置,你可以新增自己的 @Configuration
型別的 WebFluxConfigurer
類,但請注意 不要 新增 @EnableWebFlux
。
如果你想對自動配置的 HttpHandler
新增額外的定製,你可以定義 WebHttpHandlerBuilderCustomizer
型別的 Bean,並使用它們來修改 WebHttpHandlerBuilder
。
如果你想完全控制 Spring WebFlux,你可以新增自己的使用 @EnableWebFlux
註解的 @Configuration
。
Spring WebFlux 轉換服務
如果你想定製 Spring WebFlux 使用的 ConversionService
,你可以提供一個帶有 addFormatters
方法的 WebFluxConfigurer
Bean。
轉換也可以使用 spring.webflux.format.*
配置屬性進行定製。未配置時,將使用以下預設值
屬性 | DateTimeFormatter |
格式 |
---|---|---|
|
|
|
|
|
java.time 的 |
|
|
java.time 的 |
HttpMessageReaders 和 HttpMessageWriters 的 HTTP 編解碼器
Spring WebFlux 使用 HttpMessageReader
和 HttpMessageWriter
介面來轉換 HTTP 請求和響應。它們透過 CodecConfigurer
進行配置,根據你的 classpath 中可用的庫來設定合理的預設值。
Spring Boot 為編解碼器提供了專用的配置屬性,即 spring.codec.*
。它還透過使用 CodecCustomizer
例項來應用進一步的定製。例如,spring.jackson.*
配置鍵會應用於 Jackson 編解碼器。
如果你需要新增或定製編解碼器,你可以建立一個自定義的 CodecCustomizer
元件,如下例所示
-
Java
-
Kotlin
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())
}
}
}
你也可以利用 Spring Boot 的自定義 JSON 序列化器和反序列化器。
靜態內容
預設情況下,Spring Boot 從 classpath 中名為 /static
(或 /public
、/resources
或 /META-INF/resources
)的目錄提供靜態內容。它使用 Spring WebFlux 中的 ResourceWebHandler
,因此你可以透過新增自己的 WebFluxConfigurer
並重寫 addResourceHandlers
方法來修改該行為。
預設情況下,資源對映到 /**
,但你可以透過設定 spring.webflux.static-path-pattern
屬性來調整。例如,將所有資源重新定位到 /resources/**
可以如下實現
-
屬性
-
YAML
spring.webflux.static-path-pattern=/resources/**
spring:
webflux:
static-path-pattern: "/resources/**"
你還可以透過使用 spring.web.resources.static-locations
定製靜態資源的位置。這樣做會將預設值替換為目錄位置列表。如果你這樣做,預設的歡迎頁檢測將切換到你的自定義位置。因此,如果你的任何位置在啟動時有 index.html
,它將成為應用程式的主頁。
除了前面列出的“標準”靜態資源位置之外,Webjars 內容是一個特殊情況。預設情況下,路徑在 /webjars/**
中的任何資源如果以 Webjars 格式打包,都會從 jar 檔案中提供。路徑可以使用 spring.webflux.webjars-path-pattern
屬性進行定製。
Spring WebFlux 應用程式不嚴格依賴於 servlet API,因此它們不能部署為 war 檔案,也不使用 src/main/webapp 目錄。 |
歡迎頁
Spring Boot 支援靜態和模板化的歡迎頁。它首先在配置的靜態內容位置中查詢 index.html
檔案。如果沒有找到,則查詢 index
模板。如果找到其中任何一個,它將自動用作應用程式的歡迎頁。
這僅作為應用程式定義的實際索引路由的後備。順序由 HandlerMapping
Bean 的順序決定,預設情況下順序如下
|
使用 |
|
在 |
歡迎頁的 |
歡迎頁支援 |
模板引擎
除了 REST Web 服務之外,你還可以使用 Spring WebFlux 提供動態 HTML 內容。Spring WebFlux 支援多種模板技術,包括 Thymeleaf、FreeMarker 和 Mustache。
Spring Boot 包含對以下模板引擎的自動配置支援
並非所有 FreeMarker 特性都支援 WebFlux。有關更多詳細資訊,請檢視每個屬性的描述。 |
當你使用預設配置的其中一個模板引擎時,你的模板會從 src/main/resources/templates
中自動獲取。
錯誤處理
Spring Boot 提供了一個 WebExceptionHandler
,它以合理的方式處理所有錯誤。它在處理順序中的位置緊接在 WebFlux 提供的處理程式之前,WebFlux 的處理程式被視為最後處理。對於機器客戶端,它會生成一個 JSON 響應,其中包含錯誤詳情、HTTP 狀態和異常訊息。對於瀏覽器客戶端,有一個“白標籤”錯誤處理程式,它以 HTML 格式呈現相同的資料。你還可以提供自己的 HTML 模板來顯示錯誤(參見下一節)。
在直接定製 Spring Boot 中的錯誤處理之前,你可以利用 Spring WebFlux 中對 RFC 9457 Problem Details 的支援。Spring WebFlux 可以使用 application/problem+json
媒體型別生成自定義錯誤訊息,例如
{
"type": "https://example.org/problems/unknown-project",
"title": "Unknown project",
"status": 404,
"detail": "No project found for id 'spring-unknown'",
"instance": "/projects/spring-unknown"
}
可以透過將 spring.webflux.problemdetails.enabled
設定為 true
來啟用此支援。
定製此特性的第一步通常涉及使用現有機制,但替換或增強錯誤內容。為此,你可以新增一個 ErrorAttributes
型別的 Bean。
要更改錯誤處理行為,你可以實現 ErrorWebExceptionHandler
並註冊該型別的 Bean 定義。由於 ErrorWebExceptionHandler
級別較低,Spring Boot 還提供了一個方便的 AbstractErrorWebExceptionHandler
,讓你能夠以 WebFlux 函式式的方式處理錯誤,如下例所示
-
Java
-
Kotlin
import reactor.core.publisher.Mono;
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.http.codec.ServerCodecConfigurer;
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, WebProperties webProperties,
ApplicationContext applicationContext, ServerCodecConfigurer serverCodecConfigurer) {
super(errorAttributes, webProperties.getResources(), applicationContext);
setMessageReaders(serverCodecConfigurer.getReaders());
setMessageWriters(serverCodecConfigurer.getWriters());
}
@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);
// ... additional builder calls
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.http.codec.ServerCodecConfigurer
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, webProperties: WebProperties,
applicationContext: ApplicationContext, serverCodecConfigurer: ServerCodecConfigurer
) : AbstractErrorWebExceptionHandler(errorAttributes, webProperties.resources, applicationContext) {
init {
setMessageReaders(serverCodecConfigurer.readers)
setMessageWriters(serverCodecConfigurer.writers)
}
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)
// ... additional builder calls
return builder.build()
}
}
為了獲得更完整的檢視,你還可以直接繼承 DefaultErrorWebExceptionHandler
並重寫特定方法。
在某些情況下,控制器級別處理的錯誤不會被 Web 觀察或指標基礎設施記錄。應用程式可以透過在觀察上下文中設定處理的異常來確保這些異常與觀察一起被記錄。
自定義錯誤頁
如果你想為給定的狀態碼顯示自定義 HTML 錯誤頁,你可以新增從 error/*
解析的檢視,例如透過將檔案新增到 /error
目錄。錯誤頁可以是靜態 HTML(即,新增到任何靜態資源目錄下)或使用模板構建。檔名應該是準確的狀態碼、狀態碼系列掩碼,或者在沒有其他匹配項時使用 error
作為預設值。請注意,預設錯誤檢視的路徑是 error/error
,而 Spring MVC 的預設錯誤檢視是 error
。
例如,要將 404
對映到靜態 HTML 檔案,你的目錄結構應如下所示
src/
+- main/
+- java/
| + <source code>
+- resources/
+- public/
+- error/
| +- 404.html
+- <other public assets>
要使用 Mustache 模板對映所有 5xx
錯誤,你的目錄結構應如下所示
src/
+- main/
+- java/
| + <source code>
+- resources/
+- templates/
+- error/
| +- 5xx.mustache
+- <other templates>
Web 過濾器
Web 過濾器 | 順序 |
---|---|
|
|
|
嵌入式響應式伺服器支援
Spring Boot 包含對以下嵌入式響應式 Web 伺服器的支援:Reactor Netty、Tomcat、Jetty 和 Undertow。大多數開發者使用相應的 starter 獲取完全配置的例項。預設情況下,嵌入式伺服器在 8080 埠監聽 HTTP 請求。
定製響應式伺服器
常見的響應式 Web 伺服器設定可以使用 Spring Environment
屬性進行配置。通常,你會在 application.properties
或 application.yaml
檔案中定義這些屬性。
常見的伺服器設定包括
Spring Boot 儘可能地暴露通用設定,但這並非總是可行。對於這些情況,諸如 server.netty.*
等專用名稱空間提供了特定於伺服器的定製。
完整的列表請參見 ServerProperties 類。 |
程式設計方式定製
如果你需要以程式設計方式配置響應式 Web 伺服器,可以註冊一個實現 WebServerFactoryCustomizer
介面的 Spring Bean。WebServerFactoryCustomizer
提供了對 ConfigurableReactiveWebServerFactory
的訪問,其中包含許多定製 setter 方法。以下示例展示了以程式設計方式設定埠
-
Java
-
Kotlin
import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
@Component
public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableReactiveWebServerFactory> {
@Override
public void customize(ConfigurableReactiveWebServerFactory server) {
server.setPort(9000);
}
}
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory
import org.springframework.stereotype.Component
@Component
class MyWebServerFactoryCustomizer : WebServerFactoryCustomizer<ConfigurableReactiveWebServerFactory> {
override fun customize(server: ConfigurableReactiveWebServerFactory) {
server.setPort(9000)
}
}
JettyReactiveWebServerFactory
、NettyReactiveWebServerFactory
、TomcatReactiveWebServerFactory
和 UndertowReactiveWebServerFactory
是 ConfigurableReactiveWebServerFactory
的專用變體,它們分別提供了針對 Jetty、Reactor Netty、Tomcat 和 Undertow 的額外定製 setter 方法。以下示例展示瞭如何定製提供訪問 Reactor Netty 特定配置選項的 NettyReactiveWebServerFactory
-
Java
-
Kotlin
import java.time.Duration;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
@Component
public class MyNettyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
@Override
public void customize(NettyReactiveWebServerFactory factory) {
factory.addServerCustomizers((server) -> server.idleTimeout(Duration.ofSeconds(20)));
}
}
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.stereotype.Component
import java.time.Duration
@Component
class MyNettyWebServerFactoryCustomizer : WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
override fun customize(factory: NettyReactiveWebServerFactory) {
factory.addServerCustomizers({ server -> server.idleTimeout(Duration.ofSeconds(20)) })
}
}
直接定製 ConfigurableReactiveWebServerFactory
對於需要繼承 ReactiveWebServerFactory
的更高階用例,你可以自己暴露一個此類 Bean。
為許多配置選項提供了 setter 方法。如果需要進行更特別的操作,還提供了幾個受保護的方法“hook”。詳細資訊請參見 ConfigurableReactiveWebServerFactory
API 文件。
自動配置的定製器仍會應用於你的自定義工廠,因此請謹慎使用此選項。 |
響應式伺服器資源配置
當自動配置 Reactor Netty 或 Jetty 伺服器時,Spring Boot 將建立特定的 bean,這些 bean 將為伺服器例項提供 HTTP 資源:ReactorResourceFactory
或 JettyResourceFactory
。
預設情況下,這些資源也將與 Reactor Netty 和 Jetty 客戶端共享,以實現最佳效能,前提是:
-
伺服器和客戶端使用相同的技術
-
客戶端例項是使用 Spring Boot 自動配置的
WebClient.Builder
bean 構建的
開發者可以透過提供自定義的 ReactorResourceFactory
或 JettyResourceFactory
bean 來覆蓋 Jetty 和 Reactor Netty 的資源配置 - 這將同時應用於客戶端和伺服器。
您可以在WebClient 執行時部分了解更多關於客戶端資源配置的資訊。