1. Mono接口概述
Mono是Java响应式编程(Reactive Programming)中一个非常重要的概念,它是Project Reactor库提供的核心类型之一。与传统的同步编程方式不同,Mono代表的是一个异步的、可能包含0个或1个元素的序列。这种设计使得我们能够以声明式的方式处理异步操作,特别适合处理I/O密集型任务。
提示:理解Mono的关键在于把它看作是一个"未来可能产生值的容器",这个容器可能包含值,也可能为空,甚至可能包含错误。
Mono的设计遵循Reactive Streams规范,这意味着它支持背压(backpressure)机制,能够优雅地处理生产者和消费者之间的速率不匹配问题。在实际应用中,Mono常用于以下场景:
- 数据库单条记录查询
- HTTP请求的单个响应
- 异步计算单个结果
- 组合多个异步操作的结果
2. Mono与Flux的区别
2.1 基本概念对比
Mono和Flux都是Project Reactor中的核心类型,但它们适用于不同的场景:
| 特性 | Mono | Flux |
|---|---|---|
| 元素数量 | 0或1个 | 0到N个(可能无限) |
| 类比Java类型 | Optional + CompletableFuture | Stream + List |
| 典型应用 | 查询单条记录、返回单个对象 | 查询列表、处理数据流 |
| 终止信号 | onComplete或onError | onComplete或onError |
2.2 使用场景选择
选择使用Mono还是Flux主要取决于预期的结果数量:
- 当你确定操作最多只会产生一个结果时,使用Mono
- 当操作可能产生多个结果或数据流时,使用Flux
例如,在Spring WebFlux中:
java复制// 返回单个用户 - 使用Mono
@GetMapping("/users/{id}")
public Mono<User> getUserById(@PathVariable String id) {
return userRepository.findById(id);
}
// 返回用户列表 - 使用Flux
@GetMapping("/users")
public Flux<User> getAllUsers() {
return userRepository.findAll();
}
3. Mono的创建方式
3.1 基本创建方法
Mono提供了多种工厂方法来创建实例:
java复制// 1. 创建包含确定值的Mono
Mono<String> monoWithValue = Mono.just("Hello, Mono!");
// 2. 创建空的Mono
Mono<String> emptyMono = Mono.empty();
// 3. 创建包含错误的Mono
Mono<String> errorMono = Mono.error(new RuntimeException("Something went wrong"));
// 4. 从Supplier创建(延迟计算)
Mono<String> lazyMono = Mono.fromSupplier(() -> {
// 这里可以进行复杂计算
return "Computed value";
});
// 5. 从Callable创建
Mono<String> fromCallable = Mono.fromCallable(() -> "From callable");
// 6. 从Future转换
Mono<String> fromFuture = Mono.fromFuture(CompletableFuture.supplyAsync(() -> "Future result"));
3.2 延迟创建与定时操作
Mono支持延迟创建和定时操作,这在需要控制执行时间时非常有用:
java复制// 延迟2秒后创建Mono
Mono<String> delayedMono = Mono.delay(Duration.ofSeconds(2))
.then(Mono.just("Delayed value"));
// 定时创建(在指定时间后发出值)
Mono<String> timedMono = Mono.just("Timed value")
.delaySubscription(Duration.ofSeconds(3));
4. Mono的核心操作符
4.1 转换操作符
java复制Mono<Integer> numberMono = Mono.just("123")
.map(Integer::parseInt) // 将字符串转换为整数
.filter(num -> num > 100) // 过滤小于等于100的值
.defaultIfEmpty(0); // 如果为空则提供默认值
4.2 组合操作符
Mono提供了多种方式来组合多个异步操作:
java复制// 1. flatMap - 将一个Mono转换为另一个Mono
Mono<String> upperCaseMono = Mono.just("hello")
.flatMap(s -> Mono.just(s.toUpperCase()));
// 2. zip - 组合两个Mono的结果
Mono<String> first = Mono.just("Hello");
Mono<String> second = Mono.just("World");
Mono<String> zipped = Mono.zip(first, second)
.map(tuple -> tuple.getT1() + " " + tuple.getT2());
// 3. then - 忽略前一个Mono的结果,执行下一个操作
Mono<Void> completionSignal = Mono.just("do something")
.then(Mono.fromRunnable(() -> System.out.println("Done")));
4.3 错误处理操作符
java复制Mono<Integer> safeMono = Mono.just("abc")
.map(s -> Integer.parseInt(s))
.onErrorResume(e -> {
System.err.println("Error occurred: " + e.getMessage());
return Mono.just(0); // 提供回退值
})
.onErrorReturn(-1); // 或者直接返回默认值
5. Mono的订阅与执行
5.1 订阅方式
Mono是惰性的,只有订阅时才会执行:
java复制// 简单订阅
Mono.just("Hello").subscribe(System.out::println);
// 完整订阅(处理值、错误和完成信号)
Mono.just("Hello").subscribe(
value -> System.out.println("Received: " + value),
error -> System.err.println("Error: " + error),
() -> System.out.println("Completed")
);
// 带取消功能的订阅
Disposable disposable = Mono.delay(Duration.ofSeconds(5))
.subscribe(System.out::println);
// 可以取消订阅
disposable.dispose();
5.2 线程调度
Mono默认在当前线程执行,但可以指定调度器:
java复制// 使用弹性线程池
Mono.just("CPU intensive")
.subscribeOn(Schedulers.parallel())
.subscribe(System.out::println);
// 使用有界弹性线程池
Mono.just("IO intensive")
.subscribeOn(Schedulers.boundedElastic())
.subscribe(System.out::println);
// 发布到不同线程
Mono.just("Process in different thread")
.publishOn(Schedulers.single())
.subscribe(System.out::println);
6. 实际应用场景
6.1 Spring WebFlux中的Mono
java复制@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public Mono<Product> getProduct(@PathVariable String id) {
return productService.findById(id)
.switchIfEmpty(Mono.error(new ProductNotFoundException(id)));
}
@PostMapping
public Mono<Product> createProduct(@RequestBody Product product) {
return productService.save(product);
}
@DeleteMapping("/{id}")
public Mono<Void> deleteProduct(@PathVariable String id) {
return productService.deleteById(id);
}
}
6.2 数据库操作
java复制public interface UserRepository extends ReactiveCrudRepository<User, String> {
Mono<User> findByEmail(String email);
Flux<User> findByAgeGreaterThan(int age);
}
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public Mono<User> registerUser(User user) {
return userRepository.findByEmail(user.getEmail())
.flatMap(existing -> Mono.error(new UserAlreadyExistsException()))
.switchIfEmpty(userRepository.save(user))
.cast(User.class);
}
}
7. 性能优化与最佳实践
7.1 避免阻塞操作
java复制// 错误做法 - 阻塞了调用线程
Mono<String> blockingMono = Mono.fromCallable(() -> {
Thread.sleep(1000); // 阻塞调用
return "Blocking result";
});
// 正确做法 - 使用调度器隔离阻塞操作
Mono<String> nonBlockingMono = Mono.fromCallable(() -> {
Thread.sleep(1000); // 仍然阻塞,但在专用线程中
return "Non-blocking result";
})
.subscribeOn(Schedulers.boundedElastic());
7.2 合理使用缓存
java复制Mono<String> expensiveMono = Mono.fromCallable(() -> {
// 模拟耗时操作
Thread.sleep(1000);
return "Expensive result";
})
.cache(Duration.ofMinutes(5)); // 缓存5分钟
// 第一次调用会执行耗时操作
expensiveMono.subscribe(System.out::println);
// 后续调用在5分钟内会直接返回缓存结果
expensiveMono.subscribe(System.out::println);
7.3 错误处理策略
java复制Mono<String> resilientMono = someUnreliableOperation()
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)) // 指数退避重试
.timeout(Duration.ofSeconds(5)) // 超时控制
.onErrorResume(e -> fallbackOperation()); // 回退方案
8. 常见问题与解决方案
8.1 Mono不执行问题
问题现象:创建了Mono但没有执行任何操作。
原因:忘记调用subscribe()方法。
解决方案:
java复制// 必须调用subscribe才会执行
Mono.just("Hello").subscribe();
8.2 阻塞调用问题
问题现象:在Mono中执行了阻塞操作导致性能下降。
解决方案:
java复制// 将阻塞操作隔离到专用线程池
Mono.fromCallable(() -> blockingIOOperation())
.subscribeOn(Schedulers.boundedElastic())
.subscribe();
8.3 错误未被捕获
问题现象:Mono中的异常导致程序终止。
解决方案:
java复制Mono.error(new RuntimeException("Oops"))
.doOnError(e -> log.error("Error occurred", e))
.onErrorResume(e -> Mono.just("Recovered"))
.subscribe();
8.4 内存泄漏问题
问题现象:Disposable对象未被正确释放。
解决方案:
java复制Disposable disposable = someLongRunningMono.subscribe();
// 在适当的时候释放资源
disposable.dispose();
9. 测试Mono
9.1 使用StepVerifier测试
java复制@Test
void testMono() {
Mono<String> mono = Mono.just("Hello")
.delayElement(Duration.ofMillis(100));
StepVerifier.create(mono)
.expectSubscription()
.expectNoEvent(Duration.ofMillis(99))
.expectNext("Hello")
.verifyComplete();
}
9.2 测试错误场景
java复制@Test
void testErrorMono() {
Mono<String> errorMono = Mono.error(new RuntimeException("Boom"));
StepVerifier.create(errorMono)
.expectError(RuntimeException.class)
.verify();
}
9.3 测试空Mono
java复制@Test
void testEmptyMono() {
Mono<String> emptyMono = Mono.empty();
StepVerifier.create(emptyMono)
.verifyComplete();
}
10. 高级特性与技巧
10.1 使用context传递数据
java复制Mono<String> contextualMono = Mono.deferContextual(ctx -> {
String userId = ctx.get("userId");
return Mono.just("Hello, " + userId);
});
contextualMono
.contextWrite(Context.of("userId", "123"))
.subscribe(System.out::println);
10.2 热发布与冷发布
java复制// 冷发布 - 每个订阅者都会触发新的执行
Mono<String> coldMono = Mono.fromCallable(() -> {
System.out.println("Cold executing");
return "Cold data";
});
// 热发布 - 共享执行结果
Mono<String> hotMono = coldMono.cache();
hotMono.subscribe(); // 触发执行
hotMono.subscribe(); // 直接使用缓存结果
10.3 与Flux的转换
java复制// Mono转Flux
Flux<String> monoToFlux = Mono.just("Single").flux();
// Flux转Mono(取第一个元素)
Mono<String> fluxToMono = Flux.just("First", "Second").next();
// Flux转Mono(收集所有元素)
Mono<List<String>> collectedMono = Flux.just("A", "B", "C").collectList();
在实际项目中使用Mono时,我发现合理使用操作符链可以显著提高代码的可读性和可维护性。特别是在处理复杂的异步操作流程时,将操作分解为多个小的、可组合的Mono操作,然后使用flatMap、zip等操作符将它们组合起来,往往比传统的回调方式更加清晰。