Spring WebFlux使用深度實(shí)踐指南


Spring WebFlux 是 Spring Framework 5 引入的響應(yīng)式 Web 框架,基于 Project Reactor 實(shí)現(xiàn),支持非阻塞、函數(shù)式編程模型。本節(jié)將深入探討 WebFlux 的核心功能,包括 REST API 構(gòu)建、響應(yīng)式數(shù)據(jù)庫(kù)訪問(wèn)和實(shí)時(shí)通信。
4.3.1 構(gòu)建 Reactive REST API

基礎(chǔ)項(xiàng)目搭建
首先創(chuàng)建 Spring Boot WebFlux 項(xiàng)目(基于 Spring Initializr):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>響應(yīng)式控制器
注解式控制器(與傳統(tǒng)Spring MVC類似但支持響應(yīng)式類型):
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
// 構(gòu)造函數(shù)注入
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public Flux<Product> getAllProducts() {
return productService.findAll();
}
@GetMapping("/{id}")
public Mono<Product> getProductById(@PathVariable String id) {
return productService.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<Product> createProduct(@RequestBody Mono<Product> productMono) {
return productService.save(productMono);
}
@PutMapping("/{id}")
public Mono<Product> updateProduct(
@PathVariable String id,
@RequestBody Mono<Product> productMono) {
return productService.update(id, productMono);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Void> deleteProduct(@PathVariable String id) {
return productService.delete(id);
}
}函數(shù)式端點(diǎn)(RouterFunction方式):
@Configuration
public class ProductRouter {
@Bean
public RouterFunction<ServerResponse> route(ProductHandler handler) {
return RouterFunctions.route()
.GET("/fn/products", handler::getAll)
.GET("/fn/products/{id}", handler::getById)
.POST("/fn/products", handler::create)
.PUT("/fn/products/{id}", handler::update)
.DELETE("/fn/products/{id}", handler::delete)
.build();
}
}
@Component
public class ProductHandler {
private final ProductService productService;
public ProductHandler(ProductService productService) {
this.productService = productService;
}
public Mono<ServerResponse> getAll(ServerRequest request) {
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_NDJSON)
.body(productService.findAll(), Product.class);
}
public Mono<ServerResponse> getById(ServerRequest request) {
String id = request.pathVariable("id");
return productService.findById(id)
.flatMap(product -> ServerResponse.ok().bodyValue(product))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> create(ServerRequest request) {
return request.bodyToMono(Product.class)
.flatMap(productService::save)
.flatMap(product -> ServerResponse
.created(URI.create("/fn/products/" + product.getId()))
.bodyValue(product));
}
public Mono<ServerResponse> update(ServerRequest request) {
String id = request.pathVariable("id");
return request.bodyToMono(Product.class)
.flatMap(product -> productService.update(id, Mono.just(product)))
.flatMap(product -> ServerResponse.ok().bodyValue(product))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> delete(ServerRequest request) {
String id = request.pathVariable("id");
return productService.delete(id)
.then(ServerResponse.noContent().build());
}
}高級(jí)特性

流式響應(yīng)(Server-Sent Events):
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ProductEvent> streamProducts() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> new ProductEvent(
"product-" + sequence,
"Event at " + Instant.now()
));
}
請(qǐng)求驗(yàn)證與異常處理:
@ControllerAdvice
public class GlobalErrorHandler extends AbstractErrorWebExceptionHandler {
public GlobalErrorHandler(ErrorAttributes errorAttributes,
WebProperties.Resources resources,
ApplicationContext applicationContext,
ServerCodecConfigurer serverCodecConfigurer) {
super(errorAttributes, resources, applicationContext);
this.setMessageWriters(serverCodecConfigurer.getWriters());
}
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(
RequestPredicates.all(),
request -> {
Map<String, Object> errorProperties = getErrorAttributes(request, ErrorAttributeOptions.defaults());
HttpStatus status = getHttpStatus(errorProperties);
return ServerResponse.status(status)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(errorProperties);
}
);
}
private HttpStatus getHttpStatus(Map<String, Object> errorProperties) {
return HttpStatus.valueOf((Integer)errorProperties.get("status"));
}
}
// 自定義驗(yàn)證
public class ProductValidator {
public static Mono<Product> validate(Product product) {
return Mono.just(product)
.flatMap(p -> {
List<String> errors = new ArrayList<>();
if (p.getName() == null || p.getName().isEmpty()) {
errors.add("Product name is required");
}
if (p.getPrice() <= 0) {
errors.add("Price must be positive");
}
if (!errors.isEmpty()) {
return Mono.error(new ValidationException(errors));
}
return Mono.just(p);
});
}
}4.3.2 響應(yīng)式數(shù)據(jù)庫(kù)訪問(wèn)(R2DBC)

