Mutiny、Project-Reactor好像都挺冷门的,人气不高,相关资料也不多。不过其实整个Java Reactive都不是很热门,国内用的人不是很多。我个人觉得这些Reactive库都非常优秀,值得学习。
这篇博客主要是Mutiny的Hello World,重点是初步认识Mutiny和如何使用Mutiny API。文章的例子都基于官网,在此基础上写了一些自己的理解和总结。
对于Vertx core我还是比较熟悉的,但是对Mutiny和Reactor这些基于Reactive-Stream的还不是很了解。Quarkus底层基于Vertx,但不推荐我们直接使用Vertx,而是推荐使用Mutiny的API进行响应式编程。相比较而言Mutiny和Reactor提供了更强大、更炫酷的API,Vertx提供的异步API相较而言就显得有些简陋了。不少人被reactive stream的api劝退,但是入门之后还是很有趣的。
官方描述如下:Intuitive Event-Driven Reactive Programming Library for Java.
SmallRye的Mutiny库是一个响应式的、事件驱动的Java库。
Uni 和 Multi是什么
Uni 和 Multi都可以产生元素,Unit产生1个或0个元素,Multi产生多个元素。
Mutiny是惰性的,如果没有被subscribe就不会被执行。初学者经常忘记这一点,然后"为什么我写的代码没有执行?"
而且,reactive stream的代码debug是有一定难度的。
Uni和Mutiny产生event,我们写的代码就是对这些事件进行处理。我们可以用 onItem(), onFailure(), onCompletion() 来处理这些event。event的传播是具有方向性的,可以向上游传播也可以向下游传播,
和其他响应式流框架类似,流上面的元素(事件)会流经处理流水线(processing pipeline),每个operation会处理事件或者转换为别的类型的事件。
onItem().transform(Function
transform()对元素进行转换操作。自然的,该操作是同步的。
下面做一些demo。
private void printWithThread(Object o) {
System.out.println(Thread.currentThread() + ": " + o);
}
private static Executor executor = new ForkJoinPool();
Multi<Integer> multi = Multi.createFrom().items(1, 2, 3, 4, 5)
.emitOn(executor)
.onItem()
.transform(i -> {
System.out.println(Thread.currentThread() + "is working on item: "+i);
return i * 2;
});
multi.subscribe().with(this::printWithThread);
输出:
worker线程对stream的每个元素进行处理后,执行subscribe,继续处理下一个元素。
tricks(),用于构建一条周期性产生元素的stream。
Multi<Long> ticks = Multi.createFrom()
.ticks()
.every(Duration.ofMillis(100));
ticks.subscribe().with(this::printWithThread);
Thread.sleep(500);
invoke()和call()
这也是对event进行订阅的两个方法,其实是对item注册一些回调。两者都是执行回调之后才会将原始的event传递到下游。invoke和call的区别就是注册的回调是同步还是异步的区别,invoke注册的是一个runnable,call注册的回调是一个Supplier
官网描述如下:
Mutiny does not dispatch the original event downstream until the Uni returned by the callback emits an item.
invoke()和call()的区别在于invoke()是同步的,call()是异步的
invoke:
下面看一个例子。
有一个远程的服务:
Uni<String> invokeRemoteGreetingService(String name) {
return Uni.createFrom().item(name)
.emitOn(executor)
.onItem().delayIt().by(Duration.ofSeconds(Math.abs(random.nextInt(10))))
.onItem().transform(s -> "hello " + s);
}
delay主要是为了模拟耗时。
有一个不断产生String的source,而且要调用上面的服务,如何保证顺序性?
先不考虑顺序性,写出来的代码如下:
Multi.createFrom().items(IntStream.range(0, 10).boxed())
.onItem().transform(i -> "delicious-" + i)
.onItem().transform(this::invokeRemoteGreetingService)
.subscribe().with(uni -> uni.subscribe().with(System.out::println));
或者:
Multi.createFrom().items(IntStream.range(0, 10).boxed())
.onItem().transform(i -> "delicious-" + i)
.onItem().transformToUniAndMerge(this::invokeRemoteGreetingService)
.subscribe().with(System.out::println);
transformToUni()将Item转为Uni,stream中的每个元素变异成一种异步状态,然后用merge将这些Uni中的async result组装回来,但不保证顺序。
结果自然是乱序的。(两种写法都是)
如何实现消费的顺序和生产的顺序一致呢?
Multi.createFrom().items(IntStream.range(0, 10).boxed())
.onItem().transform(i -> "delicious-" + i)
.onItem().transformToUniAndConcatenate(this::invokeRemoteGreetingService)
.subscribe().with(System.out::println);
再明确一下,调用onItem()就使得元素从stream的上游流向下游了。
transformToUni()之后使用concatenate()保证元素到达下游的顺序性。准确的说,元素i到达下游之前,元素i+1一直在上游呆着。
异常处理
如果传播的过程中出现了failure,则这个错误一直向下游传播直到一个遇到一个能处理它的地方,直到最后抛出异常。遇到一个failure之后,下游不会收到更多的元素。
比如失败时做出一些动作:
uni.onFailure().invoke()
multi.onFailure().invoke()
注意invoke之后这个failure仍然会向下游传播。
Multi.createFrom().items("a","b","c")
.onItem().failWith((Supplier<Throwable>) Exception::new)
.onFailure().invoke(()->{})
.onFailure().recoverWithItem("feedbackItem")
.subscribe().with(System.out::println);
这里下游的subscribe仅能收到一个feedbackItem。
出现failure之后,可以进行retry,retry还可以配置最多重试次数、延时重试、重试间隔等等。