1. 介绍

Spring WebFlux 是一个支持异步非阻塞通信的响应式编程框架。在实际开发中,我们经常需要处理代表单个异步结果的 Mono 对象,特别是在数据转换、外部服务调用或重构数据结构时。本文将深入探讨在 Project Reactor 中将一个 Mono 对象转换为另一个 Mono 对象的多种方法。

2. Mono 对象转换基础

在深入转换方法前,我们先定义示例场景。我们将使用图书借阅系统贯穿全文,包含三个核心类:

User 类表示图书馆用户:

public class User {
    private String userId;
    private String name;
    private String email;
    private boolean active;

    // 标准 setter 和 getter
}

每个用户通过 userId 唯一标识,包含姓名、邮箱等基本信息,active 标志表示用户是否具备借书资格。

Book 类表示图书:

public class Book {
    private String bookId;
    private String title;
    private double price;
    private boolean available;

    // 标准 setter 和 getter
}

图书通过 bookId 标识,包含标题、价格等属性,available 标志表示是否可借阅。

BookBorrowResponse 类封装借阅结果:

public class BookBorrowResponse {
    private String userId;
    private String bookId;
    private String status;

    // 标准 setter 和 getter
}

该类关联用户和图书,并提供借阅状态(接受/拒绝)。

3. 使用 map() 进行同步转换

map() 操作符对 Mono 中的数据应用同步函数,适用于轻量级操作如格式化、过滤或简单计算。例如从用户对象中提取邮箱:

@Test
void givenUserId_whenTransformWithMap_thenGetEmail() {
    String userId = "U001";
    Mono<User> userMono = Mono.just(new User(userId, "John", "[email protected]"));
    Mockito.when(userService.getUser(userId))
      .thenReturn(userMono);

    Mono<String> userEmail = userService.getUser(userId)
      .map(User::getEmail);

    StepVerifier.create(userEmail)
      .expectNext("[email protected]")
      .verifyComplete();
}

✅ 适用场景:

  • 数据格式转换
  • 字段提取
  • 简单计算

❌ 不适用场景:

  • 涉及异步操作
  • 需要错误处理

4. 使用 flatMap() 进行异步转换

flatMap()Mono 发射的项转换为另一个 Publisher特别适合需要新异步流程的场景(如调用外部 API 或查询数据库)。当转换结果是 Mono 时,flatMap() 会将其扁平化为单一序列。

以图书借阅为例,系统需验证用户状态和图书可用性:

public Mono<BookBorrowResponse> borrowBook(String userId, String bookId) {
    return userService.getUser(userId)
      .flatMap(user -> {
          if (!user.isActive()) {
              return Mono.error(new RuntimeException("用户非活跃状态"));
          }
          return bookService.getBook(bookId);
      })
      .flatMap(book -> {
          if (!book.isAvailable()) {
              return Mono.error(new RuntimeException("图书不可借阅"));
          }
          return Mono.just(new BookBorrowResponse(userId, bookId, "Accepted"));
      });
}

⚠️ 关键优势:

  • 链式异步操作避免嵌套
  • 动态决策(如图书检查依赖用户状态)
  • 保持响应式流特性

5. 使用 transform() 封装可复用逻辑

transform() 方法能封装可复用的转换逻辑,避免在应用各处重复代码,提升可维护性和关注点分离。

示例:计算图书最终价格(含税和折扣):

public Mono<Book> applyDiscount(Mono<Book> bookMono) {
    return bookMono.map(book -> {
        book.setPrice(book.getPrice() - book.getPrice() * 0.2);
        return book;
    });
}

public Mono<Book> applyTax(Mono<Book> bookMono) {
    return bookMono.map(book -> {
        book.setPrice(book.getPrice() + book.getPrice() * 0.1);
        return book;
    });
}

public Mono<Book> getFinalPricedBook(String bookId) {
    return bookService.getBook(bookId)
      .transform(this::applyTax)
      .transform(this::applyDiscount);
}

✅ 使用场景:

  • 跨组件复用转换逻辑
  • 复杂计算链封装
  • 业务规则集中管理

6. 使用 zip() 合并多数据源

zip() 合并多个 Mono 对象生成单一结果,等待所有 Mono 发射数据后才应用组合函数(非并发合并)。

重构图书借阅示例:合并用户和图书信息:

public Mono<BookBorrowResponse> borrowBookZip(String userId, String bookId) {
    Mono userMono = userService.getUser(userId)
      .switchIfEmpty(Mono.error(new RuntimeException("用户不存在")));
    Mono bookMono = bookService.getBook(bookId)
      .switchIfEmpty(Mono.error(new RuntimeException("图书不存在")));
    return Mono.zip(userMono, bookMono,
      (user, book) -> new BookBorrowResponse(userId, bookId, "Accepted"));
}

⚠️ 执行特点:

  • 阻塞等待所有数据源
  • 任一失败则整体终止
  • 适合依赖所有数据源的聚合操作

7. 条件转换

结合 filter()switchIfEmpty() 可实现条件转换:

  • 满足条件时返回原始 Mono
  • 否则切换到备用 Mono

示例:仅对活跃用户应用折扣:

public Mono<Book> conditionalDiscount(String userId, String bookId) {
    return userService.getUser(userId)
      .filter(User::isActive)
      .flatMap(user -> bookService.getBook(bookId).transform(this::applyDiscount))
      .switchIfEmpty(bookService.getBook(bookId))
      .switchIfEmpty(Mono.error(new RuntimeException("图书不存在")));
}

✅ 执行流程:

  1. 获取用户信息
  2. 检查用户是否活跃 → 应用折扣
  3. 非活跃用户 → 获取原价图书
  4. 图书不存在 → 抛出错误

8. 转换过程中的错误处理

健壮的错误处理能确保系统弹性,提供优雅降级机制。onErrorResume() 是核心工具,通过备用 Mono 恢复错误

示例:借阅失败时返回拒绝状态:

public Mono<BookBorrowResponse> handleErrorBookBorrow(String userId, String bookId) {
    return borrowBook(userId, bookId)
      .onErrorResume(ex -> Mono.just(new BookBorrowResponse(userId, bookId, "Rejected")));
}

✅ 错误处理策略:

  • 提供默认响应
  • 切换备用数据源
  • 记录错误日志
  • 保证用户体验一致性

9. Mono 转换最佳实践

构建高效响应式管道需遵循以下原则:

操作符选择

  • map():同步轻量转换(数据格式化/字段提取)
  • flatMap():异步流程(API 调用/数据库查询)
  • transform():可复用逻辑封装

代码结构

  • ✅ 链式调用优于嵌套
  • ✅ 使用 transform() 分离关注点
  • ❌ 避免深度嵌套回调

错误处理

  • ✅ 关键操作添加 onErrorResume()
  • ✅ 提供有意义的备用响应
  • ❌ 避免静默吞咽错误

数据验证

  • ✅ 输入输出双重校验
  • ✅ 使用 filter() 提前过滤无效数据
  • ❌ 避免无效数据传播到下游

10. 总结

本文系统介绍了 Spring WebFlux 中 Mono 对象转换的核心方法:

  • map() 处理同步转换
  • flatMap() 链式异步操作
  • transform() 封装可复用逻辑
  • zip() 合并多数据源
  • 条件转换与错误处理策略

掌握这些技术并遵循最佳实践,能构建出灵活、可维护的响应式管道。关键在于根据场景选择合适操作符,保持代码简洁性和错误处理完备性。


原始标题:Convert Mono Object to Another Mono Object in Spring WebFlux | Baeldung