如何在包含反应管道创建者堆栈跟踪的反应管道执行中抛出异常?

How can I throw an exception within an execution of a reactive pipeline that contains the stack trace of the creator of the reactive pipeline?

如果我 运行 以下反应管道:

        Mono.empty()
                .switchIfEmpty(Mono.defer(() -> Mono.error(new RuntimeException())))
                .block();

我得到了反应步骤的堆栈跟踪(switchIfEmpty);但我对管道创建者的堆栈跟踪感兴趣。 reactive stack trace 部分没用,我实际上对调试触发管道创建的请求感兴趣(这是顺序执行的).

即使管道创建者的堆栈跟踪有时会显示在反应性堆栈元素下方,也不能保证,因为我见过太多意外分叉的管道,最坏的情况是丢失了管道创建者堆栈跟踪.最好的情况是感兴趣的堆栈跟踪被隐藏在堆栈元素下,几乎不可能进行调试。

由于我正在创建的管道将用于通过网络执行往返请求,因此我可以接受要求支付最高性能价格的解决方案,该价格相当于执行网络成本的 1 个数量级请求往返。

解决方法很简单:

  1. 在创建管道的同时创建异常;但不要扔掉它。将使用感兴趣的堆栈跟踪构造该异常。
  2. 使用构建的异常构建管道,以便在需要时使用。

以下应该有效:

        Mono.empty()
                .switchIfEmpty(Mono.error(new RuntimeException()))
                .block();

这条管道在性能方面会更昂贵;但会尊重 OP 的要求,即不增加超过网络请求往返成本的一小部分。

以下测试用例验证并说明(检查打印输出的顺序)此技术获得所需堆栈跟踪的有效性:

import org.junit.jupiter.api.Test;

import java.io.PrintWriter;
import java.io.StringWriter;

import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

public class MonoSwitchIfEmptyTest {

    @Test
    void whenRequestThrowsPrecompiledException_thenExpectDesiredStackTrace() {
        trySwitchIfEmptyWithDesiredStackTrace(createAppropriateSwitchIfEmptyMono(true), true);
    }

    @Test
    void whenRequestThrowsLiveException_thenExpectUndesiredStackTrace() {
        trySwitchIfEmptyWithDesiredStackTrace(createAppropriateSwitchIfEmptyMono(false), false);
    }

    // This is done here to force the creation of an extra stack element into the stack trace.
    // That stack element is the one that later will be checked to determine if the test passed or not.
    private Mono<Void> createAppropriateSwitchIfEmptyMono(boolean desiredStackTrace) {
        return desiredStackTrace ?
                Mono.error(new LoggedRuntimeException()) :
                Mono.defer(() -> Mono.error(new LoggedRuntimeException()));
    }

    private void trySwitchIfEmptyWithDesiredStackTrace(Mono<Void> switchIfEmptyMono, boolean desiredStackTrace) {
        Mono<Void> loggedError = Mono.fromRunnable(() -> System.out.println("Throwing exception..."))
                .then(switchIfEmptyMono);

        Mono<Object> testedPipeline = Mono.fromRunnable(() -> loggedBusyWork(1000))
                .switchIfEmpty(loggedError);

        StepVerifier.create(testedPipeline)
                .expectErrorMatches(e -> ((LoggedRuntimeException)e).isRelevantStackTrace() == desiredStackTrace)
                .verify();
    }

    private void loggedBusyWork(int millis) {
        long startTime = System.currentTimeMillis();
        System.out.println("Starting busy work @ " + startTime + "...");
        while (System.currentTimeMillis() - startTime < millis);
        System.out.println("End busy work @ " + System.currentTimeMillis());
    }

    static class LoggedRuntimeException extends RuntimeException {

        public LoggedRuntimeException() {
            System.out.println("Creating exception...");
            String stackTrace = getStackTraceStr();
            System.out.println("Stack trace: \n" + stackTrace);
        }

        private String getStackTraceStr() {
            StringWriter writer = new StringWriter();
            printStackTrace(new PrintWriter(writer));
            return writer.toString();
        }

        public boolean isRelevantStackTrace() {
            return getStackTrace()[1].toString().contains(MonoSwitchIfEmptyTest.class.getName());
        }
    }
}

Complete code on GitHub