📘 初识 WebFlux

知识文档

WebFlux 简介

  • WebFlux 是什么 Spring 5 引入的响应式 Web 框架,基于 Reactor(Mono / Flux) ,支持 异步非阻塞,适合高并发场景。

  • 核心特性

    • 基于 Reactive Streams 规范
    • 核心容器使用 Netty / Undertow / Servlet 3.1+ 容器
    • 核心对象:Mono<T>(0-1 个元素)、Flux<T>(0-N 个元素)

Hello word:

1️⃣ pom.xml 依赖(Maven)
xml 复制代码
<dependencies>
    <!-- Spring Boot WebFlux -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    <!-- Lombok (可选,用于简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- 测试依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

2️⃣ 启动类 Application.java
typescript 复制代码
package com.example.hellowebflux;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

3️⃣ Controller 示例 HelloController.java
kotlin 复制代码
package com.example.hellowebflux.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
public class HelloController {
@GetMapping("/hello")
    public Mono<String> hello() {// 返回响应式 Mono
        return Mono.just("Hello, WebFlux!");
    }
}

handler案例:

kotlin 复制代码
package com.example.webfluxhandler.handler;

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
public class HelloHandler {

    public Mono<ServerResponse> hello(ServerRequest request) {
        return ServerResponse.ok()
                .bodyValue("Hello from HandlerFunction!");
    }

    public Mono<ServerResponse> greet(ServerRequest request) {
        String name = request.pathVariable("name");
        return ServerResponse.ok()
                .bodyValue("Hello, " + name + "!");
    }
}

package com.example.webfluxhandler.router;

import com.example.webfluxhandler.handler.HelloHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;

@Configuration
public class HelloRouter {

    @Bean
    public RouterFunction<ServerResponse> router(HelloHandler helloHandler) {
        return route(GET("/hello"), helloHandler::hello)
                .andRoute(GET("/greet/{name}"), helloHandler::greet);
    }
}

4️⃣ 运行
  1. 启动 Application 类。
  2. 访问浏览器或 Postman:http://localhost:8080/hello
  3. 响应会返回:

Hello, WebFlux!


响应式基础概念

2.1 核心类型

  • Mono

    • 表示 0 或 1 个元素
    • 类似 Optional<T>CompletableFuture<T>
  • Flux

    • 表示 0 到 N 个元素
    • 类似 Stream<T>

2.2 常用操作

  • Mono.just(value) / Flux.just(...)

  • Mono.empty() / Flux.empty()

  • Mono.defer(() -> ...):延迟执行

  • 转换操作(Transforming)用于对流中的数据进行转换:

    • map(Function<T,R>) :元素逐个映射(同步转换)。
    • flatMap(Function<T,Publisher>) :将元素映射成新的 Publisher,再展开合并。
    • concatMap(Function<T,Publisher>) :和 flatMap 类似,但保证顺序。
    • filter(Predicate) :过滤掉不符合条件的元素。
    • distinct() :去重。
    • take(n) :取前 n 个元素。
    • skip(n) :跳过前 n 个元素。
  • 组合操作(Combining)用于组合多个 Publisher:

    • concat/concatWith:顺序连接多个流。
    • merge/mergeWith:并发合并多个流(不保证顺序)。
    • zip:按位置合并多个流,打包成 Tuple。
    • startWith:在流前面追加元素。

  • 错误处理操作(Error Handling)WebFlux 强调 信号流,错误也是信号:

    • onErrorReturn(value) :出错时返回默认值。
    • onErrorResume(Function<Throwable,Publisher>) :出错时切换到备用流。
    • onErrorContinue:忽略异常并继续。
    • retry(n) :失败后重试 n 次。

  • 终止操作(Terminal)只有调用了终止操作,Publisher 才会执行:

    • subscribe() :订阅触发执行。
    • block() / blockOptional() :阻塞获取结果(不推荐 WebFlux 中大量使用)。
    • collectList() :把 Flux 收集成一个 List。
    • collectMap() :收集成 Map。
    • then() :忽略数据,只关心完成信号。

  • 上下文操作(Context)Reactor 提供类似 ThreadLocal 的上下文:
  • contextWrite(Context -> Context) :写入上下文数据。
  • deferContextual(ctx -> ...) :读取上下文数据。

  • 调度(Scheduler)控制执行线程:

    • publishOn(scheduler) :切换后续操作执行的线程池。

    • subscribeOn(scheduler) :切换上游执行的线程池。

    • 常见调度器:

      • Schedulers.parallel():并行线程池。
      • Schedulers.boundedElastic():适合阻塞调用。
      • Schedulers.single():单线程。

  • WebFlux 特有操作 结合 Spring WebFlux 常用:

    • ServerRequest/ServerResponse:函数式路由 API。
    • RouterFunction/HandlerFunction:代替传统 Controller。
    • BodyInserters.fromPublisher() :返回 Mono/Flux 到响应体。
    • ServerResponse.ok().body(Publisher,Class) :响应 Publisher。
📌 Reactor 里的延时执行

举个例子:

ini 复制代码
Mono<String> mono = Mono.just("hello");

这行代码已经在内存里立刻创建了一个 "hello",不会延时。 而如果你写成:

go 复制代码
Mono<String> mono = Mono.defer(() -> {
    System.out.println("执行逻辑");return Mono.just("hello");
});

这里的 System.out.println("执行逻辑") 不会立即执行 ,只有在 mono.subscribe() 的时候才执行。

📌 那么 mono.subscribe() 什么时候会执行?
你手动调用 .subscribe()
scss 复制代码
Mono.just("hello")
    .doOnNext(System.out::println)
    .subscribe();  // ← 这里会触发执行

手动调用 .subscribe() 适合:

  • 单元测试
  • 工具类里临时执行
  • 独立的非 WebFlux 环境(比如 main 方法里)

WebFlux Controller 里返回 Mono/Flux
typescript 复制代码
@GetMapping("/hello")
public Mono<String> hello() {return Mono.just("hello webflux");
}

这里你没写 .subscribe(),但请求进来后, ⚡ WebFlux 底层 DispatcherHandler 会帮你调用 subscribe 。 所以 Controller 里 只需要返回 Mono/Flux ,不用手动订阅


WebFilterHandlerFilterFunction 等过滤器里

同样的,Spring WebFlux 内部会在响应阶段统一 subscribe(), 只要你返回 Mono<Void>Mono<ResponseEntity<...>>,它会在链路完成时自动订阅。


在响应式流中(操作符触发)

如果你用 then() / flatMap() 等组合操作符,它们内部也会订阅上游流。

例子:

ini 复制代码
Mono<Void> mono = Mono.just("hello")
    .doOnNext(System.out::println)
    .then();  // then 内部会触发订阅上游

Scheduler / 异步任务里

比如用 publishOn(Schedulers.boundedElastic())subscribeOn(...), 当线程调度器调度时,也会触发订阅。


⚠️ 注意点

  • Controller / Service / Filter不要手动调用 subscribe() , 否则会 提前触发执行,而 WebFlux 框架再执行时就可能出问题(两次订阅、空 body、异常丢失)。

  • 手动 .subscribe() 只适合独立执行的场景,不适合在请求链里用。

  • 转换

    • map:同步一对一转换
    • flatMap:异步转换,返回新的 Mono/Flux
    • filter:过滤元素
    • defaultIfEmpty:空值时提供默认值
  • 错误处理

    • onErrorResume(e -> fallbackMono)
    • onErrorReturn(defaultValue)
    • doOnError(e -> log.error("错误", e))
📌 subscribe 可以重复吗?

可以重复 subscribe,但要分情况:


Cold Publisher(冷流) → 每次订阅都会"重新执行"

例如:

kotlin 复制代码
Mono<String> mono = Mono.fromSupplier(() -> {
    System.out.println("执行逻辑");return "hello";
});
mono.subscribe(System.out::println); // 第一次订阅
mono.subscribe(System.out::println); // 第二次订阅

输出:

复制代码
执行逻辑
hello
执行逻辑
hello

解释:

  • Mono.fromSupplierMono.justFlux.just 这种是 cold 的。
  • 每次订阅 = 从头再来一次
  • 所以适合放置"可以重复"的逻辑(查询 DB、调用接口等)。

Hot Publisher(热流) → 多个订阅者共享同一个数据流

例如:

ini 复制代码
Flux<Long> flux = Flux.interval(Duration.ofSeconds(1)).share();
flux.subscribe(v -> System.out.println("订阅A: " + v));
Thread.sleep(3000);
flux.subscribe(v -> System.out.println("订阅B: " + v));

输出可能是:

makefile 复制代码
订阅A: 0
订阅A: 1
订阅A: 2
订阅B: 2   // B 从当前时间点接收,不会从头开始
订阅A: 3
订阅B: 3

解释:

  • Flux.interval 是一个 hot stream,会持续产生数据。
  • share() 后,多个订阅者共享同一个流。
  • 新的订阅者不会重放历史数据,只能接收"后续"的。

Mono 的特殊情况
  • Mono.just("value") → 每次订阅返回同一个值,可以重复订阅没问题。
  • Mono.fromCallable(...) → 每次订阅都会重新执行 Callable。
  • Mono.cache() → 缓存结果,后续订阅不再重新执行。

例子:

ini 复制代码
Mono<String> cached = Mono.fromSupplier(() -> {
    System.out.println("执行一次");return "hello";
}).cache();
cached.subscribe(System.out::println);
cached.subscribe(System.out::println);

输出:

arduino 复制代码
执行一次
hello
hello   // 第二次没有再执行逻辑

⚠️ 注意点

  1. WebFlux Controller 返回的 Mono/Flux 是冷流,所以每次请求都会重新执行,这是符合预期的。

  2. 如果你希望只执行一次并复用结果 → 用 .cache()

  3. 如果你想广播给多个订阅者 → 用 .share().publish().refCount(...)


WebFlux Controller 基础写法
less 复制代码
@RestController
@RequestMapping("/devices")
public class DeviceController {
@GetMapping("/{id}")
    public Mono<Result<Device>> getDevice(@PathVariable Long id) {return deviceService.findById(id)
                .map(Result::ok)
                .defaultIfEmpty(Result.error("设备不存在"))
                .onErrorResume(e -> {
                    log.error("查询设备异常", e);return Mono.just(Result.error("查询失败: " + e.getMessage()));
                });
    }
@GetMapping("/list")
    public Flux<Device> listDevices() {return deviceService.findAll();
    }
}

Filter / 拦截器

WebFlux 没有 HandlerInterceptor,而是使用 WebFilter

less 复制代码
@Component
@Order(-1)
public class AuthFilter implements WebFilter {@Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {String token = exchange.getRequest().getHeaders().getFirst("Authorization");if (token == null) {return Mono.error(new BusinessException("未登录"));
        }
return chain.filter(exchange)
                .contextWrite(ctx -> ctx.put("auth.token", token)); // 存入 Reactor 上下文
    }
}

在业务代码里取上下文:

go 复制代码
// 从上下文里取出用户信息
return IdeamakeSubjectContext.get()
        .flatMap(user -> {
            log.info("当前用户信息: {}", user);

            // 继续执行业务逻辑
            return deviceService.getDeviceById(id)
                    .map(Result::ok)
                    .defaultIfEmpty(Result.error("设备不存在"));
        })
        .onErrorResume(error -> {
            log.error("获取设备详情失败,ID: {}", id, error);
            return Mono.just(Result.error(STR."获取设备详情失败: {error.getMessage()}"));
        });

Reactor 上下文(Context)
  • 作用:跨层传递用户信息、追踪 ID、日志链路等。
  • 特点 :类似 ThreadLocal,但是支持异步非阻塞。
  • 写入.contextWrite(ctx -> ctx.put("key", value))
  • 读取Mono.deferContextual(ctx -> Mono.just(ctx.get("key")))

异常处理

全局异常处理

java 复制代码
package cn.ideamake.aidee.exception;

import cn.ideamake.common.response.Result;
import cn.ideamake.common.response.ResultEnum;
import com.alibaba.fastjson.JSONObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.stream.Collectors;

/**
 * @author Barcke
 * @version 1.0
 * @projectName aidee
 * @className GlobalExceptionHandler
 * @date 2025/9/10 14:17
 * @slogan: 源于生活 高于生活
 * @description:
 **/
@Slf4j
 @Component
@Order(-2) // 保证优先级高于默认异常处理
@RequiredArgsConstructor
public class GlobalExceptionHandler implements WebExceptionHandler {

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        // 已经提交响应就不再处理
        if (exchange.getResponse().isCommitted()) {
            return Mono.error(ex);
        }

        // 构造返回体
        Result<Object> result;
        if (ex instanceof BusinessException e) {
            // 业务异常
            String stack = Arrays.stream(e.getStackTrace())
                    .limit(10)
                    .map(StackTraceElement::toString)
                    .collect(Collectors.joining("\n"));

            log.error("已知异常-BusinessException: msg={} stack={}", e.getMsg(), stack);

            result = Result.error(e.getCode(), e.getMsg());
        } else {
            // 未知异常
            log.error("全局异常: {}", ex.getMessage(), ex);
            result = Result.error(ResultEnum.FAIL.getCode(), "出错了,请联系管理员!");
        }

        try {
            // 设置响应头(避免 ReadOnlyHttpHeaders 问题)
            exchange.getResponse().getHeaders().add("Content-Type", MediaType.APPLICATION_JSON_VALUE);
            byte[] bytes = JSONObject.toJSONString(result).getBytes(StandardCharsets.UTF_8);

            return exchange.getResponse()
                    .writeWith(Mono.just(exchange.getResponse()
                            .bufferFactory()
                            .wrap(bytes)));
        } catch (Exception e) {
            log.error("处理异常响应时发生错误", e);
            return Mono.error(ex);
        }
    }

}
  1. 局部异常处理

  • 使用 onErrorResume / onErrorReturn

