使用projectreactor中的Flux时,生成和处理异常的正确方法是什么

时间:2019-03-06 13:50:55

标签: java reactive-programming project-reactor

我正在使用io.projectreactor 3(reactor-core 3.2.6.RELEASE),并且注意到有关错误处理的一些差异。不幸的是,官方文档没有提供足够的详细信息来解决我的问题。

我有以下4个摘要。在某些情况下,异常将被忽略,而在其他情况下,它将进一步抛出。实际产生和使用异常的方式是什么?

代码段1

在这种情况下,异常将被忽略,并且main()将在不接收异常的情况下完成。

import reactor.core.publisher.Flux;

class Scratch {
    public static void main(String[] args) throws Throwable {
        Flux.push(sink -> {
            sink.next(1);
            sink.next(2);
        }).doOnNext(e -> {
            throw new RuntimeException("HELLO WORLD");
        }).subscribe(System.out::println, e -> {
            throw new RuntimeException(e);
        });
        System.out.println("DONE");
    }
}

输出:

DONE

代码段2

与上面的示例相似,除了我们不使用Flux.push而是使用Flux.just。 Main()将收到异常。

import reactor.core.publisher.Flux;

class Scratch {
    public static void main(String[] args) throws Throwable {
        Flux.just(
                1
        ).doOnNext(e -> {
            throw new RuntimeException("HELLO WORLD");
        }).subscribe(System.out::println, e -> {
            throw new RuntimeException(e);
        });
        System.out.println("DONE");
    }
}

输出:

Exception in thread "main" java.lang.RuntimeException: java.lang.RuntimeException: HELLO WORLD
    at Scratch.lambda$main$1(scratch_15.java:10)
...

代码段3

我们通过调用sink.error发出异常信号。 Main()将不会收到异常。

import reactor.core.publisher.Flux;

class Scratch {
    public static void main(String[] args) throws Throwable {
        Flux.push(sink -> {
            sink.next(1);
            sink.next(2);
            sink.error(new RuntimeException("HELLO WORLD"));
        }).subscribe(System.out::println, e -> {
            throw new RuntimeException(e);
        });
        System.out.println("DONE");
    }
}

输出:

1
2
DONE

代码段4

我们直接抛出异常。 Main()将收到异常。

import reactor.core.publisher.Flux;

class Scratch {
    public static void main(String[] args) throws Throwable {
        Flux.push(sink -> {
            sink.next(1);
            sink.next(2);
            throw new RuntimeException("HELLO WORLD");
        }).subscribe(System.out::println, e -> {
            throw new RuntimeException(e);
        });
        System.out.println("DONE");
    }
}

输出

1
2
Exception in thread "main" java.lang.RuntimeException: java.lang.RuntimeException: HELLO WORLD
    at Scratch.lambda$main$1(scratch_15.java:10)
...

使用反应堆内核时,处理异常的正确方法是什么?唯一可靠的方法似乎根本不使用错误回调,而是用try / catch包围flux.subscribe。但是在那种情况下,我总是收到UnsupportedOperationException而不是原始异常,然后我需要使用Exceptions.isErrorCallbackNotImplemented来检查它是否来自响应式,提取嵌套的异常然后将其抛出。

这当然可以完成,但是需要在我们使用Flux的每个地方进行一致的订阅。对我来说那不太好。我在这里想念什么?

1 个答案:

答案 0 :(得分:2)

在所有示例中,问题都是从.subscribe(...)错误处理lambda引发的。

如果要在主块中引发异常,请使用block()变体。

如果要测试错误是否在整个管道中传播,请使用StepVerifier.create(pipeline).expectError(...).verify()

.subscribe通常是为了获得“终端”状态的通知,而不是旨在恢复或重新抛出错误(为此使用上游的onError*运算符)。

基于just的示例似乎正确传播了该异常,因为它们在订阅时不执行用户提供的代码,因此在subscribe(Consumer<Throwable>)期间没有进行try / catch。

push,例如generate / create / defercompose,在以下位置执行一些用户定义的逻辑(Consumer<FluxSink>)订阅。他们防范整个Consumer抛出异常,并尝试传播(作为onError信号),而不是直接抛出该异常。

但是,如果Consumer的失败是在执行sink的一种方法时引起的,那么如果subscriber重新抛出,则可能会出现问题:我们在以下位置输入一个递归调用接收器调用接收器。当检测到接收器的递回排水时,我们通过退出来防止这种无限情况。

这就是为什么在pushsink.next(示例1和3)中触发错误的基于sink.error的示例在主程序中无法产生异常的原因:

  1. Consumer已应用
  2. sink.next被调用,并且下一个运算符创建异常1,或者sink.error被调用
  3. 例外1到达subscribe,并重新抛出为例外2
  4. 这会使Consumer.apply短路,异常2传递给sink.error
  5. 接收器已经被调用了,所以我们尽量避免无限递归
  6. 从未见过例外2

另一方面,在示例4中,我们不再处于调用接收器方法的中间,并且原始异常不会首先到达订阅者:

  1. Consumer已应用
  2. 它直接引发异常1
  3. 这会使Consumer.apply短路,并且异常1传递给sink.error
  4. 传播到订阅的
  5. 将其作为异常2重新抛出
  6. 在主要方法中看到
  7. 例外2