R2DBC 配置
添加依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-postgresql</artifactId>
<scope>runtime</scope>
</dependency>配置 application.yml:
spring:
r2dbc:
url: r2dbc:postgresql://localhost:5432/mydb
username: user
password: pass
pool:
enabled: true
max-size: 20響應(yīng)式Repository
定義實(shí)體:
@Data
@Table("products")
public class Product {
@Id
private Long id;
private String name;
private String description;
private BigDecimal price;
private Instant createdAt;
}創(chuàng)建Repository接口:
public interface ProductRepository extends ReactiveCrudRepository<Product, Long> {
Flux<Product> findByNameContaining(String name);
@Query("SELECT * FROM products WHERE price > :minPrice")
Flux<Product> findByPriceGreaterThan(BigDecimal minPrice);
@Modifying
@Query("UPDATE products SET price = price * :factor")
Mono<Integer> updateAllPrices(BigDecimal factor);
}復(fù)雜查詢與事務(wù)

自定義查詢實(shí)現(xiàn):
public class ProductRepositoryImpl implements CustomProductRepository {
private final DatabaseClient databaseClient;
public ProductRepositoryImpl(DatabaseClient databaseClient) {
this.databaseClient = databaseClient;
}
@Override
public Flux<Product> complexSearch(ProductCriteria criteria) {
return databaseClient.sql("""
SELECT * FROM products
WHERE name LIKE :name
AND price BETWEEN :minPrice AND :maxPrice
ORDER BY :sortField :sortDirection
LIMIT :limit OFFSET :offset
""")
.bind("name", "%" + criteria.getName() + "%")
.bind("minPrice", criteria.getMinPrice())
.bind("maxPrice", criteria.getMaxPrice())
.bind("sortField", criteria.getSortField())
.bind("sortDirection", criteria.getSortDirection())
.bind("limit", criteria.getPageSize())
.bind("offset", (criteria.getPageNumber() - 1) * criteria.getPageSize())
.map((row, metadata) -> toProduct(row))
.all();
}
private Product toProduct(Row row) {
// 行到對(duì)象的轉(zhuǎn)換邏輯
}
}事務(wù)管理:
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
private final TransactionalOperator transactionalOperator;
public Mono<Void> transferStock(String fromId, String toId, int quantity) {
return transactionalOperator.execute(status ->
productRepository.findById(fromId)
.flatMap(fromProduct -> {
if (fromProduct.getStock() < quantity) {
return Mono.error(new InsufficientStockException());
}
fromProduct.setStock(fromProduct.getStock() - quantity);
return productRepository.save(fromProduct)
.then(productRepository.findById(toId))
.flatMap(toProduct -> {
toProduct.setStock(toProduct.getStock() + quantity);
return productRepository.save(toProduct);
});
})
);
}
}性能優(yōu)化
- 連接池配置:
spring:
r2dbc:
pool:
max-size: 20
initial-size: 5
max-idle-time: 30m- 批處理操作:
public Mono<Integer> batchInsert(List<Product> products) {
return databaseClient.inConnectionMany(connection -> {
Batch batch = connection.createBatch();
products.forEach(product ->
batch.add("INSERT INTO products(name, price) VALUES($1, $2)")
.bind(0, product.getName())
.bind(1, product.getPrice())
);
return Flux.from(batch.execute())
.reduce(0, (count, result) -> count + result.getRowsUpdated());
});
}
4.3.3 WebSocket 實(shí)時(shí)通信

