深入 Project Reactor:从原理到工程实践的全面指南

Project Reactor 是Java响应式编程库 ,提供MonoFlux 核心类型,支持非阻塞、背压及异步数据流处理 。它是Spring WebFlux 的基础,适用于构建高并发低延迟的微服务与事件驱动应用,遵循Reactive Streams规范。

1. 响应式编程入门:从阻塞困境到数据流之美

2. 深入 Project Reactor:从原理到工程实践的全面指南

3. Flux 与 Mono:Project Reactor 核心响应式类型深度解析

4. Mono:Project Reactor 中最精巧的响应式原语

5. 创建 Flux/Mono 并订阅:Project Reactor 响应式编程的第一步

6. 程序化创建响应式序列:Flux.generate、Flux.create 与 Flux.push 深度解析

7. 线程调度与 Schedulers:Project Reactor 并发模型的核心引擎

8. 响应式流中的错误处理:Project Reactor 异常治理全体系

9. Sinks API:Project Reactor 中程序化发射数据的现代方案

1. 为什么需要响应式编程?

1.1 传统模型的瓶颈

在经典的 **Servlet 线程模型(Thread-per-Request)**中,每一个 HTTP 请求都会占用一个线程。当请求涉及数据库查询、远程 RPC 调用或文件 I/O 时,线程会阻塞等待,直到 I/O 完成。

文本 复制代码
线程-1: [处理请求] ──[████ 等待DB ██████]── [返回响应]
线程-2: [处理请求] ──[████████ 等待HTTP ████████]── [返回响应]
线程-3: [处理请求] ──[██ 等待Redis ██]── [返回响应]

在 Tomcat 默认 200 线程的配置下,一旦并发量超过线程池容量,新请求只能排队甚至被拒绝。线程成了最昂贵的资源瓶颈。

1.2 响应式编程的核心思想

响应式编程(Reactive Programming)将关注点从"线程等待数据"转变为"数据到达时通知处理":

  • 异步非阻塞:I/O 操作不阻塞线程,通过回调/事件通知结果。
  • 事件驱动:数据以"流"(Stream)的形式流动,开发者声明式地描述如何转换流。
  • 背压控制:下游消费者可以主动控制上游的生产速率,避免被数据洪流淹没。

1.3 响应式宣言(Reactive Manifesto)四大原则

原则 含义
响应性(Responsive) 系统及时响应,提供一致的服务质量
弹性(Resilient) 面对故障仍能保持响应,通过复制、隔离实现
伸缩性(Elastic) 在不同负载下保持响应性,动态伸缩资源
消息驱动(Message Driven) 组件间通过异步消息通信,实现松耦合

2. Project Reactor 全景概览

Project Reactor 是由 Pivotal(现 VMware Tanzu / Broadcom)团队主导开发的 Java 响应式编程库,是 Spring WebFlux 的底层核心依赖,也是 Java 生态中最成熟、最广泛使用的 Reactive Streams 实现。

文本 复制代码
┌─────────────────────────────────────────────────────┐
│                  Spring WebFlux                     │
│              (响应式 Web 框架)                        │
├─────────────────────────────────────────────────────┤
│               Project Reactor                       │
│         (响应式编程核心库)                             │
│   ┌──────────┐  ┌──────────┐  ┌──────────────┐      │
│   │  Mono    │  │  Flux    │  │  Scheduler   │      │
│   └──────────┘  └──────────┘  └──────────────┘      │
├─────────────────────────────────────────────────────┤
│            Reactive Streams (规范)                   │
│   Publisher / Subscriber / Subscription / Processor │
├─────────────────────────────────────────────────────┤
│              Java 9+ Flow API                       │
└─────────────────────────────────────────────────────┘

核心设计哲学:

  • 函数式 + 声明式:用操作符链描述数据转换逻辑,而非命令式循环。
  • 惰性求值(Lazy):操作符链在 subscribe() 之前不会执行任何逻辑。
  • 可组合(Composable):Mono/Flux 可以像乐高积木一样自由组合。
  • 零拷贝、低分配:内部大量使用对象池和无锁数据结构,追求极致性能。

Maven 依赖:

xml 复制代码
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
    <version>3.7.3</version> <!-- 请使用最新版本 -->