响应式编程常见坑

  1. 避免阻塞

    1. 禁止使用 block() / subscribe() 在 Controller 里
    2. 数据库/Redis/HTTP 调用必须使用响应式客户端(R2DBC, WebClient, reactive-redis)
  2. 线程模型

    1. 默认使用 reactor-netty 事件循环线程
    2. 如果有阻塞操作,用 publishOn(Schedulers.boundedElastic())
  3. 上下文传递

    1. 不要用 ThreadLocal,使用 Reactor Context

📌 总结

WebFlux 基础开发需要掌握:

1. 核心概念

  • 响应式、非阻塞、异步。
  • 核心类型:Mono<T>(0/1个元素)、Flux<T>(0-N个元素)。
  • 基于 Reactor + Netty 事件循环模型。

2. 开发模式

  • 注解式@RestController + Mono/Flux 返回。
  • 函数式RouterFunction + HandlerFunction,更灵活。

3. 常用操作

  • 创建:Mono.just()Flux.fromIterable()
  • 转换:map(同步)、flatMap(异步)
  • 过滤/条件:filterswitchIfEmpty
  • 错误处理:onErrorResumedoOnError
  • 组合操作:zipmergeconcat

4. 上下文与状态

  • 使用 Context 或自定义 SubjectContext 保存请求级信息。
  • 避免阻塞调用和 ThreadLocal。