基礎(chǔ)WebSocket配置
配置類:
@Configuration
@EnableWebFlux
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setHandshakeHandler(new DefaultHandshakeHandler())
.setAllowedOrigins("*");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}響應(yīng)式WebSocket處理
股票行情推送示例:
@Controller
public class StockTickerController {
private final Flux<StockQuote> stockQuoteFlux;
public StockTickerController(StockQuoteGenerator quoteGenerator) {
this.stockQuoteFlux = Flux.interval(Duration.ofMillis(500))
.map(sequence -> quoteGenerator.generate())
.share(); // 熱發(fā)布,多個(gè)訂閱者共享數(shù)據(jù)
}
@MessageMapping("stocks.subscribe")
@SendTo("/topic/stocks")
public Flux<StockQuote> subscribe() {
return stockQuoteFlux;
}
@MessageMapping("stocks.filter")
public Flux<StockQuote> filter(@Payload String symbol) {
return stockQuoteFlux.filter(quote ->
quote.getSymbol().equals(symbol));
}
}客戶端連接示例:
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);
stompClient.connect({}, () => {
stompClient.subscribe('/topic/stocks', (message) => {
const quote = JSON.parse(message.body);
updateStockTable(quote);
});
stompClient.send("/app/stocks.filter", {}, "AAPL");
});高級(jí)特性

RSocket集成(更強(qiáng)大的響應(yīng)式協(xié)議):
@Controller
@MessageMapping("stock.service")
public class RSocketStockController {
@MessageMapping("current")
public Mono<StockQuote> current(String symbol) {
return stockService.getCurrent(symbol);
}
@MessageMapping("stream")
public Flux<StockQuote> stream(String symbol) {
return stockService.getStream(symbol);
}
@MessageMapping("channel")
public Flux<StockQuote> channel(Flux<String> symbols) {
return symbols.flatMap(stockService::getStream);
}
}背壓控制:
@MessageMapping("large.data.stream")
public Flux<DataChunk> largeDataStream() {
return dataService.streamLargeData()
.onBackpressureBuffer(50, // 緩沖區(qū)大小
chunk -> log.warn("Dropping chunk due to backpressure"));
}
安全配置
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange()
.pathMatchers("/ws/**").authenticated()
.anyExchange().permitAll()
.and()
.httpBasic()
.and()
.csrf().disable()
.build();
}
@Bean
public MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User.withUsername("user")
.password("{noop}password")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user);
}
}性能監(jiān)控與最佳實(shí)踐