</dependency>

<!-- 测试支持 -->
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-test</artifactId>
    <version>3.7.3</version>
    <scope>test</scope>
</dependency>

3. Reactive Streams 规范:四大基石

Project Reactor 严格实现了 Reactive Streams 规范(已纳入 Java 9 的 java.util.concurrent.Flow)。规范定义了四个核心接口:

java 复制代码
// 1. 发布者 ------ 数据源
public interface Publisher<T> {
    void subscribe(Subscriber<? super T> subscriber);
}

// 2. 订阅者 ------ 数据消费者
public interface Subscriber<T> {
    void onSubscribe(Subscription s);  // 订阅建立时回调
    void onNext(T t);                  // 收到一个数据元素
    void onError(Throwable t);         // 发生不可恢复的错误
    void onComplete();                 // 数据流正常结束
}

// 3. 订阅关系 ------ 背压的通道
public interface Subscription {
    void request(long n);   // 下游向上游请求 n 个元素
    void cancel();          // 取消订阅
}

// 4. 处理器 ------ 既是订阅者又是发布者
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {}

关键规则:

  • onNext 可以被调用 0~N 次。
  • onError 和 onComplete 是终端信号,二者互斥,最多出现一次。
  • request(n) 中 n 必须 > 0,Long.MAX_VALUE 表示无界请求(即取消背压)。

4. 核心类型:Mono 与 Flux

Reactor 的一切围绕两个 Publisher 实现展开:

4.1 Mono:0 或 1 个元素

java 复制代码
// 创建一个 Mono
Mono<String> mono = Mono.just("Hello Reactor");

// 从 CompletableFuture 桥接
Mono<User> userMono = Mono.fromFuture(userService.findByIdAsync(1L));

// 空 Mono
Mono<Void> empty = Mono.empty();

// 延迟创建(每次订阅都重新计算)
Mono<Long> delayed = Mono.delay(Duration.ofSeconds(3));

// 实际使用:查询单个用户
Mono<User> findUser(Long id) {
    return userRepository.findById(id)          // 返回 Mono<User>
            .switchIfEmpty(Mono.error(new UserNotFoundException(id)))
            .map(user -> {
                user.setLastAccessTime(LocalDateTime.now());
                return user;
            })
            .cache(Duration.ofMinutes(5));      // 缓存 5 分钟
}

4.2 Flux:0 到 N 个元素

java 复制代码
// 从集合创建
Flux<Integer> numbers = Flux.fromIterable(List.of(1, 2, 3, 4, 5));

// 区间生成
Flux<Long> range = Flux.range(1, 100).map(Integer::longValue);

// 定时发射(每 500ms 一个元素)
Flux<Long> interval = Flux.interval(Duration.ofMillis(500));

// 编程式创建(适合桥接回调 API)
Flux<String> bridge = Flux.create(sink -> {
    messageListener.onMessage(msg -> sink.next(msg.getBody()));
    messageListener.onError(sink::error);
    messageListener.onClose(sink::complete);
    sink.onDispose(() -> messageListener.close());
});

4.3 Mono ↔ Flux 互转

java 复制代码
Flux<Integer> flux = Flux.just(1, 2, 3);
Mono<Integer> first = flux.next();              // 取第一个 → Mono
Flux<Integer> back  = first.flux();            // Mono → Flux

// collectList: Flux<T> → Mono<List<T>>
Mono<List<Integer>> listMono = flux.collectList();

// flatMapMany: Mono → Flux
Flux<Order> orders = userMono.flatMapMany(user -> orderRepo.findByUserId(user.getId()));

4.4 惰性求值:subscribe 才触发

java 复制代码
// ⚠️ 这行代码什么都不会发生!
Flux<String> lazy = Flux.just("A", "B", "C")
        .map(s -> {
            System.out.println("Processing: " + s);
            return s.toLowerCase();
        });

// 必须 subscribe 才会真正执行
lazy.subscribe(System.out::println);
// 输出:
// Processing: A
// a
// Processing: B
// b
// Processing: C
// c

核心要义:Reactor 的操作符链是声明式蓝图,只有在 subscribe()(或 WebFlux 框架自动订阅)时才会真正执行。

