📘 初识 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 的响应式框架,核心是非阻塞、异步和背压,适合高并发、流式数据处理场景。

相关推荐
Chenyiax10 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH10 小时前
Koa和Express的区别
后端
MariaH10 小时前
Koa框架的使用
后端
luckdewei11 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某13 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy13 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom13 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
用户14748530797417 小时前
CodeX使用Skill生成游戏美术和音乐资源,一分钟入门
后端
Melody12317 小时前
用 abort 中断 AI 流式请求,我之前做错了
后端
onething36518 小时前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 5 —— SSE 流式输出 + 打字机效果
人工智能·后端·全栈