監(jiān)控端點(diǎn)配置
management:
endpoints:
web:
exposure:
include: health, metrics, prometheus
metrics:
tags:
application: ${spring.application.name}響應(yīng)式應(yīng)用監(jiān)控
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags("application", "reactive-demo");
}
// 自定義指標(biāo)
@Bean
public WebFilter metricsWebFilter(MeterRegistry registry) {
return (exchange, chain) -> {
String path = exchange.getRequest().getPath().toString();
Timer.Sample sample = Timer.start(registry);
return chain.filter(exchange)
.doOnSuccessOrError((done, ex) -> {
sample.stop(registry.timer("http.requests",
"uri", path,
"status", exchange.getResponse().getStatusCode().toString(),
"method", exchange.getRequest().getMethodValue()));
});
};
}最佳實(shí)踐總結(jié)
- 線程模型理解:
- WebFlux 默認(rèn)使用 Netty 事件循環(huán)線程
- 阻塞操作必須使用
publishOn切換到彈性線程池
- 背壓策略選擇:
- UI 客戶端:使用
onBackpressureDrop或onBackpressureLatest - 服務(wù)間通信:使用
onBackpressureBuffer配合合理緩沖區(qū)大小
- UI 客戶端:使用
- 錯(cuò)誤處理原則:
- 盡早處理錯(cuò)誤
- 為每個(gè) Flux/Mono 鏈添加錯(cuò)誤處理
- 區(qū)分業(yè)務(wù)異常和系統(tǒng)異常
- 測(cè)試策略:
- 使用
StepVerifier測(cè)試響應(yīng)式流 - 使用
WebTestClient測(cè)試控制器 - 虛擬時(shí)間測(cè)試長(zhǎng)時(shí)間操作
- 使用
- 性能調(diào)優(yōu):
- 合理配置連接池
- 監(jiān)控關(guān)鍵指標(biāo)(延遲、吞吐量、資源使用率)
- 使用響應(yīng)式日志框架(如 Logback 異步Appender)
通過(guò)以上全面實(shí)踐,您將能夠構(gòu)建高性能、可擴(kuò)展的響應(yīng)式 Web 應(yīng)用,充分利用 WebFlux 的非阻塞特性,處理高并發(fā)場(chǎng)景下的各種挑戰(zhàn)。
到此這篇關(guān)于Spring WebFlux 深度實(shí)踐指南的文章就介紹到這了,更多相關(guān)Spring WebFlux 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring WebFlux簡(jiǎn)介使用場(chǎng)景及完整使用示例
- spring?webflux響應(yīng)式編程使用詳解
- Spring WebFlux使用函數(shù)式編程模型構(gòu)建異步非阻塞服務(wù)
- springboot webflux 過(guò)濾器(使用RouterFunction實(shí)現(xiàn))
- SpringBoot?Webflux創(chuàng)建TCP/UDP?server并使用handler解析數(shù)據(jù)
- Spring Boot webflux使用方法解析
- SpringBoot2使用WebFlux函數(shù)式編程的方法
- 使用 Spring Boot 2.0 + WebFlux 實(shí)現(xiàn) RESTful API功能
相關(guān)文章
IDEA加載項(xiàng)目沒(méi)有src目錄的問(wèn)題及解決
這篇文章主要介紹了IDEA加載項(xiàng)目沒(méi)有src目錄的問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
Guava自動(dòng)加載緩存LoadingCache使用實(shí)戰(zhàn)詳解
這篇文章主要為大家介紹了Guava自動(dòng)加載緩存LoadingCache使用實(shí)戰(zhàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
SQL注入攻擊及其在SpringBoot中使用MyBatisPlus的防范策略的方法
本文介紹了如何使用SpringBoot整合JavaDeeplearning4j構(gòu)建一個(gè)文本摘要生成系統(tǒng),該系統(tǒng)能夠自動(dòng)從長(zhǎng)篇文本中提取關(guān)鍵信息,生成簡(jiǎn)潔的摘要,幫助用戶快速了解文本的主要內(nèi)容,系統(tǒng)使用LSTM神經(jīng)網(wǎng)絡(luò)模型進(jìn)行訓(xùn)練,并通過(guò)SpringBoot創(chuàng)建RESTful?API進(jìn)行調(diào)用2024-11-11
SpringBoot整合RocketMQ的詳細(xì)過(guò)程
這篇文章主要介紹了SpringBoot整合RocketMQ的詳細(xì)過(guò)程,本文分為三部分,第一部分實(shí)現(xiàn)SpringBoot與RocketMQ的整合,第二部分解決在使用RocketMQ過(guò)程中可能遇到的一些問(wèn)題并解決他們,第三部分介紹如何封裝RocketMQ以便更好地使用,需要的朋友可以參考下2023-04-04
SpringBoot實(shí)現(xiàn)Kafka動(dòng)態(tài)反序列化的完整代碼
在分布式系統(tǒng)中,Kafka作為高吞吐量的消息隊(duì)列,常常需要處理來(lái)自不同主題(Topic)的異構(gòu)數(shù)據(jù),不同的業(yè)務(wù)場(chǎng)景可能要求對(duì)同一消費(fèi)者組內(nèi)的消息采用不同的反序列化策略,本文將深入探討如何在Spring Boot中實(shí)現(xiàn)針對(duì)不同主題的動(dòng)態(tài)反序列化,需要的朋友可以參考下2025-05-05
java實(shí)現(xiàn)簡(jiǎn)單的驗(yàn)證碼功能
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)簡(jiǎn)單的驗(yàn)證碼功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-04-04
java開(kāi)發(fā)RocketMQ生產(chǎn)者高可用示例詳解
這篇文章主要為大家介紹了java開(kāi)發(fā)RocketMQ生產(chǎn)者高可用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
Mybatis實(shí)現(xiàn)Mapper動(dòng)態(tài)代理方式詳解
這篇文章主要為大家詳細(xì)介紹了Mybatis實(shí)現(xiàn)Mapper動(dòng)態(tài)代理方式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
Java連接SAP RFC實(shí)現(xiàn)數(shù)據(jù)抽取的示例詳解
這篇文章主要為大家學(xué)習(xí)介紹了Java如何連接SAP RFC實(shí)現(xiàn)數(shù)據(jù)抽取的功能,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,需要的可以了解下2023-08-08