5. 操作符体系:数据流的"瑞士军刀"

Reactor 提供了 250+ 个操作符,以下按功能分类梳理最常用的一组。

5.1 转换(Transform)

java 复制代码
Flux.just("apple", "banana", "cherry")
    .map(String::toUpperCase)                    // 1:1 同步转换
    .flatMap(s -> queryDb(s))                    // 1:N 异步转换(不保序)
    .concatMap(s -> queryDb(s))                  // 1:N 异步转换(保序)
    .flatMapSequential(s -> queryDb(s), 4)       // 并发度=4,保序
    .subscribe(System.out::println); 

5.2 过滤(Filter)

java 复制代码
Flux.range(1, 100)
    .filter(n -> n % 2 == 0)          // 偶数
    .distinct()                        // 去重
    .skip(5)                           // 跳过前 5 个
    .take(10)                          // 只取 10 个
    .subscribe(System.out::println);

5.3 组合(Combine)

java 复制代码
// merge: 交错合并(谁先到谁先出)
Flux<String> merged = Flux.merge(fluxA, fluxB);

// concat: 顺序拼接(A 完成后才开始 B)
Flux<String> concatenated = Flux.concat(fluxA, fluxB);

// zip: 一一配对
Flux<Tuple2<User, Order>> zipped = Flux.zip(userFlux, orderFlux);

// combineLatest: 取各流最新值组合
Flux<String> combined = Flux.combineLatest(
    priceFlux, quantityFlux,
    (price, qty) -> "Total: " + price * qty
);

5.4 聚合(Aggregate)

java 复制代码
Flux.range(1, 10)
    .reduce(0, Integer::sum)          // Mono<Integer> = 55
    .subscribe(System.out::println);

Flux.range(1, 10)
    .scan(0, Integer::sum)            // Flux<Integer>: 1,3,6,10,...,55
    .subscribe(System.out::println);

Flux.just("a", "b", "a", "c", "b")
    .collect(Collectors.groupingBy(s -> s, Collectors.counting()))
    .subscribe(System.out::println);  // {a=2, b=2, c=1}

5.5 窗口与缓冲(Window / Buffer)

java 复制代码
// buffer: 每 3 个元素打包成 List
Flux.range(1, 10)
    .buffer(3)
    .subscribe(list -> System.out.println("Batch: " + list));
// Batch: [1, 2, 3]
// Batch: [4, 5, 6]
// Batch: [7, 8, 9]
// Batch: [10]

// window: 每 3 个元素切分为一个子 Flux
Flux.range(1, 10)
    .window(3)
    .flatMap(window -> window.reduce(0, Integer::sum))
    .subscribe(System.out::println);
// 6, 15, 24, 10

6. 调度器(Scheduler):线程模型的精髓

Reactor 本身不绑定线程模型,通过 Scheduler 抽象来切换执行上下文。

6.1 内置调度器

调度器 工厂方法 适用场景
immediate Schedulers.immediate() 当前线程直接执行(默认)
single Schedulers.single() 单线程,适合低延迟定时任务
boundedElastic Schedulers.boundedElastic() 有界弹性线程池,包装阻塞调用
parallel Schedulers.parallel() 固定大小(CPU 核心数),CPU 密集计算

6.2 subscribeOn vs publishOn

java 复制代码
Flux.range(1, 5)
    .map(i -> {
        System.out.println("[map] " + Thread.currentThread().getName());
        return i * 2;
    })
    .subscribeOn(Schedulers.boundedElastic())  // 影响上游:源头在哪个线程
    .publishOn(Schedulers.parallel())          // 影响下游:切换后续操作的线程
    .map(i -> {
        System.out.println("[after publishOn] " + Thread.currentThread().getName());
        return i + 1;
    })
    .subscribe();

核心区别:

  • subscribeOn:决定订阅(数据源) 在哪个线程执行,整条链中只有第一个生效。
  • publishOn:从该操作符开始,下游切换到指定调度器,可以出现多次。

6.3 包装阻塞调用的正确姿势

java 复制代码
// ❌ 错误:在响应式链中直接调用阻塞方法
Mono<User> bad = Mono.just(blockingService.getUser());

