调试 Spring WebFlux 应用程序可能是一项具有挑战性的任务,尤其是在处理复杂的反应流时。与传统的阻塞应用程序不同,在传统的阻塞应用程序中,堆栈跟踪可以清楚地指示问题的根本原因,反应式应用程序可能更难调试。阻塞代码、并发问题和竞争条件等问题都可能导致难以诊断的细微错误。
在处理错误时,它并不总是与代码相关的问题。它可能是一组因素,例如最近的重构、团队变动、硬期限等。在现实生活中,最终解决大型应用程序的故障是很常见的事情,这些应用程序是由不久前离开公司而你刚刚加入的人制作的。
对某个领域和技术了解一点并不会让您的生活更轻松。
在下面的代码示例中,我想想象一个有缺陷的代码对于最近加入团队的人来说会是什么样子。
将调试此代码更像是一段旅程而不是挑战。对于熟悉 Reactive 应用程序的人来说,根本原因很容易找到。但是,下面的一些做法可能仍然非常有助于修改。
@GetMapping("/greeting/{firstName}/{lastName}") public Mono<String> greeting(@PathVariable String firstName, @PathVariable String lastName) { return Flux.fromIterable(Arrays.asList(firstName, lastName)) .filter(this::wasWorkingNiceBeforeRefactoring) .transform(this::senselessTransformation) .collect(Collectors.joining()) .map(names -> "Hello, " + names); } private boolean wasWorkingNiceBeforeRefactoring(String aName) { // We don't want to greet with John, sorry return !aName.equals("John"); } private Flux<String> senselessTransformation(Flux<String> flux) { return flux .single() .flux() .subscribeOn(Schedulers.parallel()); }
因此,这段代码所做的是:它在作为参数提供的名称前添加“Hello,”。
您的同事 John 告诉您在他的笔记本电脑上一切正常。这是真的:
> curl localhost:8080/greeting/John/Doe > Hello, Doe
但是当你像curl localhost:8080/greeting/Mick/Jagger
一样运行它时,你会看到下一个堆栈跟踪:
java.lang.IndexOutOfBoundsException: Source emitted more than one item at reactor.core.publisher.MonoSingle$SingleSubscriber.onNext(MonoSingle.java:134) ~[reactor-core-3.5.5.jar:3.5.5] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): *__checkpoint ⇢ Handler com.example.demo.controller.GreetingController#greeting(String, String) [DispatcherHandler] *__checkpoint ⇢ HTTP GET "/greeting/Mick/Jagger" [ExceptionHandlingWebHandler] Original Stack Trace: <18 internal lines> at java.base/java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:na] (4 internal lines)
很好,两条线索都不会导致上面的代码示例。
它所揭示的只是 1) 它发生在GreetingController#greeting
方法中,以及 2) 客户端执行了一个`HTTP GET "/greeting/Mick/Jagger
首先也是最简单的尝试是将 .doOnError() 回调添加到问候语链的末尾。
@GetMapping("/greeting/{firstName}/{lastName}") public Mono<String> greeting(@PathVariable String firstName, @PathVariable String lastName) { return Flux.fromIterable(Arrays.asList(firstName, lastName)) // <...> .doOnError(e -> logger.error("Error while greeting", e)); }
不错的尝试,但日志没有显示任何改进。
尽管如此,Reactor 的内部堆栈跟踪:
doOnError
在调试过程中可以/不能提供帮助的一些方法:日志记录:您可以使用doOnError
来记录错误消息并提供有关反应流中出错的更多上下文。这在调试具有许多运算符的复杂流中的问题时特别有用。
恢复: doOnError
也可用于从错误中恢复并继续处理流。例如,您可以使用onErrorResume
在出现错误时提供回退值或流。
调试: doOnError
很可能不会提供任何更好的堆栈跟踪,除了您已经在日志中看到的内容。不要依赖它作为一个好的故障排除程序。
下一站是用log()
方法调用替换之前添加的doOnError()
。越简单越好。默认情况下log()
会观察所有 Reactive Streams 信号并将它们跟踪到 INFO 级别的日志中。
我们可以看到调用了哪些 Reactive 方法( onSubscribe
、 request
和onError
)。此外,了解从哪些线程(池)中调用了这些方法可能是非常有用的信息。但是,它与我们的案例无关。
关于线程池
线程名称ctor-http-nio-2
代表reactor-http-nio-2
。响应式方法onSubscribe()
和request()
在 IO 线程池(调度程序)上执行。这些任务在提交它们的线程上立即执行。
通过在senselessTransformation
中使用.subscribeOn(Schedulers.parallel())
我们指示 Reactor 在另一个线程池上订阅更多元素。这就是为什么onError
在parallel-1
线程上执行的原因。
您可以在本文中阅读有关线程池的更多信息。
log()
方法允许您将日志记录语句添加到流中,从而更轻松地跟踪数据流和诊断问题。如果我们有更复杂的数据流,比如 flatMap、子链、阻塞调用等,我们将从将它们全部记录下来受益匪浅。对于日常使用来说,这是一件非常简单和美好的事情。但是,我们仍然不知道根本原因。
指令Hooks.onOperatorDebug()
告诉 Reactor 为反应流中的所有操作符启用调试模式,允许更详细的错误消息和堆栈跟踪。
稍后观察到错误时,将使用详细说明原始装配线堆栈的抑制异常来丰富它们。必须在实际调用生产者(例如 Flux.map、Mono.fromCallable)之前调用以拦截正确的堆栈信息。
该指令应在每个运行时调用一次。最好的地方之一是配置或主类。对于我们的用例,它将是:
public Mono<String> greeting(@PathVariable String firstName, @PathVariable String lastName) { Hooks.onOperatorDebug(); return // <...> }
通过添加Hooks.onOperatorDebug()
我们终于可以在调查中取得进展。 Stacktrace 更有用:
在第 42 行,我们有single()
调用。
不要向上滚动,接下来是senselessTransformation
:
private Flux<String> senselessTransformation(Flux<String> flux) { return flux .single() // line 42 .flux() .subscribeOn(Schedulers.parallel()); }
这就是根本原因。
single()
从 Flux 源发出一项,或为具有多个元素的源发出IndexOutOfBoundsException
信号。这意味着该方法中的通量会发出不止一项。通过在调用层次结构中往上走,我们看到最初有一个包含两个元素的 Flux Flux.fromIterable(Arrays.asList(firstName, lastName))
。
过滤方法wasWorkingNiceBeforeRefactoring
在它等于John时从 flux 中删除一个项目。这就是代码适用于名为 John 的大学的原因。嗯。
Hooks.onOperatorDebug()
在调试复杂的反应流时特别有用,因为它提供了有关如何处理流的更多详细信息。但是,启用调试模式会影响应用程序的性能(由于填充的堆栈跟踪),因此它应该只在开发和调试期间使用,而不应在生产中使用。
为了以最小的性能影响实现与Hooks.onOperatorDebug()
几乎相同的效果,有一个特殊的checkpoint()
运算符。它将为流的该部分启用调试模式,同时不影响流的其余部分。
public Mono<String> greeting(@PathVariable String firstName, @PathVariable String lastName) { return Flux.fromIterable(Arrays.asList(firstName, lastName)) .filter(this::wasWorkingNiceBeforeRefactoring) /* new */ .checkpoint("After filtering") .transform(this::senselessTransformation) /* new */ .checkpoint("After transformation") .collect(Collectors.joining()) .map(names -> "Hello, " + names); }
看看日志:
这个检查点故障告诉我们,在我们描述为After transformation 的第二个检查点之后观察到了错误。这并不意味着在执行过程中没有到达第一个检查点。是的,但是错误仅在第二个之后才开始出现。这就是为什么我们看不到After filtering 的原因。
您还可以看到细分中提到的另外两个检查点,来自DispatcherHandler和ExceptionHandlingWebHandler 。他们是在我们设置的之后到达的,一直到调用层次结构。
除了描述之外,您还可以通过将true
作为第二个参数添加到checkpoint()
方法来强制 Reactor 为您的检查点生成堆栈跟踪。请务必注意,生成的堆栈跟踪将引导您找到带有检查点的行。它不会为原始异常填充堆栈跟踪。所以它没有多大意义,因为您可以通过提供描述轻松找到检查点。
通过遵循这些最佳实践,您可以简化调试过程并快速识别和解决 Spring WebFlux 应用程序中的问题。无论您是经验丰富的开发人员还是刚刚开始响应式编程,这些技巧都将帮助您提高代码的质量和可靠性,并为您的用户提供更好的体验。