5. 注意事项

  • 所有阻塞操作需在 Schedulers.boundedElastic() 中执行。
  • Mono/Flux 只有订阅 (subscribe) 才会执行。
  • 错误必须捕获,否则会触发 onErrorDropped 异常。

6. 总结一句话

WebFlux 是一个基于 Reactor 的响应式框架,核心是非阻塞、异步和背压,适合高并发、流式数据处理场景。

相关推荐
JohnYan2 小时前
工作笔记 - 一个浏览器环境适用的类型转换工具
javascript·后端·设计模式
得物技术2 小时前
0基础带你精通Java对象序列化--以Hessian为例|得物技术
java·后端·编程语言
橘子在努力2 小时前
【橘子SpringCloud】OpenFegin源码分析
java·spring boot·spring·spring cloud
十八旬3 小时前
苍穹外卖项目实战(day7-2)-购物车操作功能完善-记录实战教程、问题的解决方法以及完整代码
java·开发语言·windows·spring boot·mysql
喂完待续3 小时前
【序列晋升】31 Spring Cloud App Broker 微服务时代的云服务代理框架
spring·spring cloud·微服务·云原生·架构·big data·序列晋升
Java水解3 小时前
MySQL UPDATE 语句:数据更新操作详解
后端·mysql
Java水解3 小时前
深入浅出:在 Koa 中实现优雅的中间件依赖注入
后端·koa
lssjzmn3 小时前
构建实时消息应用:Spring Boot + Vue 与 WebSocket 的有机融合
java·后端·架构
金銀銅鐵3 小时前
[Java] 浅析可重复注解(Repeatable Annotation) 是如何实现的
java·后端