// ✅ 正确:用 boundedElastic 包装
Mono<User> good = Mono.fromCallable(() -> blockingService.getUser())
                      .subscribeOn(Schedulers.boundedElastic());

7. 背压(Backpressure):响应式的灵魂

背压是 Reactive Streams 区别于普通异步回调的核心特性------下游可以告诉上游"我只要 N 个"。

7.1 背压策略

java 复制代码
Flux.range(1, 1_000_000)
    .onBackpressureBuffer(1024)       // 缓冲:最多缓存 1024 个
    // .onBackpressureDrop()          // 丢弃:来不及处理的直接丢弃
    // .onBackpressureLatest()        // 只保留最新
    // .onBackpressureError()         // 溢出直接报错
    .subscribe(new BaseSubscriber<>() {
        @Override
        protected void hookOnSubscribe(Subscription subscription) {
            request(10);  // 初始只请求 10 个
        }

        @Override
        protected void hookOnNext(Integer value) {
            // 慢速消费
            process(value);
            request(1);   // 每处理完一个,再要一个
        }
    });

7.2 实际场景:数据库批量写入

java 复制代码
Flux<Order> orderStream = orderSource.listen();

orderStream
    .bufferTimeout(500, Duration.ofSeconds(2))  // 每 500 条或每 2 秒
    .onBackpressureBuffer(64)                    // 最多缓存 64 个批次
    .concatMap(batch ->                          // 顺序写入,避免并发冲突
        dbClient.batchInsert(batch)
                .then(Mono.delay(Duration.ofMillis(100)))  // 限流
    )
    .subscribe();

8. 错误处理:优雅地应对失败

8.1 错误处理操作符

java 复制代码
webClient.get()
    .uri("/api/users/{id}", userId)
    .retrieve()
    .bodyToMono(User.class)
    // 1. 捕获特定异常,返回兜底值
    .onErrorResume(UserNotFoundException.class, e -> Mono.just(User.DEFAULT))
    // 2. 重试(指数退避)
    .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
            .maxBackoff(Duration.ofSeconds(10))
            .filter(e -> e instanceof TransientException))
    // 3. 超时控制
    .timeout(Duration.ofSeconds(5))
    // 4. 全局兜底
    .onErrorReturn(User.FALLBACK)
    .subscribe();

8.2 错误传播规则

文本 复制代码
onNext → onNext → onError → (终止,后续元素不再发出)
onNext → onNext → onComplete → (正常结束)
  • 错误信号沿操作符链向下传播,直到被某个 onError* 操作符捕获。
  • 未被捕获的错误会传递给最终的 Subscriber.onError()。
  • 错误是终端事件:一旦发出,流即结束。

8.3 在 flatMap 中隔离错误

java 复制代码
Flux.just(1, 2, 3, 4, 5)
    .flatMap(i -> riskyCall(i)
            .onErrorResume(e -> {
                log.warn("Item {} failed: {}", i, e.getMessage());
                return Mono.empty();  // 跳过失败项,不影响其他
            })
    )
    .subscribe();

9. 与 Spring WebFlux 的深度集成

9.1 响应式 Controller

java 复制代码
@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserRepository userRepository;

    @GetMapping("/{id}")
    public Mono<ResponseEntity<User>> getUser(@PathVariable Long id) {
        return userRepository.findById(id)
                .map(ResponseEntity::ok)
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    @GetMapping
    public Flux<User> listUsers(@RequestParam(defaultValue = "0") int page,
                                @RequestParam(defaultValue = "20") int size) {
        return userRepository.findAll()
                .skip((long) page * size)
                .take(size);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<User> createUser(@Valid @RequestBody Mono<User> userMono) {
        return userMono.flatMap(userRepository::save);
    }

    // SSE(Server-Sent Events)流式推送
    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<User>> userStream() {
        return userRepository.listenToChanges()
                .map(user -> ServerSentEvent.<User>builder()
                        .id(user.getId().toString())
                        .event("user-update")
                        .data(user)
                        .build());
    }
}

9.2 WebClient:响应式 HTTP 客户端

java 复制代码
@Service
public class OrderService {

    private final WebClient webClient;

    public Mono<OrderDetail> getOrderDetail(Long orderId) {
        return webClient.get()
                .uri("/api/orders/{id}", orderId)
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .onStatus(HttpStatusCode::is4xxClientError, resp ->
                    Mono.error(new BusinessException("Order not found: " + orderId)))
                .bodyToMono(Order.class)
                .zipWith(
                    webClient.get()
                            .uri("/api/users/{id}", 0) // 占位
                            .retrieve()
                            .bodyToMono(User.class)
                )
                .map(tuple -> new OrderDetail(tuple.getT1(), tuple.getT2()))
                .timeout(Duration.ofSeconds(3));
    }
}

9.3 响应式数据库访问(R2DBC)

java 复制代码
@Repository
public class ReactiveUserRepository {

    private final R2dbcEntityTemplate template;

    public Flux<User> findActiveUsers(int limit) {
        Query query = Query.query(Criteria.where("status").is("ACTIVE"))
                             .sort(Sort.by(Sort.Direction.DESC, "createdAt"))
                             .limit(limit);
        return template.select(query, User.class);
    }
}

10. 工程最佳实践与常见陷阱

10.1 ✅ 最佳实践

① 保持链条完整,不要提前 subscribe

java 复制代码
// ❌ 错误:在 Service 层 subscribe 了
public void process() {
    flux.subscribe();  // 断开了与上层的响应式链路
}

// ✅ 正确:返回 Mono/Flux,让框架层 subscribe
public Mono<Result> process() {
    return flux.collectList().map(Result::new);
}

② 使用 defer 延迟副作用

java 复制代码
// ❌ 每次构建 Bean 时就执行了
Mono<Long> bad = Mono.just(System.currentTimeMillis());

// ✅ 每次 subscribe 时才执行
Mono<Long> good = Mono.defer(() -> Mono.just(System.currentTimeMillis()));

③ 善用 Context 传递请求级数据(替代 ThreadLocal)

java 复制代码
Mono<User> getUser() {
    return Mono.deferContextual(ctx -> {
        String traceId = ctx.get("traceId");
        return userRepository.findByTrace(traceId);
    });
}

// 写入 Context
getUser()
    .contextWrite(Context.of("traceId", UUID.randomUUID().toString()))
    .subscribe();

④ 使用 StepVerifier 编写单元测试

java 复制代码
@Test
void testFluxTransform() {
    StepVerifier.create(
            Flux.just(1, 2, 3).map(i -> i * 10)
    )
    .expectNext(10)
    .expectNext(20)
    .expectNext(30)
    .verifyComplete();
}

@Test
void testErrorHandling() {
    StepVerifier.create(
            Mono.error(new RuntimeException("boom"))
                 .onErrorReturn(-1)
    )
    .expectNext(-1)
    .verifyComplete();
}

10.2 ❌ 常见陷阱

陷阱 说明 修复
在响应式链中调用阻塞代码 阻塞 EventLoop 线程,导致吞吐骤降 用 subscribeOn(boundedElastic()) 包装
忘记 subscribe 链不执行,数据"消失" 确保最终有 subscribe 或返回给框架
在 map 中抛出 checked 异常 编译不过或异常被吞 用 Mono.fromCallable() + onErrorResume
过度使用 block() 退化为同步模型,丧失响应式优势 保持全链路异步
在 flatMap 中共享可变状态 并发竞争导致数据错乱 使用不可变对象或 concatMap
忽略 Disposable 无法取消订阅,可能内存泄漏 保存 Disposable,在适当时机 dispose()

11. 性能调优与监控

11.1 关键调优参数

java 复制代码
// 调整 boundedElastic 线程池
Schedulers.newBoundedElastic(
    200,          // 最大线程数
    100_000,      // 队列容量
    "blocking-io",// 线程名前缀
    60            // 空闲线程存活秒数
);

// 调整 parallel 调度器(默认 = CPU 核心数)
Schedulers.newParallel("compute", Runtime.getRuntime().availableProcessors());

11.2 使用 Hooks 全局监控

java 复制代码
// 应用启动时注册
Hooks.onOperatorDebug();  // ⚠️ 仅开发环境!生产环境性能开销极大

// 生产环境推荐:Reactor Debug Agent(Java Agent 方式,零运行时开销)
// -javaagent:reactor-tools-agent.jar

11.3 Micrometer 指标集成

java 复制代码
// Spring Boot Actuator + Micrometer 自动采集 Reactor 指标
// application.yml
management:
  metrics:
    tags:
      application: my-service
  endpoints:
    web:
      exposure:
        include: metrics, prometheus

关注的关键指标:

  • reactor.subscriber.count:活跃订阅数
  • reactor.requested:背压请求量
  • scheduler.threads.active:调度器活跃线程数
  • scheduler.queue.size:任务队列积压

11.4 避免不必要的对象分配

java 复制代码
// ❌ 每次 map 都创建新 String
flux.map(i -> "item-" + i);

// ✅ 如果下游只需要数值,避免无谓转换
flux.map(i -> i * 2);

// ✅ 使用 Flux.using 管理资源生命周期
Flux.using(
    () -> openConnection(),              // 资源创建
    conn -> Flux.fromIterable(conn.query()), // 使用资源
    conn -> conn.close()                 // 资源释放(finally)
);

12. 总结与展望

何时选择 Reactor?

场景 推荐度 说明
API 网关 / BFF 层 ⭐⭐⭐⭐⭐ 大量 I/O 转发,响应式收益最大
高并发 WebSocket / SSE ⭐⭐⭐⭐⭐ 长连接场景天然适配
流式数据处理(ETL) ⭐⭐⭐⭐ 背压机制防止 OOM
微服务间 HTTP/RPC 调用 ⭐⭐⭐⭐ WebClient + 全链路非阻塞
CPU 密集型计算 ⭐⭐ 收益有限,考虑 Fork/Join 或并行流
简单 CRUD 后台管理 ⭐⭐ 复杂度收益比不高,传统 MVC 足够

核心要点回顾

文本 复制代码
1. Mono = 0|1 个元素,Flux = 0..N 个元素
2. 一切皆惰性 ------ subscribe() 才触发执行
3. subscribeOn 影响源头,publishOn 影响下游
4. 阻塞代码必须隔离到 boundedElastic
5. 背压是权利,也是责任 ------ 合理设置 buffer/drop 策略
6. 错误是终端信号 ------ 用 onErrorResume/retryWhen 优雅处理
7. 不要 block(),不要提前 subscribe,不要在 map 里做 I/O
8. 测试用 StepVerifier,调试用 reactor-tools agent

展望

随着 Virtual Threads(虚拟线程,JDK 21+) 的成熟,Java 生态正在迎来"同步写法 + 非阻塞性能"的新范式。Spring Framework 7 已提供对虚拟线程的一级支持。但这并不意味着 Reactor 会退出舞台------在流式处理、背压控制、事件驱动架构、SSE/WebSocket 长连接 等场景中,响应式编程模型依然具有不可替代的优势。未来的最佳实践,很可能是 "虚拟线程处理请求级并发 + Reactor 处理流级并发" 的混合模型。

相关推荐
蓝桉柒712 分钟前
java字面量,变量
java
典典分享指南17 分钟前
一品牌多产品线的 GEO 内容矩阵:从策略到落地
java·c#·bash·symfony
用户094248568031 小时前
第3章:OpenJDK从源码到字节码——Class 文件与 javac 管线
java·jvm
azhou的代码园1 小时前
基于RFM模型的中小型企业客户管理系统
java·人工智能·spring boot·毕业设计
君顾11 小时前
酒吧点餐小程序系统开发实战:从架构设计到上线全流程指南
java·开发语言·酒吧
子非鱼a2 小时前
【WEB】[SWPU2019]Web1
java·服务器·前端
花生了什么事o2 小时前
自定义Spring Boot Starter全流程
java·spring boot·后端
torpidcat3 小时前
ruoyi-vue-pro 若依芋道 java springboot +mybatis 生日查询相关
java·vue.js·spring boot
AC赳赳老秦3 小时前
文旅市场公开数据分析:基于 OpenClaw 采集景区客流与门票公示数据,生成区域文旅热度监测报告
java·c语言·python·php·symfony·deepseek·openclaw
我命由我123454 小时前
Windows 操作系统 - 启用 D 盘虚拟内存
java·运维·服务器·windows·学习·java-ee·运维开发